From c6c41014dd0a2627595653aba3fd7da1341d9a0e Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Wed, 18 Nov 2020 16:40:50 +0200 Subject: [PATCH 001/129] Add Amx bidder (#1004) --- .../prebid/server/bidder/amx/AmxBidder.java | 247 ++++++++++++++++ .../server/bidder/amx/model/AmxBidExt.java | 21 ++ .../openrtb/ext/request/amx/ExtImpAmx.java | 16 + .../config/bidder/AmxConfiguration.java | 56 ++++ src/main/resources/bidder-config/amx.yaml | 25 ++ .../resources/static/bidder-params/amx.json | 16 + .../server/bidder/amx/AmxBidderTest.java | 273 ++++++++++++++++++ .../java/org/prebid/server/it/AmxTest.java | 60 ++++ .../it/openrtb2/amx/test-amx-bid-request.json | 79 +++++ .../openrtb2/amx/test-amx-bid-response.json | 24 ++ .../amx/test-auction-amx-request.json | 66 +++++ .../amx/test-auction-amx-response.json | 61 ++++ .../openrtb2/amx/test-cache-amx-request.json | 21 ++ .../openrtb2/amx/test-cache-amx-response.json | 7 + .../server/it/test-application.properties | 4 + 15 files changed, 976 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/amx/AmxBidder.java create mode 100644 src/main/java/org/prebid/server/bidder/amx/model/AmxBidExt.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/amx/ExtImpAmx.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/AmxConfiguration.java create mode 100644 src/main/resources/bidder-config/amx.yaml create mode 100644 src/main/resources/static/bidder-params/amx.json create mode 100644 src/test/java/org/prebid/server/bidder/amx/AmxBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/AmxTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/amx/test-cache-amx-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/amx/test-cache-amx-response.json diff --git a/src/main/java/org/prebid/server/bidder/amx/AmxBidder.java b/src/main/java/org/prebid/server/bidder/amx/AmxBidder.java new file mode 100644 index 00000000000..602f47ab5d6 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/amx/AmxBidder.java @@ -0,0 +1,247 @@ +package org.prebid.server.bidder.amx; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.amx.model.AmxBidExt; +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.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.amx.ExtImpAmx; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * AMX {@link Bidder} implementation. + */ +public class AmxBidder implements Bidder { + + private static final TypeReference> AMX_EXT_TYPE_REFERENCE = + new TypeReference>() { + }; + + private static final String ADAPTER_VERSION = "pbs1.0"; + private static final String VERSION_PARAM = "v"; + private static final String VAST_SEARCH_POINT = ""; + private static final String VAST_IMPRESSION_FORMAT = ""; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AmxBidder(String endpointUrl, JacksonMapper mapper) { + this.mapper = Objects.requireNonNull(mapper); + this.endpointUrl = new URIBuilder() + .setPath(HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl))) + .addParameter(VERSION_PARAM, ADAPTER_VERSION) + .toString(); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(); + final List errors = new ArrayList<>(); + String publisherId = null; + for (Imp imp : request.getImp()) { + try { + final ExtImpAmx extImpAmx = parseImpExt(imp); + final String tagId = extImpAmx.getTagId(); + if (StringUtils.isNotBlank(tagId)) { + publisherId = tagId; + } + + final String adUnitId = extImpAmx.getAdUnitId(); + if (StringUtils.isNotBlank(adUnitId)) { + modifiedImps.add(imp.toBuilder().tagid(adUnitId).build()); + } + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest outgoingRequest = createOutgoingRequest(request, publisherId, modifiedImps); + + return Result.of(Collections.singletonList( + HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .payload(outgoingRequest) + .body(mapper.encode(outgoingRequest)) + .build()), errors); + } + + private ExtImpAmx parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), AMX_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private BidRequest createOutgoingRequest(BidRequest request, String publisherId, List imps) { + final BidRequest.BidRequestBuilder outgoingRequest = request.toBuilder(); + + if (StringUtils.isNotBlank(publisherId)) { + final App app = request.getApp(); + if (app != null) { + outgoingRequest + .app(app.toBuilder() + .publisher(resolvePublisher(app.getPublisher(), publisherId)) + .build()); + } + + final Site site = request.getSite(); + if (site != null) { + outgoingRequest + .site(site.toBuilder() + .publisher(resolvePublisher(site.getPublisher(), publisherId)) + .build()); + } + } + + return outgoingRequest.imp(imps).build(); + } + + private Publisher resolvePublisher(Publisher publisher, String publisherId) { + return publisher != null + ? publisher.toBuilder().id(publisherId).build() + : Publisher.builder().id(publisherId).build(); + } + + @Override + public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || bidResponse.getSeatbid() == null) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> createBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private BidderBid createBidderBid(Bid bid, String cur, List errors) { + AmxBidExt amxBidExt = null; + try { + amxBidExt = parseBidderExt(bid.getExt()); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + + final BidType bidType = getMediaType(amxBidExt); + + final Bid updatedBid; + try { + updatedBid = bidType == BidType.video ? updateVideoBid(bid, amxBidExt) : bid; + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + return BidderBid.of(updatedBid, bidType, cur); + } + + private AmxBidExt parseBidderExt(ObjectNode ext) { + if (ext == null || StringUtils.isBlank(ext.toPrettyString())) { + return AmxBidExt.of(null, null); + } + + try { + return mapper.mapper().convertValue(ext, AmxBidExt.class); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static BidType getMediaType(AmxBidExt bidExt) { + return StringUtils.isNotBlank(bidExt.getStartDelay()) + ? BidType.video + : BidType.banner; + } + + private static Bid updateVideoBid(Bid bid, AmxBidExt bidExt) { + final String adm = bid.getAdm(); + final String bidId = bid.getId(); + validateAdm(adm, bidId); + return bid.toBuilder() + .nurl("") + .adm(updateAdm(bidExt, bid.getNurl(), adm, bidId)) + .build(); + } + + private static void validateAdm(String adm, String bidId) { + if (StringUtils.isBlank(adm)) { + throw new PreBidException(String.format("Adm should not be blank in bidder: %s", bidId)); + } + + if (!adm.contains(VAST_SEARCH_POINT)) { + throw new PreBidException(String.format("Adm should contain vast search point in bidder: %s", bidId)); + } + } + + private static String updateAdm(AmxBidExt bidExt, String nurl, String adm, String bidId) { + final StringBuilder updatedAdm = new StringBuilder(); + validateAdm(adm, bidId); + + int lastInd = adm.lastIndexOf(VAST_SEARCH_POINT); + + updatedAdm.append(adm, 0, lastInd + VAST_SEARCH_POINT.length()); + addValueIfNotEmpty(nurl, updatedAdm); + + for (String himp : bidExt.getHimp()) { + addValueIfNotEmpty(himp, updatedAdm); + } + + return updatedAdm + .append(adm.substring(lastInd + VAST_SEARCH_POINT.length())) + .toString(); + } + + private static void addValueIfNotEmpty(String value, StringBuilder dest) { + if (StringUtils.isNotBlank(value)) { + dest.append(String.format(VAST_IMPRESSION_FORMAT, value)); + } + } +} + diff --git a/src/main/java/org/prebid/server/bidder/amx/model/AmxBidExt.java b/src/main/java/org/prebid/server/bidder/amx/model/AmxBidExt.java new file mode 100644 index 00000000000..1e00531db78 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/amx/model/AmxBidExt.java @@ -0,0 +1,21 @@ +package org.prebid.server.bidder.amx.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value +public class AmxBidExt { + + @JsonProperty("himp") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + List himp; + + @JsonProperty("startdelay") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + String startDelay; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/amx/ExtImpAmx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/amx/ExtImpAmx.java new file mode 100644 index 00000000000..1ad50749816 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/amx/ExtImpAmx.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.amx; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpAmx { + + @JsonProperty("tagId") + String tagId; + + @JsonProperty("adUnitId") + String adUnitId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AmxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AmxConfiguration.java new file mode 100644 index 00000000000..6cdf5db7287 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AmxConfiguration.java @@ -0,0 +1,56 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.amx.AmxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.BidderInfoCreator; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/amx.yaml", factory = YamlPropertySourceFactory.class) +public class AmxConfiguration { + + private static final String BIDDER_NAME = "amx"; + + @Value("${external-url}") + @NotBlank + private String externalUrl; + + @Autowired + private JacksonMapper mapper; + + @Autowired + @Qualifier("amxConfigurationProperties") + private BidderConfigurationProperties configProperties; + + @Bean("amxConfigurationProperties") + @ConfigurationProperties("adapters.amx") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps amxBidderDeps() { + final UsersyncConfigurationProperties usersync = configProperties.getUsersync(); + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(configProperties) + .bidderInfo(BidderInfoCreator.create(configProperties)) + .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl)) + .bidderCreator(() -> new AmxBidder(configProperties.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/amx.yaml b/src/main/resources/bidder-config/amx.yaml new file mode 100644 index 00000000000..2b59825225b --- /dev/null +++ b/src/main/resources/bidder-config/amx.yaml @@ -0,0 +1,25 @@ +adapters: + amx: + enabled: false + endpoint: http://pbs.amxrtb.com/auction/openrtb + pbs-enforces-gdpr: true + pbs-enforces-ccpa: true + modifying-vast-xml-allowed: true + deprecated-names: + aliases: + meta-info: + maintainer-email: prebid@amxrtb.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 0 + usersync: + url: https://prebid.a-mo.net/cchain/0?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&cb= + redirect-url: /setuid?bidder=amx&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid= + cookie-family-name: amx + type: redirect + support-cors: false diff --git a/src/main/resources/static/bidder-params/amx.json b/src/main/resources/static/bidder-params/amx.json new file mode 100644 index 00000000000..f9b1b26b3db --- /dev/null +++ b/src/main/resources/static/bidder-params/amx.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AMX RTB Adapter Params", + "description": "A schema to validate params accepted by the AMX adapter", + "type": "object", + "properties": { + "tagId" : { + "type": "string", + "description": "Set a tagId (overrides site.publisher.id, or app.publisher.id)" + }, + "adUnitId": { + "type": "string", + "description": "Override imp.tagid value to provide a custom value in AMX ad unit ID reporting" + } + } +} diff --git a/src/test/java/org/prebid/server/bidder/amx/AmxBidderTest.java b/src/test/java/org/prebid/server/bidder/amx/AmxBidderTest.java new file mode 100644 index 00000000000..1aafc804b8c --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/amx/AmxBidderTest.java @@ -0,0 +1,273 @@ +package org.prebid.server.bidder.amx; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import 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 org.prebid.server.proto.openrtb.ext.request.amx.ExtImpAmx; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import static java.util.Collections.singletonList; +import static java.util.function.Function.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; + +public class AmxBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.com/prebid/bid"; + + private AmxBidder amxBidder; + + @Before + public void setUp() { + amxBidder = new AmxBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new AmxBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + // when + final Result>> result = amxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_input + && error.getMessage().startsWith("Cannot deserialize instance")); + } + + @Test + public void makeHttpRequestsShouldCreateCorrectURL() { + // given + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build())); + + // when + final Result>> result = amxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.com/prebid/bid?v=pbs1.0"); + } + + @Test + public void makeHttpRequestsShouldUpdateRequestAndImps() { + // given + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.app(App.builder().build()).site(Site.builder().build()), + impBuilder -> impBuilder.banner(Banner.builder().build())); + + // when + final Result>> result = amxBidder.makeHttpRequests(bidRequest); + + // then + final BidRequest expectedBidRequest = BidRequest.builder() + .app(App.builder().publisher(Publisher.builder().id("testTagId").build()).build()) + .site(Site.builder().publisher(Publisher.builder().id("testTagId").build()).build()) + .imp(singletonList(Imp.builder() + .id("123") + .banner(Banner.builder().build()) + .tagid("testAdUnitId") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpAmx.of("testTagId", "testAdUnitId")))) + .build())).build(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .containsExactly(expectedBidRequest); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final HttpCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = amxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_server_response + && error.getMessage().startsWith("Failed to decode: Unrecognized token")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(null)); + + // when + final Result> result = amxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = amxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidIfBannerIsBidExtNotPresent() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder().build(), mapper.writeValueAsString(givenBidResponse(identity()))); + + // when + final Result> result = amxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfStartDelayIsPresentInBidExt() throws JsonProcessingException { + // given + final ObjectNode bidExt = mapper.createObjectNode(); + bidExt.put("himp", mapper.convertValue(Arrays.asList("someHintVAlue1", "someHintValue2"), JsonNode.class)); + bidExt.put("startdelay", "2"); + final HttpCall httpCall = givenHttpCall(BidRequest.builder().build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder + .adm("ExistingAdm") + .nurl("nurlValue") + .ext(bidExt)))); + + // when + final Result> result = amxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + final String expectedAdm = "ExistingAdm" + + "" + + "" + + ""; + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder() + .nurl("") + .ext(bidExt) + .adm(expectedAdm) + .build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorIfAdmNotContainSearchPoint() throws JsonProcessingException { + // given + final ObjectNode bidExt = mapper.createObjectNode(); + bidExt.put("startdelay", "2"); + final HttpCall httpCall = givenHttpCall(BidRequest.builder().build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder + .id("bidId") + .adm("no_point") + .ext(bidExt)))); + + // when + final Result> result = amxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsExactly( + BidderError.badServerResponse("Adm should contain vast search point in bidder: bidId")); + } + + @Test + public void makeBidsShouldReturnErrorIfAdmIsNotPresent() throws JsonProcessingException { + // given + final ObjectNode bidExt = mapper.createObjectNode(); + bidExt.put("startdelay", "2"); + final HttpCall httpCall = givenHttpCall(BidRequest.builder().build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder + .id("bidId") + .ext(bidExt)))); + + // when + final Result> result = amxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("Adm should not be blank in bidder: bidId")); + } + + private static BidRequest givenBidRequest( + Function bidRequestCustomizer, + Function impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static BidRequest givenBidRequest(Function impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static Imp givenImp(Function impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .banner(Banner.builder().id("banner_id").build()).ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpAmx.of("testTagId", "testAdUnitId"))))) + .build(); + } + + private static BidResponse givenBidResponse(Function bidCustomizer) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static HttpCall givenHttpCall(BidRequest bidRequest, String body) { + return HttpCall.success( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} + diff --git a/src/test/java/org/prebid/server/it/AmxTest.java b/src/test/java/org/prebid/server/it/AmxTest.java new file mode 100644 index 00000000000..2ec0bb9fd15 --- /dev/null +++ b/src/test/java/org/prebid/server/it/AmxTest.java @@ -0,0 +1,60 @@ +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.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToIgnoreCase; +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 io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; + +@RunWith(SpringRunner.class) +public class AmxTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAmx() throws IOException, JSONException { + // given + // Amx bid response for imp + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/amx-exchange")) + .withQueryParam("v", equalTo("pbs1.0")) + .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/amx/test-amx-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/amx/test-amx-bid-response.json")))); + + // pre-bid cache + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/amx/test-cache-amx-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/amx/test-cache-amx-response.json")))); + + // when + final Response response = given(SPEC) + .header("Referer", "http://www.example.com") + .header("X-Forwarded-For", "193.168.244.1") + .header("User-Agent", "userAgent") + .header("Origin", "http://www.example.com") + // this uids cookie value stands for {"uids":{"amx":"AMX-UID"}} + .cookie("uids", "eyJ1aWRzIjp7ImFteCI6IkFNWC1VSUQifX0=") + .body(jsonFrom("openrtb2/amx/test-auction-amx-request.json")) + .post("/openrtb2/auction"); + + // then + final String expectedAuctionResponse = openrtbAuctionResponseFrom( + "openrtb2/amx/test-auction-amx-response.json", + response, singletonList("amx")); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-request.json new file mode 100644 index 00000000000..3c064c54c7c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-request.json @@ -0,0 +1,79 @@ +{ + "id": "some-request-id", + "imp": [ + { + "id": "testimpid", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "tagid": "testAdUnitId", + "ext" : { + "bidder" : { + "tagId" : "testTagId", + "adUnitId" : "testAdUnitId" + } + } + } + ], + "site": { + "domain": "example.com", + "page": "http://www.example.com", + "publisher": { + "id": "testTagId" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "user": { + "buyeruid": "AMX-UID" + }, + "at": 1, + "tmax": 1000, + "cur": [ + "USD" + ], + "source": { + "fd": 1, + "tid": "tid" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "cache": { + "bids": {} + }, + "auctiontimestamp": 1000, + "channel": { + "name": "web" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-response.json new file mode 100644 index 00000000000..3f362e2f62d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-response.json @@ -0,0 +1,24 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "testid", + "impid": "testimpid", + "adm": "00:00:15", + "nurl": "https://example.com/nurl", + "cid": "8048", + "ext": { + "himp": ["https://example.com/imp-tracker/pixel.gif?param=1¶m2=2"], + "startdelay": 0 + } + } + ], + "type": "video" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-request.json new file mode 100644 index 00000000000..f38e58bc429 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-request.json @@ -0,0 +1,66 @@ +{ + "id": "some-request-id", + "imp": [ + { + "id": "testimpid", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "ext": { + "amx": { + "tagId": "testTagId", + "adUnitId": "testAdUnitId" + } + }, + "tagid" : "testAdUnitId" + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "publisher": { + "id": "testTagId" + } + }, + "at": 1, + "tmax": 1000, + "cur": [ + "USD" + ], + "source": { + "fd": 1, + "tid": "tid" + }, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + } + }, + "cache": { + "bids": {} + }, + "auctiontimestamp": 1000 + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json new file mode 100644 index 00000000000..6945a023670 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json @@ -0,0 +1,61 @@ +{ + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "testid", + "impid": "testimpid", + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "adm": "00:00:15", + "nurl": "", + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_pb": "0.00", + "hb_cache_id_amx": "3c0769d8-0dd9-465c-8bf3-f570605ba698", + "hb_bidder_amx": "amx", + "hb_bidder": "amx", + "hb_cache_id": "3c0769d8-0dd9-465c-8bf3-f570605ba698", + "hb_pb_amx": "0.00", + "hb_cache_host": "{{ cache.host }}", + "hb_cache_host_amx": "{{ cache.host }}", + "hb_cache_path": "{{ cache.path }}", + "hb_cache_path_amx": "{{ cache.path }}" + }, + "cache": { + "bids": { + "url": "{{ cache.resource_url }}3c0769d8-0dd9-465c-8bf3-f570605ba698", + "cacheId": "3c0769d8-0dd9-465c-8bf3-f570605ba698" + } + } + }, + "bidder": { + "himp": [ + "https://example.com/imp-tracker/pixel.gif?param=1¶m2=2" + ], + "startdelay": 0 + } + } + } + ], + "seat": "amx", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "amx": "{{ amx.response_time_ms }}", + "cache": "{{ cache.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 1000 + }, + "tmaxrequest": 1000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-cache-amx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-cache-amx-request.json new file mode 100644 index 00000000000..5764e471148 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-cache-amx-request.json @@ -0,0 +1,21 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "testid", + "price": 0.01, + "nurl" : "", + "adm": "00:00:15", + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "impid": "testimpid", + "ext": { + "himp": ["https://example.com/imp-tracker/pixel.gif?param=1¶m2=2"], + "startdelay": 0 + } + } + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-cache-amx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-cache-amx-response.json new file mode 100644 index 00000000000..c0100536be1 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-cache-amx-response.json @@ -0,0 +1,7 @@ +{ + "responses": [ + { + "uuid": "3c0769d8-0dd9-465c-8bf3-f570605ba698" + } + ] +} 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 2107574be47..17a3e82504d 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -62,6 +62,10 @@ adapters.aja.enabled=true adapters.aja.endpoint=http://localhost:8090/aja adapters.aja.pbs-enforces-gdpr=true adapters.aja.usersync.url=//v1/sync/ssp?ssp= +adapters.amx.enabled=true +adapters.amx.endpoint=http://localhost:8090/amx-exchange +adapters.amx.pbs-enforces-gdpr=true +adapters.amx.usersync.url=//amx-usersync adapters.appnexus.enabled=true adapters.appnexus.endpoint=http://localhost:8090/appnexus-exchange adapters.appnexus.pbs-enforces-gdpr=true From e39955da12c2eaae9d572c76be6096d8eab0fcde Mon Sep 17 00:00:00 2001 From: bretg Date: Wed, 18 Nov 2020 10:40:59 -0500 Subject: [PATCH 002/129] readme: updated for repo move (#1021) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e7edc4aca48..36434dd5811 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -### This code is being used in production by multiple Prebid.org members, but is not the "official" version. See https://github.com/prebid/prebid-server/ +### This is the Java version of Prebid Server. See the Prebid Server [Feature List](https://docs.prebid.org/prebid-server/features/pbs-feature-idx.html) and [FAQ entry](https://docs.prebid.org/faq/prebid-server-faq.html#why-are-there-two-versions-of-prebid-server-are-they-kept-in-sync) to understand the differences between PBS-Java and [PBS-Go](https://github.com/prebid/prebid-server). -# Prebid Server +# Prebid Server (Java) [![GitHub version](https://badge.fury.io/gh/rubicon-project%2fprebid-server-java.svg)](http://badge.fury.io/gh/rubicon-project%2fprebid-server-java) [![Language grade: Java](https://img.shields.io/lgtm/grade/java/g/rubicon-project/prebid-server-java.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/rubicon-project/prebid-server-java/context:java) From dfa94669022a968420751f7f419c063adb003cf2 Mon Sep 17 00:00:00 2001 From: pragnesh Date: Thu, 19 Nov 2020 14:47:14 +0530 Subject: [PATCH 003/129] fix documentation link (#1026) --- README.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 36434dd5811..a4809e78da1 100644 --- a/README.md +++ b/README.md @@ -84,28 +84,28 @@ and verify response status is `200 OK`. # Documentation ## Development -- [Differences Between Prebid Server Go and Java](differenceBetweenPBSGo-and-Java.md) -- [Endpoints](endpoints) -- [Adding new bidder](developers/add-new-bidder.md) -- [Adding new analytics module](developers/add-new-analytics-module.md) -- [Adding viewability support](developers/add-viewability-vendors.md) -- [Auction result post-processing](developers/auction-result-post-processing.md) -- [Cookie Syncs](developers/cookie-syncs.md) -- [Stored Requests](developers/stored-requests.md) -- [Unit Tests](developers/unit-tests.md) -- [GDPR](developers/gdpr.md) +- [Differences Between Prebid Server Go and Java](docs/differenceBetweenPBSGo-and-Java.md) +- [Endpoints](docs/endpoints) +- [Adding new bidder](docs/developers/add-new-bidder.md) +- [Adding new analytics module](docs/developers/add-new-analytics-module.md) +- [Adding viewability support](docs/developers/add-viewability-vendors.md) +- [Auction result post-processing](docs/developers/auction-result-post-processing.md) +- [Cookie Syncs](docs/developers/cookie-syncs.md) +- [Stored Requests](docs/developers/stored-requests.md) +- [Unit Tests](docs/developers/unit-tests.md) +- [GDPR](docs/developers/gdpr.md) ## Maintenance -- [Build for local](build.md) -- [Build for AWS](build-aws.md) -- [Configure application](config.md) - - [Full list of configuration options](config-app.md) - - [Application settings](application-settings.md) -- [Run with optimizations](run.md) -- [Metrics](metrics.md) +- [Build for local](docs/build.md) +- [Build for AWS](docs/build-aws.md) +- [Configure application](docs/config.md) + - [Full list of configuration options](docs/config-app.md) + - [Application settings](docs/application-settings.md) +- [Run with optimizations](docs/run.md) +- [Metrics](docs/metrics.md) ## Contributing -- [Contributing](contributing.md) -- [Code Style](code-style.md) -- [Code Review](code-reviews.md) -- [Versioning](versioning.md) +- [Contributing](docs/contributing.md) +- [Code Style](docs/code-style.md) +- [Code Review](docs/code-reviews.md) +- [Versioning](docs/versioning.md) From 52eed36ce32708213deaa6512d074a430c484d59 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Thu, 19 Nov 2020 11:40:05 +0200 Subject: [PATCH 004/129] Add video support to EMX Digital bidder (#988) --- .../bidder/emxdigital/EmxDigitalBidder.java | 77 ++++-- .../resources/bidder-config/emxdigital.yaml | 3 + .../emxdigital/EmxDigitalBidderTest.java | 228 +++++++++++++++++- .../test-auction-emxdigital-response.json | 2 +- 4 files changed, 294 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java b/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java index a569edbd0cb..f9f2bcc8a9a 100644 --- a/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java +++ b/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java @@ -1,12 +1,14 @@ package org.prebid.server.bidder.emxdigital; import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -41,7 +43,8 @@ public class EmxDigitalBidder implements Bidder { - private static final String DEFAULT_BID_CURRENCY = "USD"; + private static final String USD_CURRENCY = "USD"; + private static final Integer PROTOCOL_VAST_40 = 7; private static final TypeReference> EMXDIGITAL_EXT_TYPE_REFERENCE = new TypeReference>() { @@ -79,7 +82,7 @@ public Result>> makeHttpRequests(BidRequest request // Handle request errors and formatting to be sent to EMX private BidRequest makeBidRequest(BidRequest request) { - final boolean isSecure = isSecure(request.getSite()); + final boolean isSecure = resolveUrl(request).startsWith("https"); final List modifiedImps = request.getImp().stream() .map(imp -> modifyImp(imp, isSecure, unpackImpExt(imp))) @@ -90,10 +93,21 @@ private BidRequest makeBidRequest(BidRequest request) { .build(); } - private static boolean isSecure(Site site) { - return site != null - && StringUtils.isNotBlank(site.getPage()) - && site.getPage().startsWith("https"); + private static String resolveUrl(BidRequest request) { + final Site site = request.getSite(); + final String page = site != null ? site.getPage() : null; + if (StringUtils.isNotBlank(page)) { + return page; + } + final App app = request.getApp(); + if (app != null) { + if (StringUtils.isNotBlank(app.getDomain())) { + return app.getDomain(); + } else if (StringUtils.isNotBlank(app.getStoreurl())) { + return app.getStoreurl(); + } + } + return ""; } private ExtImpEmxDigital unpackImpExt(Imp imp) { @@ -122,13 +136,16 @@ private ExtImpEmxDigital unpackImpExt(Imp imp) { } private static Imp modifyImp(Imp imp, boolean isSecure, ExtImpEmxDigital extImpEmxDigital) { - final Banner banner = modifyImpBanner(imp.getBanner()); final Imp.ImpBuilder impBuilder = imp.toBuilder() .tagid(extImpEmxDigital.getTagid()) - .secure(BooleanUtils.toInteger(isSecure)) - .banner(banner) - .ext(null); + .secure(BooleanUtils.toInteger(isSecure)); + final Video video = imp.getVideo(); + if (video != null) { + impBuilder.video(modifyImpVideo(video)); + } else { + impBuilder.banner(modifyImpBanner(imp.getBanner())); + } final String stringBidfloor = extImpEmxDigital.getBidfloor(); if (StringUtils.isBlank(stringBidfloor)) { @@ -144,10 +161,36 @@ private static Imp modifyImp(Imp imp, boolean isSecure, ExtImpEmxDigital extImpE return impBuilder .bidfloor(bidfloor) - .bidfloorcur(DEFAULT_BID_CURRENCY) + .bidfloorcur(USD_CURRENCY) .build(); } + private static Video modifyImpVideo(Video video) { + if (CollectionUtils.isEmpty(video.getMimes())) { + throw new PreBidException("Video: missing required field mimes"); + } + if (isNotPresentSize(video.getH()) && isNotPresentSize(video.getW())) { + throw new PreBidException("Video: Need at least one size to build request"); + } + if (CollectionUtils.isNotEmpty(video.getProtocols())) { + final List updatedProtocols = removeVast40Protocols(video.getProtocols()); + return video.toBuilder().protocols(updatedProtocols).build(); + } + + return video; + } + + private static boolean isNotPresentSize(Integer size) { + return Objects.isNull(size) || size == 0; + } + + // not supporting VAST protocol 7 (VAST 4.0); + private static List removeVast40Protocols(List protocols) { + return protocols.stream() + .filter(protocol -> !protocol.equals(PROTOCOL_VAST_40)) + .collect(Collectors.toList()); + } + private static Banner modifyImpBanner(Banner banner) { if (banner == null) { throw new PreBidException("Request needs to include a Banner object"); @@ -186,8 +229,9 @@ private static MultiMap makeHeaders(BidRequest request) { } final Site site = request.getSite(); - if (site != null && StringUtils.isNotBlank(site.getPage())) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getPage()); + final String page = site != null ? site.getPage() : null; + if (StringUtils.isNotBlank(page)) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, page); } return headers; @@ -241,10 +285,15 @@ private static List bidsFromResponse(BidResponse bidResponse) { .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(modifyBid(bid), BidType.banner, bidResponse.getCur())) + .map(bid -> BidderBid.of(modifyBid(bid), getBidType(bid.getAdm()), bidResponse.getCur())) .collect(Collectors.toList()); } + private static BidType getBidType(String bidAdm) { + return StringUtils.containsAny(bidAdm, ">> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + final Imp expectedImp = Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .video(Video.builder() + .mimes(Collections.singletonList("someMime")) + .protocols(Arrays.asList(1, 2)) + .w(100) + .h(100) + .build()) + .tagid("123") + .secure(0) + .bidfloor(new BigDecimal("2")) + .bidfloorcur("USD") + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .containsOnly(expectedImp); + } + + @Test + public void shouldThrowExceptionIfVideoDoNotHaveAtLeastOneSizeParameter() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .video(Video.builder().mimes(Collections.singletonList("someMime")).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Video: Need at least one size to build request")); + } + + @Test + public void shouldThrowExceptionIfVideoDoNotHaveAnyMimeParameter() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .video(Video.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Video: missing required field mimes")); + } + + @Test + public void requestSecureShouldBeOneIfPageStartsWithHttps() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .site(Site.builder().page("https://exmaple/").build()) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(request -> request.getImp().get(0).getSecure()) + .containsOnly(1); + } + + @Test + public void requestSecureShouldBeOneIfUrlStartsWithHttps() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .app(App.builder().domain("https://exmaple/").build()) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getSecure) + .containsOnly(1); + } + + @Test + public void requestSecureShouldBe1IfStoreUrlStartsWithHttps() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .app(App.builder().storeurl("https://exmaple/").build()) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getSecure) + .containsOnly(1); + } + + @Test + public void requestSecureShouldBe0IfPageDoNotStartsWithHttps() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .site(Site.builder().page("http://exmaple/").build()) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getSecure) + .containsOnly(0); + } + @Test public void makeHttpRequestsShouldModifyBannerFormatAndWidthAndHeightWhenRequestBannerWidthAndHeightIsNull() { // given @@ -255,6 +440,7 @@ public void makeHttpRequestsShouldModifyBannerFormatAndWidthAndHeightWhenRequest .format(singletonList(Format.builder().h(30).w(31).build())).build(); final Imp expectedImp = Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("1", "asd")))) .banner(expectedBanner) .tagid("1") .secure(0) @@ -395,7 +581,7 @@ public void makeBidsShouldReturnEmptyListWhenBidResponseSeatBidIsNull() } @Test - public void makeBidsShouldAlwaysReturnBannerBidWithChangedBidImpId() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBidWithChangedBidImpId() throws JsonProcessingException { // given final HttpCall httpCall = givenHttpCall( BidRequest.builder() @@ -413,6 +599,46 @@ public void makeBidsShouldAlwaysReturnBannerBidWithChangedBidImpId() throws Json .containsOnly(BidderBid.of(Bid.builder().id("321").impid("321").build(), banner, "USD")); } + @Test + public void makeBidsShouldReturnVideoBidIfAdmContainsVastPrefix() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.id("321").adm("> result = emxDigitalBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().id("321").adm(" httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.id("321").adm("> result = emxDigitalBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().id("321").adm(" bidCustomizer) { return BidResponse.builder() diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json index 9c5c067a286..6c9e0d2158f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json @@ -50,7 +50,7 @@ "emx_digital": [ { "uri": "{{ emx_digital.exchange_uri }}?t=1000&ts=2060541160", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0,\"ext\":{\"bidder\":{\"tagid\":\"25251\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"some_test_auction\",\"seatbid\":[{\"seat\":\"12356\",\"bid\":[{\"id\":\"uuid\",\"adm\":\"
\",\"impid\":\"uuid\",\"ttl\":300,\"crid\":\"94395500\",\"w\":300,\"price\":2.942808,\"adid\":\"94395500\",\"h\":250}]}],\"cur\":\"USD\"}", "status": 200 } From d058fbd73470119802aa0736b9d5bf51935ace25 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Thu, 19 Nov 2020 14:28:52 +0200 Subject: [PATCH 005/129] Add Nobid bidder (#1005) --- .../server/bidder/nobid/NobidBidder.java | 106 ++++++++++ .../ext/request/nobid/ExtImpNobid.java | 16 ++ .../config/bidder/NobidConfiguration.java | 56 +++++ src/main/resources/bidder-config/nobid.yaml | 25 +++ .../resources/static/bidder-params/nobid.json | 17 ++ .../server/bidder/nobid/NobidBidderTest.java | 196 ++++++++++++++++++ .../java/org/prebid/server/it/NobidTest.java | 60 ++++++ .../nobid/test-auction-nobid-request.json | 63 ++++++ .../nobid/test-auction-nobid-response.json | 53 +++++ .../nobid/test-cache-nobid-request.json | 15 ++ .../nobid/test-cache-nobid-response.json | 7 + .../nobid/test-nobid-bid-request.json | 76 +++++++ .../nobid/test-nobid-bid-response.json | 18 ++ .../server/it/test-application.properties | 4 + 14 files changed, 712 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/nobid/NobidBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/nobid/ExtImpNobid.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/NobidConfiguration.java create mode 100644 src/main/resources/bidder-config/nobid.yaml create mode 100644 src/main/resources/static/bidder-params/nobid.json create mode 100644 src/test/java/org/prebid/server/bidder/nobid/NobidBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/NobidTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/nobid/test-cache-nobid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/nobid/test-cache-nobid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/nobid/test-nobid-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/nobid/test-nobid-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/nobid/NobidBidder.java b/src/main/java/org/prebid/server/bidder/nobid/NobidBidder.java new file mode 100644 index 00000000000..e373d77c5e1 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nobid/NobidBidder.java @@ -0,0 +1,106 @@ +package org.prebid.server.bidder.nobid; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.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.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Nobid {@link Bidder} implementation. + */ +public class NobidBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public NobidBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + + return Result.of(Collections.singletonList( + HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .payload(request) + .body(mapper.encode(request)) + .build()), + Collections.emptyList()); + } + + @Override + public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + final List errors = new ArrayList<>(); + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidRequest bidRequest, BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidRequest, bidResponse, errors); + } + + private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse, List errors) { + + return bidResponse.getSeatbid().stream() + .map(SeatBid::getBid) + .flatMap(Collection::stream) + .map(bid -> mapToBidderBid(bid, bidRequest.getImp(), bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid mapToBidderBid(Bid bid, List imps, String currency, List errors) { + final BidType bidType; + try { + bidType = getBidType(bid.getImpid(), imps); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + return null; + } + return BidderBid.of(bid, bidType, currency); + } + + private static BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getBanner() == null && imp.getVideo() != null) { + return BidType.video; + } + return BidType.banner; + } + } + throw new PreBidException(String.format("Failed to find impression %s", impId)); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/nobid/ExtImpNobid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nobid/ExtImpNobid.java new file mode 100644 index 00000000000..e2a25fc93b3 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nobid/ExtImpNobid.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.nobid; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpNobid { + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("placementId") + Integer placementId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/NobidConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/NobidConfiguration.java new file mode 100644 index 00000000000..8f5b0067458 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/NobidConfiguration.java @@ -0,0 +1,56 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.nobid.NobidBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.BidderInfoCreator; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/nobid.yaml", factory = YamlPropertySourceFactory.class) +public class NobidConfiguration { + + private static final String BIDDER_NAME = "nobid"; + + @Value("${external-url}") + @NotBlank + private String externalUrl; + + @Autowired + private JacksonMapper mapper; + + @Autowired + @Qualifier("nobidConfigurationProperties") + private BidderConfigurationProperties configProperties; + + @Bean("nobidConfigurationProperties") + @ConfigurationProperties("adapters.nobid") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps nobidBidderDeps() { + final UsersyncConfigurationProperties usersync = configProperties.getUsersync(); + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(configProperties) + .bidderInfo(BidderInfoCreator.create(configProperties)) + .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl)) + .bidderCreator(() -> new NobidBidder(configProperties.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/nobid.yaml b/src/main/resources/bidder-config/nobid.yaml new file mode 100644 index 00000000000..2312d7f3469 --- /dev/null +++ b/src/main/resources/bidder-config/nobid.yaml @@ -0,0 +1,25 @@ +adapters: + nobid: + enabled: false + endpoint: https://ads.servenobid.com/ortb_adreq?tek=pbs&ver=1 + pbs-enforces-gdpr: true + pbs-enforces-ccpa: true + modifying-vast-xml-allowed: true + deprecated-names: + aliases: + meta-info: + maintainer-email: developers@nobid.io + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 816 + usersync: + url: https://ads.servenobid.com/getsync?tek=pbs&ver=1&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect= + redirect-url: /setuid?bidder=nobid&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID + cookie-family-name: nobid + type: redirect + support-cors: false diff --git a/src/main/resources/static/bidder-params/nobid.json b/src/main/resources/static/bidder-params/nobid.json new file mode 100644 index 00000000000..10c2f248d38 --- /dev/null +++ b/src/main/resources/static/bidder-params/nobid.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "NoBid Adapter Params", + "description": "A schema which validates params accepted by the NoBid adapter", + + "type": "object", + "properties": { + "siteId": { + "type": "integer", + "description": "A Required ID which identifies the NoBid site. The siteId paramerter is provided by your NoBid account manager." + }, "placementId": { + "type": "integer", + "description": "An oprional ID which identifies an adunit in a site. The placementId paramerter is provided by your NoBid account manager." + } + }, + "required": ["siteId"] +} diff --git a/src/test/java/org/prebid/server/bidder/nobid/NobidBidderTest.java b/src/test/java/org/prebid/server/bidder/nobid/NobidBidderTest.java new file mode 100644 index 00000000000..f24d9357f7d --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/nobid/NobidBidderTest.java @@ -0,0 +1,196 @@ +package org.prebid.server.bidder.nobid; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.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 java.util.Arrays; +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; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; + +public class NobidBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.com/prebid/bid&key={{AccountID}}"; + + private NobidBidder nobidBidder; + + @Before + public void setUp() { + nobidBidder = new NobidBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new NobidBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final HttpCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = nobidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors()) + .allMatch(error -> error.getType() == BidderError.Type.bad_server_response + && error.getMessage().startsWith("Failed to decode: Unrecognized token")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(null)); + + // when + final Result> result = nobidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = nobidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = nobidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnBidFromEverySeatBid() throws JsonProcessingException { + // given + final SeatBid firstSeatBId = SeatBid.builder() + .bid(singletonList(Bid.builder() + .impid("123") + .build())) + .build(); + + final SeatBid secondSeatBid = SeatBid.builder() + .bid(singletonList(Bid.builder() + .impid("456") + .build())) + .build(); + + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(Arrays.asList(Imp.builder().id("123").banner(Banner.builder().build()).build(), + Imp.builder().id("456").video(Video.builder().build()).build())) + .build(), + mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(Arrays.asList(firstSeatBId, secondSeatBid)) + .build())); + + // when + final Result> result = nobidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"), + BidderBid.of(Bid.builder().impid("456").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = nobidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorIfImpWasNotFound() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("125")))); + + // when + final Result> result = nobidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("Failed to find impression 125")); + } + + private static BidResponse givenBidResponse(Function bidCustomizer) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static HttpCall givenHttpCall(BidRequest bidRequest, String body) { + return HttpCall.success( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/NobidTest.java b/src/test/java/org/prebid/server/it/NobidTest.java new file mode 100644 index 00000000000..a9b31f52623 --- /dev/null +++ b/src/test/java/org/prebid/server/it/NobidTest.java @@ -0,0 +1,60 @@ +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.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToIgnoreCase; +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 io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; + +@RunWith(SpringRunner.class) +public class NobidTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromNobid() throws IOException, JSONException { + // given + // Nobid bid response for imp + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/nobid-exchange")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/nobid/test-nobid-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/nobid/test-nobid-bid-response.json")))); + + // pre-bid cache + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/nobid/test-cache-nobid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/nobid/test-cache-nobid-response.json")))); + + // when + final Response response = given(SPEC) + .header("Referer", "http://www.example.com") + .header("X-Forwarded-For", "193.168.244.1") + .header("User-Agent", "userAgent") + .header("Origin", "http://www.example.com") + // this uids cookie value stands for {"uids":{"nobid":"NB-UID"}} + .cookie("uids", "eyJ1aWRzIjp7Im5vYmlkIjoiTkItVUlEIn19") + .body(jsonFrom("openrtb2/nobid/test-auction-nobid-request.json")) + .post("/openrtb2/auction"); + + // then + final String expectedAuctionResponse = openrtbAuctionResponseFrom( + "openrtb2/nobid/test-auction-nobid-response.json", + response, singletonList("nobid")); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-request.json new file mode 100644 index 00000000000..69a50977e20 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-request.json @@ -0,0 +1,63 @@ +{ + "id": "some-request-id", + "imp": [ + { + "id": "testimpid", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "nobid": { + "siteId": 23, + "placementId": 25 + } + }, + "tagid": "impId021" + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "site": { + "publisher": { + "id": "publisherId" + } + }, + "at": 1, + "tmax": 1000, + "cur": [ + "USD" + ], + "source": { + "fd": 1, + "tid": "tid" + }, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + } + }, + "cache": { + "bids": {} + }, + "auctiontimestamp": 1000 + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json new file mode 100644 index 00000000000..4e20fc3207b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json @@ -0,0 +1,53 @@ +{ + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "testid", + "impid": "testimpid", + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_pb": "0.00", + "hb_cache_id_nobid": "3c0769d8-0dd9-465c-8bf3-f570605ba698", + "hb_bidder_nobid": "nobid", + "hb_bidder": "nobid", + "hb_cache_id": "3c0769d8-0dd9-465c-8bf3-f570605ba698", + "hb_pb_nobid": "0.00", + "hb_cache_host": "{{ cache.host }}", + "hb_cache_host_nobid": "{{ cache.host }}", + "hb_cache_path": "{{ cache.path }}", + "hb_cache_path_nobid": "{{ cache.path }}" + }, + "cache": { + "bids": { + "url": "{{ cache.resource_url }}3c0769d8-0dd9-465c-8bf3-f570605ba698", + "cacheId": "3c0769d8-0dd9-465c-8bf3-f570605ba698" + } + } + } + } + } + ], + "seat": "nobid", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "nobid": "{{ nobid.response_time_ms }}", + "cache": "{{ cache.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 1000 + }, + "tmaxrequest": 1000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-cache-nobid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-cache-nobid-request.json new file mode 100644 index 00000000000..ca8e3ab2f6a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-cache-nobid-request.json @@ -0,0 +1,15 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "testid", + "impid": "testimpid", + "cid": "8048" + } + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-cache-nobid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-cache-nobid-response.json new file mode 100644 index 00000000000..c0100536be1 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-cache-nobid-response.json @@ -0,0 +1,7 @@ +{ + "responses": [ + { + "uuid": "3c0769d8-0dd9-465c-8bf3-f570605ba698" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-nobid-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-nobid-bid-request.json new file mode 100644 index 00000000000..e0868c03cee --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-nobid-bid-request.json @@ -0,0 +1,76 @@ +{ + "id": "some-request-id", + "imp": [ + { + "id": "testimpid", + "banner": { + "w": 320, + "h": 250 + }, + "tagid": "impId021", + "ext": { + "bidder": { + "siteId" : 23, + "placementId" : 25 + } + } + } + ], + "site": { + "domain": "example.com", + "page": "http://www.example.com", + "publisher": { + "id": "publisherId" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "user": { + "buyeruid": "NB-UID" + }, + "at": 1, + "tmax": 1000, + "cur": [ + "USD" + ], + "source": { + "fd": 1, + "tid": "tid" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "cache": { + "bids": {} + }, + "auctiontimestamp": 1000, + "channel": { + "name": "web" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-nobid-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-nobid-bid-response.json new file mode 100644 index 00000000000..ca4e6ee1db4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-nobid-bid-response.json @@ -0,0 +1,18 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "testid", + "impid": "testimpid", + "cid": "8048" + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 17a3e82504d..e1a37f1a5c7 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -214,6 +214,10 @@ adapters.ninthdecimal.enabled=true adapters.ninthdecimal.endpoint=http://localhost:8090/ninthdecimal-exchange?pubid= adapters.ninthdecimal.pbs-enforces-gdpr=true adapters.ninthdecimal.usersync.url=//ninthdecimal-usersync +adapters.nobid.enabled=true +adapters.nobid.endpoint=http://localhost:8090/nobid-exchange?pubid= +adapters.nobid.pbs-enforces-gdpr=true +adapters.nobid.usersync.url=//nobid-usersync adapters.openx.enabled=true adapters.openx.endpoint=http://localhost:8090/openx-exchange adapters.openx.pbs-enforces-gdpr=true From a4bfad14f02d08014046852a919740c1eac1d96d Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Thu, 19 Nov 2020 14:32:39 +0200 Subject: [PATCH 006/129] Add client/AccountID support into Adoppler adapter (#1010) --- .../bidder/adoppler/AdopplerBidder.java | 42 +++++++--- .../ext/request/adoppler/ExtImpAdoppler.java | 1 + .../resources/bidder-config/adoppler.yaml | 4 +- .../static/bidder-params/adoppler.json | 4 + .../bidder/adoppler/AdopplerBidderTest.java | 76 +++++++++---------- .../org/prebid/server/it/AdopplerTest.java | 3 +- .../adoppler/test-adoppler-bid-request-1.json | 5 +- .../test-auction-adoppler-request.json | 5 +- 8 files changed, 82 insertions(+), 58 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adoppler/AdopplerBidder.java b/src/main/java/org/prebid/server/bidder/adoppler/AdopplerBidder.java index b7f25f11ba7..1b475e56dd2 100644 --- a/src/main/java/org/prebid/server/bidder/adoppler/AdopplerBidder.java +++ b/src/main/java/org/prebid/server/bidder/adoppler/AdopplerBidder.java @@ -44,6 +44,7 @@ public class AdopplerBidder implements Bidder { private static final TypeReference> ADOPPLER_EXT_TYPE_REFERENCE = new TypeReference>() { }; + private static final String DEFAULT_CLIENT = "app"; private final String endpointTemplate; private final JacksonMapper mapper; @@ -63,9 +64,9 @@ public Result>> makeHttpRequests(BidRequest request final ExtImpAdoppler validExtImp = parseAndValidateImpExt(imp); final String updateRequestId = request.getId() + "-" + validExtImp.getAdunit(); final BidRequest updateRequest = request.toBuilder().id(updateRequestId).build(); - final String uri = String.format("%s/processHeaderBid/%s", endpointTemplate, - HttpUtil.encodeUrl(validExtImp.getAdunit())); - result.add(createSingleRequest(imp, updateRequest, uri)); + final String url = resolveUrl(validExtImp); + + result.add(createSingleRequest(imp, updateRequest, url)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } @@ -78,7 +79,7 @@ private ExtImpAdoppler parseAndValidateImpExt(Imp imp) { try { extImpAdoppler = mapper.mapper().convertValue(imp.getExt(), ADOPPLER_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); + throw new PreBidException(e.getMessage()); } if (StringUtils.isBlank(extImpAdoppler.getAdunit())) { throw new PreBidException("$.imp.ext.adoppler.adunit required"); @@ -86,6 +87,22 @@ private ExtImpAdoppler parseAndValidateImpExt(Imp imp) { return extImpAdoppler; } + private String resolveUrl(ExtImpAdoppler extImp) { + final String client = extImp.getClient(); + + try { + final String accountIdMacro = StringUtils.isBlank(client) + ? DEFAULT_CLIENT + : HttpUtil.encodeUrl(client); + + return endpointTemplate + .replace("{{AccountID}}", accountIdMacro) + .replace("{{AdUnit}}", HttpUtil.encodeUrl(extImp.getAdunit())); + } catch (Exception e) { + throw new PreBidException(e.getMessage()); + } + } + private HttpRequest createSingleRequest(Imp imp, BidRequest request, String url) { final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); final String body = mapper.encode(outgoingRequest); @@ -121,24 +138,25 @@ private BidResponse decodeBodyToBidResponse(HttpCall httpCall) { try { return mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); } catch (DecodeException e) { - throw new PreBidException(e.getMessage(), e); + throw new PreBidException(String.format("invalid body: %s", e.getMessage())); } } private Map getImpTypes(BidRequest bidRequest) { final Map impTypes = new HashMap<>(); for (Imp imp : bidRequest.getImp()) { - if (impTypes.get(imp.getId()) != null) { - throw new PreBidException(String.format("duplicate $.imp.id %s", imp.getId())); + final String impId = imp.getId(); + if (impTypes.get(impId) != null) { + throw new PreBidException(String.format("duplicate $.imp.id %s", impId)); } if (imp.getBanner() != null) { - impTypes.put(imp.getId(), BidType.banner); + impTypes.put(impId, BidType.banner); } else if (imp.getVideo() != null) { - impTypes.put(imp.getId(), BidType.video); + impTypes.put(impId, BidType.video); } else if (imp.getAudio() != null) { - impTypes.put(imp.getId(), BidType.audio); + impTypes.put(impId, BidType.audio); } else if (imp.getXNative() != null) { - impTypes.put(imp.getId(), BidType.xNative); + impTypes.put(impId, BidType.xNative); } else { throw new PreBidException("one of $.imp.banner, $.imp.video, $.imp.audio " + "and $.imp.native field required"); @@ -149,7 +167,7 @@ private Map getImpTypes(BidRequest bidRequest) { private BidderBid createBid(Bid bid, Map impTypes, String currency) { if (impTypes.get(bid.getImpid()) == null) { - throw new PreBidException(String.format("unknown impid: %s", bid.getImpid())); + throw new PreBidException(String.format("unknown impId: %s", bid.getImpid())); } validateResponseVideoExt(bid, impTypes); return BidderBid.of(bid, impTypes.get(bid.getImpid()), currency); diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adoppler/ExtImpAdoppler.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adoppler/ExtImpAdoppler.java index ac28d2c7bdf..b839645945b 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adoppler/ExtImpAdoppler.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adoppler/ExtImpAdoppler.java @@ -8,4 +8,5 @@ public class ExtImpAdoppler { String adunit; + String client; } diff --git a/src/main/resources/bidder-config/adoppler.yaml b/src/main/resources/bidder-config/adoppler.yaml index ad9492bf19d..203ae18907e 100644 --- a/src/main/resources/bidder-config/adoppler.yaml +++ b/src/main/resources/bidder-config/adoppler.yaml @@ -1,7 +1,7 @@ adapters: adoppler: enabled: false - endpoint: http://app.trustedmarketplace.io/ads + endpoint: http://{{AccountID}}.trustedmarketplace.io/ads/processHeaderBid/{{AdUnit}} pbs-enforces-gdpr: true pbs-enforces-ccpa: true modifying-vast-xml-allowed: true @@ -22,4 +22,4 @@ adapters: redirect-url: cookie-family-name: adoppler type: redirect - support-cors: false \ No newline at end of file + support-cors: false diff --git a/src/main/resources/static/bidder-params/adoppler.json b/src/main/resources/static/bidder-params/adoppler.json index 7bf55ec1250..eaa4e6df80e 100644 --- a/src/main/resources/static/bidder-params/adoppler.json +++ b/src/main/resources/static/bidder-params/adoppler.json @@ -7,6 +7,10 @@ "adunit": { "type": "string", "description": "AdUnit to bid against to." + }, + "client": { + "type": "string", + "description": "Client name." } }, "required": ["adunit"] diff --git a/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java b/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java index 4500272bdbc..2f5aad3ea3a 100644 --- a/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; @@ -25,7 +24,7 @@ import org.prebid.server.proto.openrtb.ext.request.adoppler.ExtImpAdoppler; import org.prebid.server.util.HttpUtil; -import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -39,7 +38,7 @@ public class AdopplerBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://test.endpoint.com"; + private static final String ENDPOINT_URL = "http://{{AccountID}}.test.com/some/path/{{AdUnit}}"; private AdopplerBidder adopplerBidder; @@ -58,26 +57,35 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // given final BidRequest bidRequest = givenBidRequest( impBuilder -> impBuilder - .banner(Banner.builder() - .format(singletonList(Format.builder().w(300).h(500).build())) - .build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdoppler.of(null))))); + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdoppler.of(null, null))))); // when final Result>> result = adopplerBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("$.imp.ext.adoppler.adunit required"); + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("$.imp.ext.adoppler.adunit required")); } @Test public void makeHttpRequestsShouldCreateCorrectURL() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = adopplerBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().get(0).getUri()).isEqualTo("http://clientId.test.com/some/path/adUnit"); + } + + @Test + public void makeHttpRequestsShouldCreateUrlWithDefaultAppParamIfClientIsMissing() { // given final BidRequest bidRequest = givenBidRequest( impBuilder -> impBuilder - .banner(Banner.builder() - .format(singletonList(Format.builder().w(300).h(500).build())) - .build())); + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdoppler.of("adUnit", ""))))); // when final Result>> result = adopplerBidder.makeHttpRequests(bidRequest); @@ -85,11 +93,11 @@ public void makeHttpRequestsShouldCreateCorrectURL() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1); - assertThat(result.getValue().get(0).getUri()).isEqualTo("https://test.endpoint.com/processHeaderBid/adUnit"); + assertThat(result.getValue().get(0).getUri()).isEqualTo("http://app.test.com/some/path/adUnit"); } @Test - public void makeHttpRequestsShouldSetExpectedRequestUrlAndDefaultHeaders() { + public void makeHttpRequestsShouldSetExpectedHeaders() { // given final BidRequest bidRequest = givenBidRequest(identity()); @@ -100,7 +108,7 @@ public void makeHttpRequestsShouldSetExpectedRequestUrlAndDefaultHeaders() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue().get(0).getHeaders()).isNotNull() .extracting(Map.Entry::getKey, Map.Entry::getValue) - .containsOnly(tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"), + .containsExactlyInAnyOrder(tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"), tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE), tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString())); } @@ -110,11 +118,9 @@ public void makeBidsShouldReturnErrorIfDuplicateId() throws JsonProcessingExcept // given final Imp imp1 = Imp.builder().id("impId").banner(Banner.builder().build()).build(); final Imp imp2 = Imp.builder().id("impId").video(Video.builder().build()).build(); - final List imps = new ArrayList(); - imps.add(imp1); - imps.add(imp2); - BidRequest bidRequest = BidRequest.builder() - .imp(imps) + + final BidRequest bidRequest = BidRequest.builder() + .imp(Arrays.asList(imp1, imp2)) .build(); final HttpCall httpCall = givenHttpCall( bidRequest, mapper.writeValueAsString( @@ -124,10 +130,8 @@ public void makeBidsShouldReturnErrorIfDuplicateId() throws JsonProcessingExcept final Result> result = adopplerBidder.makeBids(httpCall, bidRequest); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()) - .startsWith("duplicate $.imp.id impId"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("duplicate $.imp.id impId")); assertThat(result.getValue()).isEmpty(); } @@ -150,10 +154,9 @@ public void makeBidsShouldReturnErrorIfEmptyImp() throws JsonProcessingException final Result> result = adopplerBidder.makeBids(httpCall, bidRequest); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()) - .startsWith("one of $.imp.banner, $.imp.video, $.imp.audio and $.imp.native field required"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("one of $.imp.banner, $.imp.video, " + + "$.imp.audio and $.imp.native field required")); assertThat(result.getValue()).isEmpty(); } @@ -173,10 +176,8 @@ public void makeBidsShouldReturnErrorIfBidIdEmpty() throws JsonProcessingExcepti final Result> result = adopplerBidder.makeBids(httpCall, bidRequest); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()) - .startsWith("unknown impid: null"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("unknown impId: null")); assertThat(result.getValue()).isEmpty(); } @@ -184,8 +185,7 @@ public void makeBidsShouldReturnErrorIfBidIdEmpty() throws JsonProcessingExcepti public void makeBidsShouldReturnErrorIfExtEmpty() throws JsonProcessingException { // given final Imp imp = Imp.builder().id("impId").video(Video.builder().build()).build(); - final List imps = Collections.singletonList(imp); - final BidRequest bidRequest = BidRequest.builder().imp(imps).build(); + final BidRequest bidRequest = BidRequest.builder().imp(Collections.singletonList(imp)).build(); final ObjectNode ext = mapper.valueToTree(AdopplerResponseExt.of(null)); final HttpCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString( givenBidResponse(bidBuilder -> bidBuilder @@ -197,10 +197,8 @@ public void makeBidsShouldReturnErrorIfExtEmpty() throws JsonProcessingException final Result> result = adopplerBidder.makeBids(httpCall, bidRequest); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()) - .startsWith("$.seatbid.bid.ext.ads.video required"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("$.seatbid.bid.ext.ads.video required")); assertThat(result.getValue()).isEmpty(); } @@ -221,7 +219,7 @@ private static Imp givenImp(Function impCustomiz return impCustomizer.apply(Imp.builder() .id("123") .banner(Banner.builder().id("banner_id").build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdoppler.of("adUnit"))))) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdoppler.of("adUnit", "clientId"))))) .build(); } diff --git a/src/test/java/org/prebid/server/it/AdopplerTest.java b/src/test/java/org/prebid/server/it/AdopplerTest.java index 8aa0a6601c6..4550bd6c8d5 100644 --- a/src/test/java/org/prebid/server/it/AdopplerTest.java +++ b/src/test/java/org/prebid/server/it/AdopplerTest.java @@ -25,9 +25,10 @@ public class AdopplerTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromAdoppler() throws IOException, JSONException { // given // Adoppler bid response for imp 001 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adoppler-exchange/processHeaderBid/unit1")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adoppler-exchange")) .withHeader("Accept", equalTo("application/json")) .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) + .withHeader("X-OpenRTB-Version", equalTo("2.5")) .withRequestBody(equalToJson(jsonFrom("openrtb2/adoppler/test-adoppler-bid-request-1.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/adoppler/test-adoppler-bid-response-1.json")))); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json index a6d57161046..8ee1468f868 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json @@ -9,7 +9,8 @@ }, "ext": { "bidder": { - "adunit": "unit1" + "adunit": "unit1", + "client": "testClient" } } } @@ -84,4 +85,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json index cf8621f7d95..2ab1c7860f2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json @@ -9,7 +9,8 @@ }, "ext": { "adoppler": { - "adunit": "unit1" + "adunit": "unit1", + "client": "testClient" } } } @@ -81,4 +82,4 @@ "gdpr": 0 } } -} \ No newline at end of file +} From d68d8540d8d71ccd621ceae3425c32838be445e9 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Thu, 19 Nov 2020 14:55:04 +0200 Subject: [PATCH 007/129] Adman bidder review (#1024) --- .../server/bidder/adman/AdmanBidder.java | 17 +++ .../server/bidder/adman/AdmanBidderTest.java | 103 +++++++++--------- 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adman/AdmanBidder.java b/src/main/java/org/prebid/server/bidder/adman/AdmanBidder.java index 45634bf5a4d..229d937d20c 100644 --- a/src/main/java/org/prebid/server/bidder/adman/AdmanBidder.java +++ b/src/main/java/org/prebid/server/bidder/adman/AdmanBidder.java @@ -3,8 +3,12 @@ import com.iab.openrtb.request.Imp; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.OpenrtbBidder; +import org.prebid.server.exception.PreBidException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.request.adman.ExtImpAdman; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.List; /** * Adman {@link Bidder} implementation. @@ -21,4 +25,17 @@ protected Imp modifyImp(Imp imp, ExtImpAdman impExt) { .tagid(impExt.getTagId()) .build(); } + + @Override + protected BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getBanner() == null && imp.getVideo() != null) { + return BidType.video; + } + return BidType.banner; + } + } + throw new PreBidException(String.format("Failed to find impression %s", impId)); + } } diff --git a/src/test/java/org/prebid/server/bidder/adman/AdmanBidderTest.java b/src/test/java/org/prebid/server/bidder/adman/AdmanBidderTest.java index fe78fb7ab5d..d32963a99d9 100644 --- a/src/test/java/org/prebid/server/bidder/adman/AdmanBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adman/AdmanBidderTest.java @@ -1,9 +1,11 @@ package org.prebid.server.bidder.adman; import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; @@ -27,7 +29,6 @@ import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; @@ -42,29 +43,6 @@ public void setUp() { admanBidder = new AdmanBidder(ENDPOINT_URL, jacksonMapper); } - @Test - public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new AdmanBidder("invalid_url", jacksonMapper)); - } - - @Test - public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { - // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) - .build())) - .build(); - - // when - final Result>> result = admanBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance"); - assertThat(result.getValue()).isEmpty(); - } - @Test public void makeHttpRequestsShouldReturnExpectedBidRequest() { // given @@ -80,7 +58,7 @@ public void makeHttpRequestsShouldReturnExpectedBidRequest() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .containsOnly(expectedRequest); + .containsExactly(expectedRequest); } @Test @@ -103,98 +81,115 @@ public void makeHttpRequestsShouldMakeOneRequestPerImp() { .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .flatExtracting(BidRequest::getImp).hasSize(2) .extracting(Imp::getTagid) - .containsOnly("tagidString", "otherTagId"); + .containsExactly("tagidString", "otherTagId"); } @Test - public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { // given - final HttpCall httpCall = givenHttpCall(null, "invalid"); + final HttpCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); // when final Result> result = admanBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); } @Test - public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException { // given - final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build())) + .build(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); // when final Result> result = admanBidder.makeBids(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); } @Test - public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException { // given - final HttpCall httpCall = givenHttpCall(null, - mapper.writeValueAsString(BidResponse.builder().build())); + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .build(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); // when final Result> result = admanBidder.makeBids(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); } @Test - public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBidIfNativeIsPresentInRequestImp() throws JsonProcessingException { // given final HttpCall httpCall = givenHttpCall( - BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), - mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); // when final Result> result = admanBidder.makeBids(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); } @Test - public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBidIfAudioIsPresentInRequestImp() throws JsonProcessingException { // given final HttpCall httpCall = givenHttpCall( BidRequest.builder() - .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build())) + .imp(singletonList(Imp.builder().id("123").audio(Audio.builder().build()).build())) .build(), - mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); // when final Result> result = admanBidder.makeBids(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); } @Test - public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException { + public void makeBidsShouldReturnErrorWithUnknownBidTypeIfNotSupportedBidType() throws JsonProcessingException { // given - final HttpCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(givenImp(identity()))) + final HttpCall httpCall = givenHttpCall(BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) .build(), - mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("125")))); // when final Result> result = admanBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + assertThat(result.getErrors()).hasSize(1) + .containsExactly(BidderError.badServerResponse("Failed to find impression 125")); + assertThat(result.getValue()).isEmpty(); } private static BidRequest givenBidRequest( From 4aa6ad4c0a7c646daf0342a22797d7a96b7a79c0 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Thu, 19 Nov 2020 15:03:38 +0200 Subject: [PATCH 008/129] AdkernelAdn bidder review (#1023) --- .../bidder/adkerneladn/AdkernelAdnBidder.java | 145 ++++++++---------- .../resources/bidder-config/adkerneladn.yaml | 2 +- .../adkerneladn/AdkernelAdnBidderTest.java | 26 +--- .../org/prebid/server/it/AdkernelAdnTest.java | 4 +- .../server/it/test-application.properties | 2 +- 5 files changed, 75 insertions(+), 104 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java b/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java index ec3c133098e..91d95e67ae2 100644 --- a/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java +++ b/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java @@ -12,6 +12,8 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -26,8 +28,6 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; -import java.net.MalformedURLException; -import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -45,6 +45,9 @@ public class AdkernelAdnBidder implements Bidder { private static final TypeReference> ADKERNELADN_EXT_TYPE_REFERENCE = new TypeReference>() { }; + private static final String DEFAULT_DOMAIN = "tag.adkernel.com"; + private static final String URL_HOST_MACRO = "{{Host}}"; + private static final String URL_PUBLISHER_ID_MACRO = "{{PublisherID}}"; private final String endpointUrl; private final JacksonMapper mapper; @@ -56,39 +59,36 @@ public AdkernelAdnBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest bidRequest) { - final List imps = bidRequest.getImp(); - + final List validImps = bidRequest.getImp(); final List errors = new ArrayList<>(); - final List> httpRequests = new ArrayList<>(); - try { - final List impExts = getAndValidateImpExt(imps); - final Map> pubToImps = dispatchImpressions(imps, impExts); - httpRequests.addAll(buildAdapterRequests(bidRequest, pubToImps, endpointUrl)); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - } - return Result.of(httpRequests, errors); - } + final Map impWithExts = getAndValidateImpExt(validImps, errors); + final Map> pubToImps = dispatchImpressions(impWithExts, errors); + if (MapUtils.isEmpty(pubToImps)) { + return Result.withErrors(errors); + } - private static MultiMap headers() { - return HttpUtil.headers() - .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + return Result.of(buildAdapterRequests(bidRequest, pubToImps), errors); } - private List getAndValidateImpExt(List imps) { - return imps.stream() - .map(AdkernelAdnBidder::validateImp) - .map(this::parseAndValidateAdkernelAdnExt) - .collect(Collectors.toList()); + private Map getAndValidateImpExt(List imps, List errors) { + final Map validImpsWithExts = new HashMap<>(); + for (Imp imp : imps) { + try { + validateImp(imp); + validImpsWithExts.put(imp, parseAndValidateAdkernelAdnExt(imp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + return validImpsWithExts; } - private static Imp validateImp(Imp imp) { + private static void validateImp(Imp imp) { if (imp.getBanner() == null && imp.getVideo() == null) { throw new PreBidException(String.format("Invalid imp with id=%s. Expected imp.banner or imp.video", imp.getId())); } - return imp; } private ExtImpAdkernelAdn parseAndValidateAdkernelAdnExt(Imp imp) { @@ -105,41 +105,42 @@ private ExtImpAdkernelAdn parseAndValidateAdkernelAdnExt(Imp imp) { return adkernelAdnExt; } - /** - * Group impressions by AdKernel-specific parameters `pubId` & `host`. - */ - private static Map> dispatchImpressions(List imps, - List impExts) { + private static Map> dispatchImpressions(Map impsWithExts, + List errors) { final Map> result = new HashMap<>(); - for (int i = 0; i < imps.size(); i++) { - final Imp imp = compatImpression(imps.get(i)); - final ExtImpAdkernelAdn impExt = impExts.get(i); + for (Imp key : impsWithExts.keySet()) { + final Imp imp; + try { + imp = compatImpression(key); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + continue; + } + final ExtImpAdkernelAdn impExt = impsWithExts.get(key); result.putIfAbsent(impExt, new ArrayList<>()); result.get(impExt).add(imp); } + return result; } - /** - * Alter impression info to comply with adkernel platform requirements. - */ private static Imp compatImpression(Imp imp) { final Imp.ImpBuilder impBuilder = imp.toBuilder(); - impBuilder.ext(null); // do not forward ext to adkernel platform + impBuilder.ext(null); final Banner banner = imp.getBanner(); if (banner != null) { - return compatBannerImpression(impBuilder, banner); + compatBannerImpression(impBuilder, banner); } - return impBuilder.audio(null) + return impBuilder + .audio(null) .xNative(null) .build(); } - private static Imp compatBannerImpression(Imp.ImpBuilder impBuilder, Banner compatBanner) { + private static void compatBannerImpression(Imp.ImpBuilder impBuilder, Banner compatBanner) { if (compatBanner.getW() == null && compatBanner.getH() == null) { - // As banner.w/h are required fields for adkernel adn platform - take the first format entry final List compatBannerFormat = compatBanner.getFormat(); if (CollectionUtils.isEmpty(compatBannerFormat)) { @@ -161,32 +162,32 @@ private static Imp compatBannerImpression(Imp.ImpBuilder impBuilder, Banner comp impBuilder.banner(bannerBuilder.build()); } - return impBuilder.video(null) - .audio(null) - .xNative(null) - .build(); + impBuilder.video(null); } private List> buildAdapterRequests(BidRequest preBidRequest, - Map> pubToImps, - String endpointUrl) { + Map> pubToImps) { final List> result = new ArrayList<>(); for (Map.Entry> entry : pubToImps.entrySet()) { - final BidRequest outgoingRequest = createBidRequest(preBidRequest, entry.getValue()); - final String body = mapper.encode(outgoingRequest); - result.add(HttpRequest.builder() - .method(HttpMethod.POST) - .uri(buildEndpoint(entry.getKey(), endpointUrl)) - .body(body) - .headers(headers()) - .payload(outgoingRequest) - .build()); + result.add(createRequest(entry.getKey(), entry.getValue(), preBidRequest)); } return result; } + private HttpRequest createRequest(ExtImpAdkernelAdn extImp, List imps, BidRequest preBidRequest) { + final BidRequest outgoingRequest = createBidRequest(preBidRequest, imps); + final String body = mapper.encode(outgoingRequest); + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(buildEndpoint(extImp)) + .body(body) + .headers(headers()) + .payload(outgoingRequest) + .build(); + } + private static BidRequest createBidRequest(BidRequest preBidRequest, List imps) { final BidRequest.BidRequestBuilder bidRequestBuilder = preBidRequest.toBuilder() .imp(imps); @@ -203,27 +204,18 @@ private static BidRequest createBidRequest(BidRequest preBidRequest, List i return bidRequestBuilder.build(); } - /** - * Builds endpoint url based on adapter-specific pub settings from imp.ext. - */ - private static String buildEndpoint(ExtImpAdkernelAdn impExt, String endpointUrl) { - final String updatedEndpointUrl; + private String buildEndpoint(ExtImpAdkernelAdn impExt) { + final String impHost = impExt.getHost(); + final String host = StringUtils.isNotBlank(impHost) ? impHost : DEFAULT_DOMAIN; - if (impExt.getHost() != null) { - final URL url; - try { - url = new URL(endpointUrl); - } catch (MalformedURLException e) { - throw new PreBidException( - String.format("Error occurred while parsing AdkernelAdn endpoint url: %s", endpointUrl), e); - } - final String currentHostAndPort = url.getHost() + (url.getPort() == -1 ? "" : ":" + url.getPort()); - updatedEndpointUrl = endpointUrl.replace(currentHostAndPort, impExt.getHost()); - } else { - updatedEndpointUrl = endpointUrl; - } + return endpointUrl + .replace(URL_HOST_MACRO, host) + .replace(URL_PUBLISHER_ID_MACRO, impExt.getPubId().toString()); + } - return String.format("%s%s", updatedEndpointUrl, impExt.getPubId()); + private static MultiMap headers() { + return HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); } @Override @@ -249,14 +241,13 @@ private static List extractBids(BidRequest bidRequest, BidResponse bi private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { return bidResponse.getSeatbid().stream() .map(SeatBid::getBid) + .filter(Objects::nonNull) .flatMap(Collection::stream) + .filter(Objects::nonNull) .map(bid -> BidderBid.of(bid, getType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); } - /** - * Figures out which media type this bid is for. - */ private static BidType getType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId) && imp.getBanner() != null) { diff --git a/src/main/resources/bidder-config/adkerneladn.yaml b/src/main/resources/bidder-config/adkerneladn.yaml index 72a9b04857d..2c0f93c7c66 100644 --- a/src/main/resources/bidder-config/adkerneladn.yaml +++ b/src/main/resources/bidder-config/adkerneladn.yaml @@ -1,7 +1,7 @@ adapters: adkerneladn: enabled: false - endpoint: http://tag.adkernel.com/rtbpub?account= + endpoint: http://{{Host}}/rtbpub?account={{PublisherID}} pbs-enforces-gdpr: true pbs-enforces-ccpa: true modifying-vast-xml-allowed: true diff --git a/src/test/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidderTest.java b/src/test/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidderTest.java index 0f9cc726a66..c3591e0a899 100644 --- a/src/test/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidderTest.java @@ -44,7 +44,7 @@ public class AdkernelAdnBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "http://test.domain.com/rtbpub?account="; + private static final String ENDPOINT_URL = "http://{{Host}}/test?account={{PublisherID}}"; private AdkernelAdnBidder adkernelAdnBidder; @@ -149,7 +149,7 @@ public void makeHttpRequestsShouldFillMethodAndUrlAndExpectedHeaders() { // then assertThat(result.getValue()).hasSize(1).element(0).isNotNull() .returns(HttpMethod.POST, HttpRequest::getMethod) - .returns("http://test.domain.com/rtbpub?account=50357", HttpRequest::getUri); + .returns("http://tag.adkernel.com/test?account=50357", HttpRequest::getUri); assertThat(result.getValue().get(0).getHeaders()).isNotNull() .extracting(Map.Entry::getKey, Map.Entry::getValue) .containsOnly( @@ -172,27 +172,7 @@ public void makeHttpRequestShouldChangeDomainIfHostIsSpecified() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getUri) - .containsOnly("http://different.domanin.com/rtbpub?account=50357"); - } - - @Test - public void makeHttpRequestShouldRemovePortIfHostIsSpecified() { - // given - final String urlWithPort = "http://test:8080/rtbpub?account="; - adkernelAdnBidder = new AdkernelAdnBidder(urlWithPort, jacksonMapper); - - final BidRequest bidRequest = givenBidRequest( - identity(), - extImpAdkernelAdnBuilder -> extImpAdkernelAdnBuilder.host("different.domanin.com")); - - // when - final Result>> result = adkernelAdnBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .extracting(HttpRequest::getUri) - .containsOnly("http://different.domanin.com/rtbpub?account=50357"); + .containsOnly("http://different.domanin.com/test?account=50357"); } @Test diff --git a/src/test/java/org/prebid/server/it/AdkernelAdnTest.java b/src/test/java/org/prebid/server/it/AdkernelAdnTest.java index 8f143fafb68..2203ea3a99d 100644 --- a/src/test/java/org/prebid/server/it/AdkernelAdnTest.java +++ b/src/test/java/org/prebid/server/it/AdkernelAdnTest.java @@ -26,7 +26,7 @@ public class AdkernelAdnTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromAdkerneladn() throws IOException, JSONException { // given // adkernelAdn bid response for imp 021 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adkernelAdn-exchange")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adkernelAdn.tag.adkernel.com")) .withQueryParam("account", equalTo("101")) .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) .withHeader("Accept", equalTo("application/json")) @@ -36,7 +36,7 @@ public void openrtb2AuctionShouldRespondWithBidsFromAdkerneladn() throws IOExcep jsonFrom("openrtb2/adkerneladn/test-adkerneladn-bid-response-1.json")))); // adkernelAdn bid response for imp 022 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adkernelAdn-exchange")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adkernelAdn.tag.adkernel.com")) .withQueryParam("account", equalTo("102")) .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) .withHeader("Accept", equalTo("application/json")) diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index e1a37f1a5c7..6b2dfcaf412 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -15,7 +15,7 @@ adapters.adhese.endpoint=http://localhost:8090/adhese-exchange adapters.adhese.pbs-enforces-gdpr=true adapters.adhese.usersync.url=//adhese-usersync adapters.adkerneladn.enabled=true -adapters.adkerneladn.endpoint=http://localhost:8090/adkernelAdn-exchange?account= +adapters.adkerneladn.endpoint=http://localhost:8090/adkernelAdn.{{Host}}?account={{PublisherID}} adapters.adkerneladn.pbs-enforces-gdpr=true adapters.adkerneladn.usersync.url=//adkernelAdn-usersync adapters.adkernel.enabled=true From 2d6b248fa6d3eb18f5b0581c088497b093c87e1b Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Thu, 19 Nov 2020 15:05:39 +0200 Subject: [PATCH 009/129] Update repository reference after transfer under prebid organization (#1027) --- README.md | 14 +++++++------- docs/build.md | 2 +- docs/code-reviews.md | 4 ++-- docs/contributing.md | 4 ++-- docs/differenceBetweenPBSGo-and-Java.md | 10 +++++----- docs/endpoints/auction.md | 2 +- docs/endpoints/info/bidders/bidderName.md | 2 +- docs/endpoints/openrtb2/auction.md | 4 ++-- docs/metrics.md | 2 +- pom.xml | 2 +- 10 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index a4809e78da1..fa5fc1a7dba 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ # Prebid Server (Java) -[![GitHub version](https://badge.fury.io/gh/rubicon-project%2fprebid-server-java.svg)](http://badge.fury.io/gh/rubicon-project%2fprebid-server-java) -[![Language grade: Java](https://img.shields.io/lgtm/grade/java/g/rubicon-project/prebid-server-java.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/rubicon-project/prebid-server-java/context:java) -[![Total alerts](https://img.shields.io/lgtm/alerts/g/rubicon-project/prebid-server-java.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/rubicon-project/prebid-server-java/alerts/) -[![GitHub contributors](https://img.shields.io/github/contributors/rubicon-project/prebid-server-java.svg)](https://GitHub.com/rubicon-project/prebid-server-java/contributors/) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/rubicon-project/prebid-server-java/blob/master/docs/contributing.md) -[![GitHub pull-requests closed](https://img.shields.io/github/issues-pr-closed/rubicon-project/prebid-server-java.svg)](https://GitHub.com/rubicon-project/prebid-server-java/pull/) +[![GitHub version](https://badge.fury.io/gh/prebid%2fprebid-server-java.svg)](http://badge.fury.io/gh/prebid%2fprebid-server-java) +[![Language grade: Java](https://img.shields.io/lgtm/grade/java/g/prebid/prebid-server-java.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/prebid/prebid-server-java/context:java) +[![Total alerts](https://img.shields.io/lgtm/alerts/g/prebid/prebid-server-java.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/prebid/prebid-server-java/alerts/) +[![GitHub contributors](https://img.shields.io/github/contributors/prebid/prebid-server-java.svg)](https://GitHub.com/prebid/prebid-server-java/contributors/) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/prebid/prebid-server-java/blob/master/docs/contributing.md) +[![GitHub pull-requests closed](https://img.shields.io/github/issues-pr-closed/prebid/prebid-server-java.svg)](https://GitHub.com/prebid/prebid-server-java/pull/) Prebid Server is an open source implementation of Server-Side Header Bidding. It is managed by [Prebid.org](http://prebid.org/overview/what-is-prebid-org.html), @@ -44,7 +44,7 @@ Follow next steps to create JAR file which can be deployed locally. - Download or clone a project: ```bash -git clone https://github.com/rubicon-project/prebid-server-java.git +git clone https://github.com/prebid/prebid-server-java.git ``` - Move to project directory: diff --git a/docs/build.md b/docs/build.md index 89e3d9a20f9..44b13bee47b 100644 --- a/docs/build.md +++ b/docs/build.md @@ -19,7 +19,7 @@ Follow next steps to create JAR which can be deployed locally. Download or clone a project locally: ```bash -git clone https://github.com/rubicon-project/prebid-server-java.git +git clone https://github.com/prebid/prebid-server-java.git ``` Move to project directory: diff --git a/docs/code-reviews.md b/docs/code-reviews.md index 972597751f5..78728fef18a 100644 --- a/docs/code-reviews.md +++ b/docs/code-reviews.md @@ -1,7 +1,7 @@ # Code Reviews ## Standards -Anyone is free to review and comment on any [open pull requests](https://github.com/rubicon-project/prebid-server-java/pulls). +Anyone is free to review and comment on any [open pull requests](https://github.com/prebid/prebid-server-java/pulls). All pull requests must be reviewed and approved by at least one [core member](https://github.com/orgs/prebid/teams/core/members) before merge. @@ -38,7 +38,7 @@ Some examples include: - Can we improve the user's experience in any way? - Have the relevant [docs]() been added or updated? If not, add the `needs docs` label. - Do you believe that the code works by looking at the unit tests? If not, suggest more tests until you do! -- Is the motivation behind these changes clear? If not, there must be [an issue](https://github.com/rubicon-project/prebid-server-java/issues) +- Is the motivation behind these changes clear? If not, there must be [an issue](https://github.com/prebid/prebid-server-java/issues) explaining it. Are there better ways to achieve those goals? - Does the code use any global, mutable state? [Inject dependencies](https://en.wikipedia.org/wiki/Dependency_injection) instead! - Can the code be organized into smaller, more modular pieces? diff --git a/docs/contributing.md b/docs/contributing.md index 3ca432f3905..ae3f10dc4e9 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -2,7 +2,7 @@ ## Create an issue -[Create an issue](https://github.com/rubicon-project/prebid-server-java/issues/new) describing the motivation for your changes. +[Create an issue](https://github.com/prebid/prebid-server-java/issues/new) describing the motivation for your changes. Are you fixing a bug? Improving documentation? Optimizing some slow code? Pull Requests without associated Issues may still be accepted, if the motivation is obvious. @@ -29,6 +29,6 @@ those updates must be submitted in the same Pull Request as the code changes. ## Open a Pull Request When you're ready, [submit a Pull Request](https://help.github.com/articles/creating-a-pull-request/) -against the `master` branch of [our GitHub repository](https://github.com/rubicon-project/prebid-server-java/compare). +against the `master` branch of [our GitHub repository](https://github.com/prebid/prebid-server-java/compare). If the tests pass locally, but fail on your PR, [update your fork](https://help.github.com/articles/syncing-a-fork/) with the latest code from `master`. diff --git a/docs/differenceBetweenPBSGo-and-Java.md b/docs/differenceBetweenPBSGo-and-Java.md index 3e6d185f550..8d1eb1023da 100644 --- a/docs/differenceBetweenPBSGo-and-Java.md +++ b/docs/differenceBetweenPBSGo-and-Java.md @@ -9,22 +9,22 @@ and not the other for an interim period. This page tracks known differences that ## Feature Differences -1) PBS-Java supports Stored Responses [issue 861](https://github.com/prebid/prebid-server/issues/861). PBS-Java [PR 354](https://github.com/rubicon-project/prebid-server-java/pull/354). -1) PBS-Java supports Currency conversion. PBS-Go has it implemented, but disabled by default(still under dev) [issue 280](https://github.com/prebid/prebid-server/issues/280), [issue 760](https://github.com/prebid/prebid-server/pull/760). PBS-Java [PR 22](https://github.com/rubicon-project/prebid-server-java/pull/22) +1) PBS-Java supports Stored Responses [issue 861](https://github.com/prebid/prebid-server/issues/861). PBS-Java [PR 354](https://github.com/prebid/prebid-server-java/pull/354). +1) PBS-Java supports Currency conversion. PBS-Go has it implemented, but disabled by default(still under dev) [issue 280](https://github.com/prebid/prebid-server/issues/280), [issue 760](https://github.com/prebid/prebid-server/pull/760). PBS-Java [PR 22](https://github.com/prebid/prebid-server-java/pull/22) 1) PBS-Java Currency conversion supports finding intermediate conversion rate, e.g. if pairs USD : AUD = 1.2 and EUR : AUD = 1.5 are present and EUR to USD conversion is needed, will return (1/1.5) * 1.2 conversion rate. 1) PBS-Go Currency conversion admin debug endpoint exposes following information: Sync source URL, Internal rates, Update frequency, Last update. PBS-Java `/currency-rates` admin endpoint currently supports checking the latest update time only and is not available if currency conversion is disabled. -1) PBS-Java supports IP-address lookup in certain scenarios around GDPR. See https://github.com/rubicon-project/prebid-server-java/blob/master/docs/developers/PrebidServerJava_GDPR_Requirements.pdf +1) PBS-Java supports IP-address lookup in certain scenarios around GDPR. See https://github.com/prebid/prebid-server-java/blob/master/docs/developers/PrebidServerJava_GDPR_Requirements.pdf 1) PBS-Java supports InfluxDB, Graphite and Prometheus, PBS-Go supports InfluxDB and Prometheus as metrics backend. 1) PBS-Java has Circuit Breaker mechanism for database, http and geolocation requests. This can protect the server in scenarios where an external service becomes unavailable. 1) PBS-Java supports `ext.prebid.cache.{bids,vastxml}.returnCreative` field to control creative presence in response (`true` by default). -1) PBS-Java support caching winning bids only through `auction.cache.only-winning-bids` configuration property or request field `request.ext.prebid.cache.winningonly`. PBS-Java [issue 279](https://github.com/rubicon-project/prebid-server-java/issues/279), [PR 484](https://github.com/rubicon-project/prebid-server-java/pull/484). +1) PBS-Java support caching winning bids only through `auction.cache.only-winning-bids` configuration property or request field `request.ext.prebid.cache.winningonly`. PBS-Java [issue 279](https://github.com/prebid/prebid-server-java/issues/279), [PR 484](https://github.com/prebid/prebid-server-java/pull/484). 1) PBS-Java has a specific `host-cookie` and `uids` cookie processing for all endpoints, that sets `uids.HOST-BIDDER` from `host-cookie` if first is absent or not equal to second. 1) PBS-Java has a specific `/cookie-sync` behaviour, that sets `/setuid` as usersync-url for host-bidder if `host-cookie` specified but `uids.HOST-BIDDER` undefined or differs. 1) PBS-Java has `/event` endpoint to allow Web browsers and mobile applications to notify about different ad events (win, view etc). Filling new bid extensions `response.seatbid.bid.ext.prebid.events.{win,view}` with events url after successful auction completing makes it possible. 1) PBS-Java supports per-account cache TTL and event URLs configuration in the database in columns `banner_cache_ttl`, `video_cache_ttl` and `events_enabled`. 1) PBS-Java does not support passing bidder extensions in `imp[...].ext.prebid.bidder`. PBS-Go [PR 846](https://github.com/prebid/prebid-server/pull/846) 1) PBS-Java responds with active bidders only in `/info/bidders` and `/info/bidders/all` endpoints, although PBS-Go returns all implemented ones. Calling `/info/bidders/{bidderName}` with a disabled bidder name will result in 404 Not Found, which is a desired behaviour, unlike in PBS-Go [issue 988](https://github.com/prebid/prebid-server/issues/988), [PR](https://github.com/prebid/prebid-server/pull/989). -1) PBS-Java supports video impression tracking [issue 1015](https://github.com/prebid/prebid-server/issues/1015). PBS-Java [PR 437](https://github.com/rubicon-project/prebid-server-java/pull/437). +1) PBS-Java supports video impression tracking [issue 1015](https://github.com/prebid/prebid-server/issues/1015). PBS-Java [PR 437](https://github.com/prebid/prebid-server-java/pull/437). ## Minor differences diff --git a/docs/endpoints/auction.md b/docs/endpoints/auction.md index ac4f19f1c6a..9eb35f77914 100644 --- a/docs/endpoints/auction.md +++ b/docs/endpoints/auction.md @@ -1,6 +1,6 @@ # Auction The `/auction` endpoint expects a JSON request in to format defined by -[pbs_request.json](https://github.com/rubicon-project/prebid-server-java/blob/master/src/main/resources/static/pbs_request.json). +[pbs_request.json](../../src/main/resources/static/pbs_request.json). This endpoint does not support OpenRTB request, so can be assumed as legacy. diff --git a/docs/endpoints/info/bidders/bidderName.md b/docs/endpoints/info/bidders/bidderName.md index f3a041635ca..bf4b65dc4e7 100644 --- a/docs/endpoints/info/bidders/bidderName.md +++ b/docs/endpoints/info/bidders/bidderName.md @@ -34,7 +34,7 @@ This endpoint returns JSON like: The fields hold the following information: -- `maintainer.email`: A contact email for the Bidder's maintainer. In general, Bidder bugs should be logged as [issues](https://github.com/rubicon-project/prebid-server-java/issues)... but this contact email may be useful in case of emergency. +- `maintainer.email`: A contact email for the Bidder's maintainer. In general, Bidder bugs should be logged as [issues](https://github.com/prebid/prebid-server-java/issues)... but this contact email may be useful in case of emergency. - `capabilities.app.mediaTypes`: A list of media types this Bidder supports from Mobile Apps. - `capabilities.site.mediaTypes`: A list of media types this Bidder supports from Web pages. diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index ed43a951a1a..ec690ae104a 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -768,8 +768,8 @@ This supports publishers who want to sell different impressions to different bid This endpoint returns a 400 if the request contains deprecated properties (e.g. `imp.wmin`, `imp.hmax`). The error message in the response should describe how to "fix" the request to make it legal. -If the message is unclear, please [log an issue](https://github.com/rubicon-project/prebid-server-java/issues) -or [submit a pull request](https://github.com/rubicon-project/prebid-server-java/pulls) to improve it. +If the message is unclear, please [log an issue](https://github.com/prebid/prebid-server-java/issues) +or [submit a pull request](https://github.com/prebid/prebid-server-java/pulls) to improve it. #### Determining Bid Security (http/https) diff --git a/docs/metrics.md b/docs/metrics.md index 51521670249..75ad003f72b 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -86,7 +86,7 @@ Following metrics are collected and submitted if account is configured with `det - `account...request_time` - timer tracking how long did it take to make a request to `` when incoming request was from `` - `account...bids_received` - number of bids received from `` when incoming request was from `` - `account...requests.(gotbids|nobid)` - number of requests made to `` broken down by result status when incoming request was from `` -- `account..requests.rejected` - number of rejected requests caused by incorrect `accountId` ([UnauthorizedAccountException.java](https://github.com/rubicon-project/prebid-server-java/blob/master/src/main/java/org/prebid/server/exception/UnauthorizedAccountException.java)) +- `account..requests.rejected` - number of rejected requests caused by incorrect `accountId` ## General Prebid Cache metrics - `prebid_cache.requests.ok` - timer tracking how long did successful cache requests take diff --git a/pom.xml b/pom.xml index c49b9e56288..74bf827503a 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ - scm:git:git@github.com:rubicon-project/prebid-server-java.git + scm:git:git@github.com:prebid/prebid-server-java.git HEAD From 356a66a88e11da6de62c84b05ab03d773c2b1f08 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Thu, 19 Nov 2020 15:24:30 +0200 Subject: [PATCH 010/129] Admixer bidder review (#1025) --- .../server/bidder/admixer/AdmixerBidder.java | 27 ++-- src/main/resources/bidder-config/admixer.yaml | 2 +- .../bidder/admixer/AdmixerBidderTest.java | 125 +++++++++++++++--- 3 files changed, 123 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java b/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java index eb2d1a8bda1..0ab9489dc9c 100644 --- a/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java +++ b/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java @@ -9,6 +9,7 @@ import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -54,11 +55,6 @@ public Result>> makeHttpRequests(BidRequest request final List errors = new ArrayList<>(); final List validImps = new ArrayList<>(); - if (CollectionUtils.isEmpty(request.getImp())) { - errors.add(BidderError.badInput("No valid impressions in the bid request")); - return Result.withErrors(errors); - } - for (Imp imp : request.getImp()) { try { final ExtImpAdmixer extImp = parseImpExt(imp); @@ -83,21 +79,25 @@ public Result>> makeHttpRequests(BidRequest request } private ExtImpAdmixer parseImpExt(Imp imp) { + final ExtImpAdmixer extImpAdmixer; try { - return mapper.mapper().convertValue(imp.getExt(), ADMIXER_EXT_TYPE_REFERENCE).getBidder(); + extImpAdmixer = mapper.mapper().convertValue(imp.getExt(), ADMIXER_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); + throw new PreBidException(String.format("Wrong Admixer bidder ext in imp with id : %s", imp.getId())); } - } - - private Imp processImp(Imp imp, ExtImpAdmixer extImpAdmixer) { - if (extImpAdmixer.getZone().length() != 36) { + if (StringUtils.length(extImpAdmixer.getZone()) != 36) { throw new PreBidException("ZoneId must be UUID/GUID"); } + return extImpAdmixer; + } + + private Imp processImp(Imp imp, ExtImpAdmixer extImpAdmixer) { + final Double extImpFloor = extImpAdmixer.getCustomFloor(); + final BigDecimal customFloor = extImpFloor != null ? BigDecimal.valueOf(extImpFloor) : BigDecimal.ZERO; return imp.toBuilder() .tagid(extImpAdmixer.getZone()) - .bidfloor(BigDecimal.valueOf(extImpAdmixer.getCustomFloor())) + .bidfloor(customFloor) .ext(makeImpExt(extImpAdmixer.getCustomParams())) .build(); } @@ -117,7 +117,8 @@ public Result> makeBids(HttpCall httpCall, BidReques return Result.withError(BidderError.badServerResponse(e.getMessage())); } - if (CollectionUtils.isEmpty(bidResponse.getSeatbid()) + if (bidResponse == null + || CollectionUtils.isEmpty(bidResponse.getSeatbid()) || CollectionUtils.isEmpty(bidResponse.getSeatbid().get(0).getBid())) { return Result.empty(); } diff --git a/src/main/resources/bidder-config/admixer.yaml b/src/main/resources/bidder-config/admixer.yaml index 82cf3b92552..244dbccdcb6 100644 --- a/src/main/resources/bidder-config/admixer.yaml +++ b/src/main/resources/bidder-config/admixer.yaml @@ -20,7 +20,7 @@ adapters: - native - audio supported-vendors: - vendor-id: 0 + vendor-id: 511 usersync: url: https://inv-nets.admixer.net/adxcm.aspx?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=1&rurl= redirect-url: /setuid?bidder=admixer&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$$visitor_cookie$$ diff --git a/src/test/java/org/prebid/server/bidder/admixer/AdmixerBidderTest.java b/src/test/java/org/prebid/server/bidder/admixer/AdmixerBidderTest.java index 84bfc3f3c18..462a7d78710 100644 --- a/src/test/java/org/prebid/server/bidder/admixer/AdmixerBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/admixer/AdmixerBidderTest.java @@ -23,7 +23,7 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.admixer.ExtImpAdmixer; -import java.util.Collections; +import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -55,18 +55,79 @@ public void creationShouldFailOnInvalidEndpointUrl() { } @Test - public void makeHttpRequestsShouldReturnErrorIfImpressionListSizeIsZero() { + public void shouldSetBidfloorToZeroIfExtImpFloorValuIsNull() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(emptyList()) + .imp(singletonList(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpAdmixer.of("tentententtentententtentententetetet", null, + givenCustomParams("foo1", singletonList("bar1")))))) + .build())) .build(); // when final Result>> result = admixerBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("No valid impressions in the bid request")); + final Imp expectedImp = Imp.builder() + .id("123") + .tagid("tentententtentententtentententetetet") + .bidfloor(BigDecimal.ZERO) + .ext(mapper.valueToTree(ExtImpAdmixer.of(null, null, + givenCustomParams("foo1", singletonList("bar1"))))) + .build(); + assertThat(result.getErrors()).hasSize(0); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .containsExactly(expectedImp); + } + + @Test + public void shouldSetExtToNullIfCustomParamsAreNotPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpAdmixer.of("tentententtentententtentententetetet", null, null)))) + .build())) + .build(); + + // when + final Result>> result = admixerBidder.makeHttpRequests(bidRequest); + + // then + final Imp expectedImp = Imp.builder() + .id("123") + .tagid("tentententtentententtentententetetet") + .bidfloor(BigDecimal.ZERO) + .ext(null) + .build(); + assertThat(result.getErrors()).hasSize(0); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .containsExactly(expectedImp); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))).build())) + .build(); + + // when + final Result>> result = admixerBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_input + && error.getMessage().startsWith("Wrong Admixer bidder ext in imp with id : 123")); } @Test @@ -84,7 +145,7 @@ public void makeHttpRequestsShouldReturnErrorIfZoneIdNotHaveLength() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("ZoneId must be UUID/GUID")); + .containsExactly(BidderError.badInput("ZoneId must be UUID/GUID")); } @Test @@ -96,7 +157,22 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { final Result> result = admixerBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_server_response + && error.getMessage().startsWith("Failed to decode:")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyResponseWhenResponseIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = + givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = admixerBidder.makeBids(httpCall, BidRequest.builder().build()); + + // then + assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).isEmpty(); } @@ -111,9 +187,7 @@ public void makeBidsShouldReturnErrorsWhenSeatBidIsEmptyList() throws JsonProces // then assertThat(result.getErrors()).isEmpty(); - assertThat(result).isNotNull() - .extracting(Result::getValue, Result::getErrors) - .containsOnly(Collections.emptyList(), Collections.emptyList()); + assertThat(result.getValue()).isEmpty(); } @Test @@ -131,9 +205,26 @@ public void makeBidsShouldReturnErrorsWhenBidsEmptyList() // then assertThat(result.getErrors()).isEmpty(); - assertThat(result).isNotNull() - .extracting(Result::getValue, Result::getErrors) - .containsOnly(Collections.emptyList(), Collections.emptyList()); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = admixerBidder.makeBids(httpCall, + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build()); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); } @Test @@ -152,7 +243,7 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); } @Test @@ -171,7 +262,7 @@ public void makeBidsShouldReturnBannerBidIfVideoIsPresentInRequestImp() throws J // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); } @Test @@ -190,7 +281,7 @@ public void makeBidsShouldReturnBannerBidIfNativeIsPresentInRequestImp() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); } @Test @@ -209,7 +300,7 @@ public void makeBidsShouldReturnBannerBidIfAudioIsPresentInRequestImp() throws J // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD")); } private static BidResponse givenBidResponse(Function bidCustomizer) { From a36c40afcdc4a00929248aaa2b5344eb11df7608 Mon Sep 17 00:00:00 2001 From: Sander Date: Fri, 20 Nov 2020 10:23:40 +0100 Subject: [PATCH 011/129] Use POST, preserve original user agent and user ip in the headers (#1020) * Use POST, preserve original user agent and user ip in the headers * Change test to stub for post * cleanup * actually return modified headers --- .../prebid/server/bidder/adhese/AdheseBidder.java | 15 +++++++++++++-- .../java/org/prebid/server/it/AdheseTest.java | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java index 1c22887bfe7..6e4c55f666c 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java +++ b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java @@ -11,6 +11,8 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -83,13 +85,22 @@ public Result>> makeHttpRequests(BidRequest request) { return Result.of(Collections.singletonList( HttpRequest.builder() - .method(HttpMethod.GET) + .method(HttpMethod.POST) .uri(uri) - .headers(HttpUtil.headers()) + .headers(replaceHeaders(request.getDevice())) .build()), Collections.emptyList()); } + private MultiMap replaceHeaders(Device device) { + MultiMap headers = HttpUtil.headers(); + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpHeaders.createOptimized("X-Real-IP"), device.getIp()); + } + return headers; + } + private ExtImpAdhese parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), ADHESE_EXT_TYPE_REFERENCE).getBidder(); diff --git a/src/test/java/org/prebid/server/it/AdheseTest.java b/src/test/java/org/prebid/server/it/AdheseTest.java index 5c108aaff67..8c714060b30 100644 --- a/src/test/java/org/prebid/server/it/AdheseTest.java +++ b/src/test/java/org/prebid/server/it/AdheseTest.java @@ -20,7 +20,7 @@ public class AdheseTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdhese() throws IOException, JSONException { - WIRE_MOCK_RULE.stubFor(WireMock.get(WireMock.urlPathEqualTo("/adhese-exchange/sl_adhese_prebid_demo_-" + WIRE_MOCK_RULE.stubFor(WireMock.post(WireMock.urlPathEqualTo("/adhese-exchange/sl_adhese_prebid_demo_-" + "leaderboard/ag55/cigent;brussels/tlall/xtconsentValue/xfhttp%3A%2F%2Fwww.example.com/xzifaId")) .withHeader("Accept", WireMock.equalTo("application/json")) .withHeader("Content-Type", WireMock.equalTo("application/json;charset=UTF-8")) From 97a682f353db1a4dfc0a17e0f84807cbfb728361 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Fri, 20 Nov 2020 11:38:18 +0200 Subject: [PATCH 012/129] Avoid serializing analytics event entirely in LogAnalyticsReporter since event could contain non-serializable objects (#1028) --- .../analytics/LogAnalyticsReporter.java | 20 +++++++++++-------- .../server/auction/model/AuctionContext.java | 4 ---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/prebid/server/analytics/LogAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/LogAnalyticsReporter.java index 7fc108f1d42..3ca358a9cca 100644 --- a/src/main/java/org/prebid/server/analytics/LogAnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/LogAnalyticsReporter.java @@ -29,22 +29,26 @@ public LogAnalyticsReporter(JacksonMapper mapper) { @Override public void processEvent(T event) { - final String type; + final LogEvent logEvent; + if (event instanceof AuctionEvent) { - type = "/openrtb2/auction"; + logEvent = new LogEvent<>("/openrtb2/auction", ((AuctionEvent) event).getBidResponse()); } else if (event instanceof AmpEvent) { - type = "/openrtb2/amp"; + logEvent = new LogEvent<>("/openrtb2/amp", ((AmpEvent) event).getBidResponse()); } else if (event instanceof VideoEvent) { - type = "/openrtb2/video"; + logEvent = new LogEvent<>("/openrtb2/video", ((VideoEvent) event).getBidResponse()); } else if (event instanceof SetuidEvent) { - type = "/setuid"; + final SetuidEvent setuidEvent = (SetuidEvent) event; + logEvent = new LogEvent<>( + "/setuid", + setuidEvent.getBidder() + ":" + setuidEvent.getUid() + ":" + setuidEvent.getSuccess()); } else if (event instanceof CookieSyncEvent) { - type = "/cookie_sync"; + logEvent = new LogEvent<>("/cookie_sync", ((CookieSyncEvent) event).getBidderStatus()); } else { - type = "unknown"; + logEvent = new LogEvent<>("unknown", null); } - logger.debug(mapper.encode(new LogEvent<>(type, event))); + logger.debug(mapper.encode(logEvent)); } @AllArgsConstructor diff --git a/src/main/java/org/prebid/server/auction/model/AuctionContext.java b/src/main/java/org/prebid/server/auction/model/AuctionContext.java index 1870839a7a5..6e72ea05484 100644 --- a/src/main/java/org/prebid/server/auction/model/AuctionContext.java +++ b/src/main/java/org/prebid/server/auction/model/AuctionContext.java @@ -1,6 +1,5 @@ package org.prebid.server.auction.model; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.iab.openrtb.request.BidRequest; import io.vertx.ext.web.RoutingContext; import lombok.Builder; @@ -20,15 +19,12 @@ @Value public class AuctionContext { - @JsonIgnore RoutingContext routingContext; - @JsonIgnore UidsCookie uidsCookie; BidRequest bidRequest; - @JsonIgnore Timeout timeout; Account account; From 47ad1ff98ce846c69dfabb422e36fdc473c5e525 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Mon, 30 Nov 2020 15:56:28 +0200 Subject: [PATCH 013/129] Review SmartadserverBidder (#1037) --- .../smartadserver/SmartadserverBidder.java | 58 +++++----- .../SmartadserverBidderTest.java | 102 +++++++++++++++--- .../prebid/server/it/SmartadserverTest.java | 4 +- .../test-auction-smartadserver-response.json | 2 +- 4 files changed, 120 insertions(+), 46 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java b/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java index 432f0ee3ef6..c5764ca7fc3 100644 --- a/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java +++ b/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java @@ -5,11 +5,11 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Site; -import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -25,6 +25,8 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -52,11 +54,12 @@ public SmartadserverBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { final List> result = new ArrayList<>(); + final List errors = new ArrayList<>(); for (Imp imp : request.getImp()) { try { final ExtImpSmartadserver extImpSmartadserver = parseImpExt(imp); - final BidRequest updateRequest = request.toBuilder() + final BidRequest updatedRequest = request.toBuilder() .imp(Collections.singletonList(imp)) .site(Site.builder() .publisher(Publisher.builder() @@ -64,42 +67,46 @@ public Result>> makeHttpRequests(BidRequest request .build()) .build()) .build(); - result.add(createSingleRequest(updateRequest, getUri())); + result.add(createSingleRequest(updatedRequest)); } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); + errors.add(BidderError.badInput(e.getMessage())); } } - return Result.withValues(result); + return Result.of(result, errors); } private ExtImpSmartadserver parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), SMARTADSERVER_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); + throw new PreBidException("Error parsing smartadserverExt parameters"); } } - private String getUri() { - final URIBuilder uriBuilder = new URIBuilder() - .setPath(String.join("api/bid", endpointUrl)) - .addParameter("callerId", "5"); - - return uriBuilder.toString(); - } + private HttpRequest createSingleRequest(BidRequest request) { - private HttpRequest createSingleRequest(BidRequest request, String url) { - final BidRequest outgoingRequest = request.toBuilder().build(); - final String body = mapper.encode(outgoingRequest); return HttpRequest.builder() .method(HttpMethod.POST) - .uri(url) + .uri(getUri()) .headers(HttpUtil.headers()) - .body(body) - .payload(outgoingRequest) + .body(mapper.encode(request)) + .payload(request) .build(); } + private String getUri() { + final URI uri; + try { + uri = new URI(endpointUrl); + } catch (URISyntaxException e) { + throw new PreBidException(String.format("Malformed URL: %s.", endpointUrl)); + } + return new URIBuilder(uri) + .setPath(String.format("%s/api/bid", StringUtils.removeEnd(uri.getPath(), "/"))) + .addParameter("callerId", "5") + .toString(); + } + @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { @@ -120,22 +127,11 @@ private Result> extractBids(BidRequest bidRequest, BidResponse b .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> toBidderBid(bidRequest, bid, bidResponse.getCur(), errors)) - .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); return Result.of(bidderBids, errors); } - private BidderBid toBidderBid(BidRequest bidRequest, Bid bid, String currency, List errors) { - try { - final BidType bidType = getBidType(bid.getImpid(), bidRequest.getImp()); - return BidderBid.of(bid, bidType, currency); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - return null; - } - } - private static BidType getBidType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId)) { diff --git a/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java b/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java index 3e44d89262e..fe54c02a0fc 100644 --- a/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java @@ -20,6 +20,7 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.smartadserver.ExtImpSmartadserver; +import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -31,7 +32,7 @@ public class SmartadserverBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://test.endpoint.com/"; + private static final String ENDPOINT_URL = "https://test.endpoint.com/path?testParam=testVal"; private SmartadserverBidder smartadserverBidder; @@ -49,25 +50,25 @@ public void creationShouldFailOnInvalidEndpointUrl() { public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) - .build())) + .imp(singletonList( + Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) + .build())) .build(); + // when final Result>> result = smartadserverBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance"); + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("Error parsing smartadserverExt parameters")); } @Test public void makeHttpRequestsShouldCreateCorrectURL() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSmartadserver.of(1, 2, 3, 4)))) - .build())) + .imp(singletonList(givenImp(Function.identity()))) .build(); // when @@ -76,7 +77,54 @@ public void makeHttpRequestsShouldCreateCorrectURL() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1); - assertThat(result.getValue().get(0).getUri()).isEqualTo("https://test.endpoint.com/?callerId=5"); + assertThat(result.getValue().get(0).getUri()) + .isEqualTo("https://test.endpoint.com/path/api/bid?testParam=testVal&callerId=5"); + } + + @Test + public void makeHttpRequestsShouldCreateRequestForEveryValidImp() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(Arrays.asList(givenImp(Function.identity()), + givenImp(impBuilder -> impBuilder.id("456")) + )) + .build(); + + // when + final Result>> result = smartadserverBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .flatExtracting(Imp::getId) + .containsExactly("123", "456"); + } + + @Test + public void makeHttpRequestsShouldCreateRequestForValidImpAndSaveErrorForInvalid() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(Arrays.asList(givenImp(impBuilder -> impBuilder.id("456")), + Imp.builder() + .id("invalidImp") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) + .build() + )) + .build(); + + // when + final Result>> result = smartadserverBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("Error parsing smartadserverExt parameters")); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .flatExtracting(Imp::getId) + .containsExactly("456"); } @Test @@ -88,9 +136,9 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { final Result> result = smartadserverBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(result.getErrors()) + .allMatch(error -> error.getMessage().startsWith("Failed to decode: Unrecognized token") + && error.getType() == BidderError.Type.bad_server_response); assertThat(result.getValue()).isEmpty(); } @@ -141,6 +189,25 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessi .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR")); } + @Test + public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(Function.identity()))); + + // when + final Result> result = smartadserverBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().build(), banner, "EUR")); + } + @Test public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessingException { // given @@ -160,6 +227,15 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessing .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "EUR")); } + private static Imp givenImp(Function impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123")) + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSmartadserver.of(1, 2, 3, 4)))) + .build(); + } + private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() .cur("EUR") diff --git a/src/test/java/org/prebid/server/it/SmartadserverTest.java b/src/test/java/org/prebid/server/it/SmartadserverTest.java index dcfc58f6b23..da9a567f618 100644 --- a/src/test/java/org/prebid/server/it/SmartadserverTest.java +++ b/src/test/java/org/prebid/server/it/SmartadserverTest.java @@ -11,6 +11,7 @@ import java.io.IOException; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; @@ -24,7 +25,8 @@ public class SmartadserverTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromSmartadserver() throws IOException, JSONException { // given // Smartadserver bid response for imp 001 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smartadserver-exchange")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smartadserver-exchange/api/bid")) + .withQueryParam("callerId", equalTo("5")) .withRequestBody(equalToJson(jsonFrom("openrtb2/smartadserver/test-smartadserver-bid-request-1.json"))) .willReturn(aResponse() .withBody(jsonFrom("openrtb2/smartadserver/test-smartadserver-bid-response-1.json")))); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json index 00a73c254e1..62365b0bbe3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json @@ -61,7 +61,7 @@ ], "smartadserver": [ { - "uri": "{{ smartadserver.exchange_uri }}?callerId=5", + "uri": "{{ smartadserver.exchange_uri }}/api/bid?callerId=5", "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-banner-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":500,\"h\":400},\"ext\":{\"bidder\":{\"siteId\":1,\"pageId\":2,\"formatId\":3,\"networkId\":73}}}],\"site\":{\"publisher\":{\"id\":\"73\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"SA-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-banner-id\",\"price\":0.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"advertsite.com\"],\"cid\":\"772\",\"crid\":\"29681110\",\"h\":576,\"w\":1024}]}]}", "status": 200 From a7b34efeb126c05af98d206812ad8363a76a0d8f Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Wed, 2 Dec 2020 12:18:14 +0200 Subject: [PATCH 014/129] Prebid Server prepare release 1.50.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 74bf827503a..93ebc5915ff 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.50.0-SNAPSHOT + 1.50.0 prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 1.50.0 From c37d81243d43832ac6af2d8adafe4777d1a6faec Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Wed, 2 Dec 2020 12:18:26 +0200 Subject: [PATCH 015/129] Prebid Server prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 93ebc5915ff..23c05b24237 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.50.0 + 1.51.0-SNAPSHOT prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - 1.50.0 + HEAD From f31d91ba35ce33ef468218b298dd20ecdbe31aab Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Wed, 2 Dec 2020 13:39:17 +0200 Subject: [PATCH 016/129] Fix github actions release publishing with set env in new way (#1043) --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index f5d07b2639d..b34d4827eae 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -14,7 +14,7 @@ jobs: - name: Extract tag from commit message run: | target_tag=${COMMIT_MSG#"Prebid Server prepare release "} - echo "::set-env name=TARGET_TAG::$target_tag" + echo "TARGET_TAG=$target_tag" >> $GITHUB_ENV env: COMMIT_MSG: ${{ github.event.head_commit.message }} - name: Create and publish release From 4e3bc4ee5034a94ae27b7259eee53094b9d3f435 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Wed, 2 Dec 2020 13:46:55 +0200 Subject: [PATCH 017/129] Prebid Server prepare release 1.50.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 23c05b24237..93ebc5915ff 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.51.0-SNAPSHOT + 1.50.0 prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 1.50.0 From f326927eb6d087ca1b94eef84d31b73ed2467eb2 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Wed, 2 Dec 2020 13:47:10 +0200 Subject: [PATCH 018/129] Prebid Server prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 93ebc5915ff..23c05b24237 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.50.0 + 1.51.0-SNAPSHOT prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - 1.50.0 + HEAD From 2fcb8abd013c7cbd6f48f7238df35066d6afc76f Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Wed, 2 Dec 2020 14:50:09 +0200 Subject: [PATCH 019/129] Add currency rates staleness metric (#1030) --- docs/config-app.md | 1 + docs/metrics.md | 1 + .../currency/CurrencyConversionService.java | 23 +++++-- .../server/metric/CurrencyRatesMetrics.java | 42 ++++++++++++ .../org/prebid/server/metric/MetricName.java | 5 +- .../org/prebid/server/metric/Metrics.java | 10 +++ .../spring/config/ServiceConfiguration.java | 17 ++++- .../model/ExternalConversionProperties.java | 15 ++++- src/main/resources/application.yaml | 1 + .../CurrencyConversionServiceTest.java | 67 ++++++++++++++++--- .../org/prebid/server/metric/MetricsTest.java | 9 +++ 11 files changed, 171 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/prebid/server/metric/CurrencyRatesMetrics.java diff --git a/docs/config-app.md b/docs/config-app.md index 5716bd19717..e13eb89ce47 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -134,6 +134,7 @@ But feel free to add additional bidder's specific options. - `currency-converter.external-rates.url` - the url for Prebid.org’s currency file. [More details](http://prebid.org/dev-docs/modules/currency.html) - `currency-converter.external-rates.default-timeout-ms` - default operation timeout for fetching currency rates. - `currency-converter.external-rates.refresh-period-ms` - default refresh period for currency rates updates. +- `currency-converter.external-rates.stale-after-ms` - how old currency rates should be to become considered stale. ## Admin Endpoints - `admin-endpoints.version.enabled` - if equals to `true` the endpoint will be available. diff --git a/docs/metrics.md b/docs/metrics.md index 75ad003f72b..185e143c427 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -63,6 +63,7 @@ where `[DATASOURCE]` is a data source name, `DEFAULT_DS` by defaul. - `circuit-breaker.geo.opened` - state of the geo location circuit breaker: `1` means opened (geo location resource is unavailable), `0` - closed - `timeout_notification.ok` - number of times bidders were successfully notified about timeouts - `timeout_notification.failed` - number of unsuccessful attempts to notify bidders about timeouts +- `currency-rates.stale` - a flag indicating if currency rates obtained from external source are fresh (`0`) or stale (`1`) ## Auction per-adapter metrics - `adapter..no_cookie_requests` - number of requests made to `` that did not contain UID diff --git a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java index 8645c86d458..43060faf4d0 100644 --- a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java +++ b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java @@ -19,7 +19,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; -import java.time.Clock; +import java.time.Duration; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; @@ -64,14 +64,16 @@ public CurrencyConversionService(ExternalConversionProperties externalConversion @Override public void initialize() { if (externalConversionProperties != null) { - final Long refreshPeriod = externalConversionProperties.getRefreshPeriod(); - final Long defaultTimeout = externalConversionProperties.getDefaultTimeout(); + final Long refreshPeriod = externalConversionProperties.getRefreshPeriodMs(); + final Long defaultTimeout = externalConversionProperties.getDefaultTimeoutMs(); final HttpClient httpClient = externalConversionProperties.getHttpClient(); final Vertx vertx = externalConversionProperties.getVertx(); vertx.setPeriodic(refreshPeriod, ignored -> populatesLatestCurrencyRates(currencyServerUrl, defaultTimeout, httpClient)); populatesLatestCurrencyRates(currencyServerUrl, defaultTimeout, httpClient); + + externalConversionProperties.getMetrics().createCurrencyRatesGauge(this::isRatesStale); } } @@ -108,7 +110,7 @@ private CurrencyConversionRates updateCurrencyRates(CurrencyConversionRates curr final Map> receivedCurrencyRates = currencyConversionRates.getConversions(); if (receivedCurrencyRates != null) { externalCurrencyRates = receivedCurrencyRates; - lastUpdated = ZonedDateTime.now(Clock.systemUTC()); + lastUpdated = ZonedDateTime.now(externalConversionProperties.getClock()); } return currencyConversionRates; } @@ -130,7 +132,7 @@ public String getCurrencyServerUrl() { } public Long getRefreshPeriod() { - return externalConversionProperties != null ? externalConversionProperties.getRefreshPeriod() : null; + return externalConversionProperties != null ? externalConversionProperties.getRefreshPeriodMs() : null; } public ZonedDateTime getLastUpdated() { @@ -263,4 +265,15 @@ private static BigDecimal findIntermediateConversionRate(Map } return conversionRate; } + + private boolean isRatesStale() { + if (lastUpdated == null) { + return false; + } + + final ZonedDateTime stalenessBoundary = ZonedDateTime.now(externalConversionProperties.getClock()) + .minus(Duration.ofMillis(externalConversionProperties.getStaleAfterMs())); + + return lastUpdated.isBefore(stalenessBoundary); + } } diff --git a/src/main/java/org/prebid/server/metric/CurrencyRatesMetrics.java b/src/main/java/org/prebid/server/metric/CurrencyRatesMetrics.java new file mode 100644 index 00000000000..b85eba57d85 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/CurrencyRatesMetrics.java @@ -0,0 +1,42 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Circuit breaker metrics support. + */ +class CurrencyRatesMetrics extends UpdatableMetrics { + + private static final String SUFFIX = ".count"; + + CurrencyRatesMetrics(MetricRegistry metricRegistry, CounterType counterType) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), nameCreator()); + } + + private static Function nameCreator() { + return metricName -> String.format("currency-rates.%s%s", metricName.toString(), SUFFIX); + } + + @Override + void incCounter(MetricName metricName) { + throw new UnsupportedOperationException(); + } + + @Override + void incCounter(MetricName metricName, long value) { + throw new UnsupportedOperationException(); + } + + @Override + void updateTimer(MetricName metricName, long millis) { + throw new UnsupportedOperationException(); + } + + @Override + void updateHistogram(MetricName metricName, long value) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index 0418dbde191..15fa3e82df6 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -101,7 +101,10 @@ public enum MetricName { creative_size, //account.*.requests. - rejected; + rejected, + + //currency rates + stale; private final String name; diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 05d333fc31e..cd6d659f380 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -45,6 +45,7 @@ public class Metrics extends UpdatableMetrics { private final Map circuitBreakerMetrics; private final CacheMetrics cacheMetrics; private final TimeoutNotificationMetrics timeoutNotificationMetrics; + private final CurrencyRatesMetrics currencyRatesMetrics; public Metrics(MetricRegistry metricRegistry, CounterType counterType, AccountMetricsVerbosity accountMetricsVerbosity, BidderCatalog bidderCatalog) { @@ -69,6 +70,7 @@ public Metrics(MetricRegistry metricRegistry, CounterType counterType, AccountMe circuitBreakerMetrics = new HashMap<>(); cacheMetrics = new CacheMetrics(metricRegistry, counterType); timeoutNotificationMetrics = new TimeoutNotificationMetrics(metricRegistry, counterType); + currencyRatesMetrics = new CurrencyRatesMetrics(metricRegistry, counterType); } RequestStatusMetrics forRequestType(MetricName requestType) { @@ -107,6 +109,10 @@ CacheMetrics cache() { return cacheMetrics; } + CurrencyRatesMetrics currencyRates() { + return currencyRatesMetrics; + } + public void updateSafariRequestsMetric(boolean isSafari) { if (isSafari) { incCounter(MetricName.safari_requests); @@ -454,6 +460,10 @@ public void updateTimeoutNotificationMetric(boolean success) { } } + public void createCurrencyRatesGauge(BooleanSupplier stateSupplier) { + currencyRates().createGauge(MetricName.stale, () -> stateSupplier.getAsBoolean() ? 1 : 0); + } + private String resolveMetricsBidderName(String bidder) { return bidderCatalog.isValidName(bidder) ? bidder : METRICS_UNKNOWN_BIDDER; } diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 4c48db51da1..84edca874ea 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -594,13 +594,24 @@ CurrencyConversionService currencyConversionService( @ConditionalOnProperty(prefix = "currency-converter.external-rates", name = "enabled", havingValue = "true") ExternalConversionProperties externalConversionProperties( @Value("${currency-converter.external-rates.url}") String currencyServerUrl, - @Value("${currency-converter.external-rates.default-timeout-ms}") long defaultTimeout, - @Value("${currency-converter.external-rates.refresh-period-ms}") long refreshPeriod, + @Value("${currency-converter.external-rates.default-timeout-ms}") long defaultTimeoutMs, + @Value("${currency-converter.external-rates.refresh-period-ms}") long refreshPeriodMs, + @Value("${currency-converter.external-rates.stale-after-ms}") long staleAfterMs, Vertx vertx, HttpClient httpClient, + Metrics metrics, + Clock clock, JacksonMapper mapper) { - return new ExternalConversionProperties(currencyServerUrl, defaultTimeout, refreshPeriod, vertx, httpClient, + return new ExternalConversionProperties( + currencyServerUrl, + defaultTimeoutMs, + refreshPeriodMs, + staleAfterMs, + vertx, + httpClient, + metrics, + clock, mapper); } diff --git a/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java b/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java index ac556347f51..fa97d533d73 100644 --- a/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java +++ b/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java @@ -4,12 +4,14 @@ import lombok.AllArgsConstructor; import lombok.Data; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.metric.Metrics; import org.prebid.server.vertx.http.HttpClient; import org.springframework.validation.annotation.Validated; import javax.validation.constraints.Min; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; +import java.time.Clock; @Validated @Data @@ -21,11 +23,14 @@ public class ExternalConversionProperties { @NotNull @Min(2) - Long defaultTimeout; + Long defaultTimeoutMs; @NotNull @Min(2) - Long refreshPeriod; + Long refreshPeriodMs; + + @NotNull + Long staleAfterMs; @NotNull Vertx vertx; @@ -33,6 +38,12 @@ public class ExternalConversionProperties { @NotNull HttpClient httpClient; + @NotNull + Metrics metrics; + + @NotNull + Clock clock; + @NotNull JacksonMapper mapper; } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 1e948d962dd..984c5955e9a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -113,6 +113,7 @@ currency-converter: url: https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json default-timeout-ms: 4000 refresh-period-ms: 900000 + stale-after-ms: 259200000 metrics: metricType: flushingCounter accounts: diff --git a/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java b/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java index 283786c38ea..326bfff2149 100644 --- a/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java +++ b/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java @@ -15,13 +15,18 @@ import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.currency.proto.CurrencyConversionRates; import org.prebid.server.exception.PreBidException; +import org.prebid.server.metric.Metrics; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.vertx.http.HttpClient; import org.prebid.server.vertx.http.model.HttpClientResponse; import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.HashMap; import java.util.Map; +import java.util.function.BooleanSupplier; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; @@ -53,6 +58,9 @@ public class CurrencyConversionServiceTest extends VertxTest { private HttpClient httpClient; @Mock private Vertx vertx; + @Mock + private Metrics metrics; + private final Clock clock = Clock.fixed(Instant.now(), ZoneOffset.UTC); private CurrencyConversionService currencyService; @@ -64,13 +72,13 @@ public void setUp() throws JsonProcessingException { givenHttpClientReturnsResponse(httpClient, 200, mapper.writeValueAsString(CurrencyConversionRates.of(null, currencyRates))); - currencyService = setExternalResource(URL, 1L, vertx, httpClient); + currencyService = createInitializedService(URL, 1L, -3600L, vertx, httpClient, metrics, clock); } @Test public void creationShouldFailOnInvalidCurrencyServerUrl() { assertThatIllegalArgumentException() - .isThrownBy(() -> setExternalResource("invalid-url", 1L, vertx, httpClient)) + .isThrownBy(() -> createInitializedService("invalid-url", 1L, -1L, vertx, httpClient, metrics, clock)) .withMessage("URL supplied is not valid: invalid-url"); } @@ -79,6 +87,30 @@ public void initializeShouldSetLastUpdatedDate() { assertThat(currencyService.getLastUpdated()).isNotNull(); } + @Test + public void currencyRatesGaugeShouldReportStale() { + // then + final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(BooleanSupplier.class); + verify(metrics).createCurrencyRatesGauge(gaugeValueProviderCaptor.capture()); + final BooleanSupplier gaugeValueProvider = gaugeValueProviderCaptor.getValue(); + + assertThat(gaugeValueProvider.getAsBoolean()).isTrue(); + } + + @Test + public void currencyRatesGaugeShouldReportNotStale() { + // when + metrics = mock(Metrics.class); // original mock is already spoiled by service initialization in setUp + currencyService = createInitializedService(URL, 1L, 3600L, vertx, httpClient, metrics, clock); + + // then + final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(BooleanSupplier.class); + verify(metrics).createCurrencyRatesGauge(gaugeValueProviderCaptor.capture()); + final BooleanSupplier gaugeValueProvider = gaugeValueProviderCaptor.getValue(); + + assertThat(gaugeValueProvider.getAsBoolean()).isFalse(); + } + @Test public void convertCurrencyShouldReturnSamePriceIfBidAndServerCurrenciesEquals() { // given @@ -251,7 +283,7 @@ public void convertCurrencyShouldThrowPrebidExceptionIfMultiplierWasNotFoundFrom givenHttpClientReturnsResponse(httpClient, 503, "server unavailable"); // when - currencyService = setExternalResource(URL, 1L, vertx, httpClient); + currencyService = createInitializedService(URL, 1L, -1L, vertx, httpClient, metrics, clock); // then assertThatExceptionOfType(PreBidException.class) @@ -266,7 +298,7 @@ public void convertCurrencyShouldThrowExceptionWhenCurrencyServerResponseStatusN givenHttpClientReturnsResponse(httpClient, 503, "server unavailable"); // when - currencyService = setExternalResource(URL, 1L, vertx, httpClient); + currencyService = createInitializedService(URL, 1L, -1L, vertx, httpClient, metrics, clock); // then assertThatExceptionOfType(PreBidException.class) @@ -280,7 +312,7 @@ public void convertCurrencyShouldThrowExceptionWhenCurrencyServerResponseContain givenHttpClientReturnsResponse(httpClient, 200, "{\"foo\": \"bar\"}"); // when - currencyService = setExternalResource(URL, 1L, vertx, httpClient); + currencyService = createInitializedService(URL, 1L, -1L, vertx, httpClient, metrics, clock); // then assertThatExceptionOfType(PreBidException.class) @@ -297,7 +329,7 @@ public void initializeShouldMakeOneInitialRequestAndTwoScheduled() { givenHttpClientReturnsResponse(httpClient, 200, "{\"foo\": \"bar\"}"); // when and then - currencyService = setExternalResource(URL, 1000, vertx, httpClient); + currencyService = createInitializedService(URL, 1000, -1L, vertx, httpClient, metrics, clock); final ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); verify(vertx).setPeriodic(eq(1000L), handlerCaptor.capture()); @@ -309,11 +341,28 @@ public void initializeShouldMakeOneInitialRequestAndTwoScheduled() { verify(httpClient, times(3)).get(anyString(), anyLong()); } - private static CurrencyConversionService setExternalResource(String url, long refreshPeriod, Vertx vertx, - HttpClient httpClient) { + private static CurrencyConversionService createInitializedService(String url, + long refreshPeriod, + long staleAfter, + Vertx vertx, + HttpClient httpClient, + Metrics metrics, + Clock clock) { + final CurrencyConversionService currencyService = new CurrencyConversionService( - new ExternalConversionProperties(url, 1000L, refreshPeriod, vertx, httpClient, jacksonMapper)); + new ExternalConversionProperties( + url, + 1000L, + refreshPeriod, + staleAfter, + vertx, + httpClient, + metrics, + clock, + jacksonMapper)); + currencyService.initialize(); + return currencyService; } diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index 0ddffed3dc7..a59d825ec65 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -987,6 +987,15 @@ public void shouldIncrementPrebidCacheCreativeSizeHistogram() { .isEqualTo(1); } + @Test + public void shouldCreateCurrencyRatesGaugeMetric() { + // when + metrics.createCurrencyRatesGauge(() -> true); + + // then + assertThat(metricRegistry.gauge("currency-rates.stale.count", () -> null).getValue()).isEqualTo(1L); + } + private void verifyCreatesConfiguredCounterType(Consumer metricsConsumer) { final EnumMap> counterTypeClasses = new EnumMap<>(CounterType.class); counterTypeClasses.put(CounterType.counter, Counter.class); From 7e85714a0ce144455f30e4213a7e955942819298 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Wed, 2 Dec 2020 14:57:10 +0200 Subject: [PATCH 020/129] Update conversant adapter for new prebid-server interface (#972) --- .../bidder/conversant/ConversantBidder.java | 127 +++++++----------- .../static/bidder-params/conversant.json | 6 +- .../conversant/ConversantBidderTest.java | 40 ++---- .../alias/test-conversant-bid-request.json | 4 +- .../test-conversant-bid-request.json | 6 +- 5 files changed, 66 insertions(+), 117 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java b/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java index cbf86bbddbe..3ddd89ae758 100644 --- a/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java +++ b/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java @@ -11,7 +11,6 @@ import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -29,7 +28,6 @@ import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -56,7 +54,7 @@ public class ConversantBidder implements Bidder { private static final Set AD_POSITIONS = IntStream.range(0, 8).boxed().collect(Collectors.toSet()); private static final String DISPLAY_MANAGER = "prebid-s2s"; - private static final String DISPLAY_MANAGER_VER = "1.0.1"; + private static final String DISPLAY_MANAGER_VER = "2.0.0"; private final String endpointUrl; private final JacksonMapper mapper; @@ -68,15 +66,12 @@ public ConversantBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest bidRequest) { - final List errors = new ArrayList<>(); final BidRequest outgoingRequest; try { - outgoingRequest = createBidRequest(bidRequest, errors); + outgoingRequest = createOutgoingRequest(bidRequest); } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - return Result.withErrors(errors); + return Result.withError(BidderError.badInput(e.getMessage())); } - final String body = mapper.encode(outgoingRequest); return Result.of(Collections.singletonList( @@ -87,77 +82,50 @@ public Result>> makeHttpRequests(BidRequest bidRequ .headers(HttpUtil.headers()) .payload(outgoingRequest) .build()), - errors); + Collections.emptyList()); } - private BidRequest createBidRequest(BidRequest bidRequest, List errors) { + private BidRequest createOutgoingRequest(BidRequest bidRequest) { + final BidRequest.BidRequestBuilder requestBuilder = bidRequest.toBuilder(); final List modifiedImps = new ArrayList<>(); - Integer extMobile = null; - String extSiteId = null; - - for (Imp imp : bidRequest.getImp()) { - try { - validateImp(imp); - final ExtImpConversant impExt = parseImpExt(imp); - modifiedImps.add(modifyImp(imp, impExt)); - if (StringUtils.isNotEmpty(impExt.getSiteId())) { - extSiteId = impExt.getSiteId(); + final List requestImps = bidRequest.getImp(); + for (int i = 0; i < requestImps.size(); i++) { + final Imp imp = bidRequest.getImp().get(i); + final ExtImpConversant impExt = parseImpExt(imp, i); + if (i == 0) { + final String siteId = impExt.getSiteId(); + if (bidRequest.getSite() != null) { + requestBuilder.site(updateSite(bidRequest.getSite(), siteId)); + } else if (bidRequest.getApp() != null) { + requestBuilder.app(updateApp(bidRequest.getApp(), siteId)); } - if (impExt.getMobile() != null) { - extMobile = impExt.getMobile(); - } - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - } - } - if (modifiedImps.isEmpty()) { - throw new PreBidException("No valid impressions"); - } - - final Site site = bidRequest.getSite(); - final App app = bidRequest.getApp(); - validateSiteAppId(extSiteId, site, app); - - final BidRequest.BidRequestBuilder requestBuilder = bidRequest.toBuilder() - .imp(modifiedImps); - - if (site != null) { - if (StringUtils.isEmpty(site.getId()) || extMobile != null) { - final String siteId = ObjectUtils.defaultIfNull(site.getId(), extSiteId); - requestBuilder.site(site.toBuilder().id(siteId).mobile(extMobile).build()); - } - } else if (app != null) { - if (StringUtils.isEmpty(app.getId())) { - requestBuilder.app(app.toBuilder().id(extSiteId).build()); } + modifiedImps.add(modifyImp(imp, impExt)); } - + requestBuilder.imp(modifiedImps); return requestBuilder.build(); } - private void validateSiteAppId(String extSiteId, Site site, App app) { - if (site != null && StringUtils.isEmpty(site.getId()) && StringUtils.isEmpty(extSiteId)) { - throw new PreBidException("Missing site id"); - } - - if (app != null && StringUtils.isEmpty(app.getId()) && StringUtils.isEmpty(extSiteId)) { - throw new PreBidException("Missing app id"); - } + private static Site updateSite(Site site, String siteId) { + return site.toBuilder().id(siteId).build(); } - private static void validateImp(Imp imp) { - if (imp.getBanner() == null && imp.getVideo() == null) { - throw new PreBidException(String.format("Invalid MediaType. Conversant supports only Banner and Video. " - + "Ignoring ImpID=%s", imp.getId())); - } + private static App updateApp(App app, String siteId) { + return app.toBuilder().id(siteId).build(); } - private ExtImpConversant parseImpExt(Imp imp) { + private ExtImpConversant parseImpExt(Imp imp, int impIndex) { + final ExtImpConversant extImp; try { - return mapper.mapper().convertValue(imp.getExt(), CONVERSANT_EXT_TYPE_REFERENCE).getBidder(); + extImp = mapper.mapper().convertValue(imp.getExt(), CONVERSANT_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); + throw new PreBidException(String.format("Impression[%d] missing ext.bidder object", impIndex)); + } + + if (StringUtils.isBlank(extImp.getSiteId())) { + throw new PreBidException(String.format("Impression[%d] requires ext.bidder.site_id", impIndex)); } + return extImp; } private static Imp modifyImp(Imp imp, ExtImpConversant impExt) { @@ -169,15 +137,10 @@ private static Imp modifyImp(Imp imp, ExtImpConversant impExt) { final Integer extPosition = impExt.getPosition(); final Video impVideo = imp.getVideo(); - final Imp.ImpBuilder impBuilder = imp.toBuilder(); - - if (impBanner != null && extPosition != null) { - impBuilder.banner(impBanner.toBuilder().pos(extPosition).build()); - } - - return impBuilder + return imp.toBuilder() .displaymanager(DISPLAY_MANAGER) .displaymanagerver(DISPLAY_MANAGER_VER) + .banner(modifyBanner(impBanner, extPosition)) .bidfloor(extBidfloor != null ? extBidfloor : imp.getBidfloor()) .tagid(extTagId != null ? extTagId : imp.getTagid()) .secure(shouldChangeSecure ? extSecure : imp.getSecure()) @@ -185,6 +148,15 @@ private static Imp modifyImp(Imp imp, ExtImpConversant impExt) { .build(); } + private static Banner modifyBanner(Banner impBanner, Integer extPosition) { + if (impBanner != null && extPosition != null) { + return impBanner.toBuilder() + .pos(AD_POSITIONS.contains(extPosition) ? extPosition : null) + .build(); + } + return impBanner; + } + private static Video modifyVideo(Video video, ExtImpConversant impExt) { final List extMimes = impExt.getMimes(); final Integer extMaxduration = impExt.getMaxduration(); @@ -224,24 +196,23 @@ private static List makeProtocols(List extProtocols, List> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { - return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) - ? Collections.emptyList() - : bidsFromResponse(bidRequest, bidResponse); + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + throw new PreBidException("Empty bid request"); + } + return bidsFromResponse(bidRequest, bidResponse); } private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { - return bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) + final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); + return firstSeatBid.getBid().stream() .filter(Objects::nonNull) - .flatMap(Collection::stream) .map(bid -> BidderBid.of(bid, getType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); } diff --git a/src/main/resources/static/bidder-params/conversant.json b/src/main/resources/static/bidder-params/conversant.json index 1154a4a998f..947a38b5be6 100644 --- a/src/main/resources/static/bidder-params/conversant.json +++ b/src/main/resources/static/bidder-params/conversant.json @@ -24,10 +24,6 @@ "type": "integer", "description": "Ad position on screen." }, - "mobile": { - "type": "integer", - "description": "Indicate if the site is mobile optimized." - }, "mimes": { "type": "array", "description": "Array of content MIME types. For videos only.", @@ -56,4 +52,4 @@ }, "required": [ ] -} \ No newline at end of file +} diff --git a/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java b/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java index 7e011b10a2d..ec8e9ddc873 100644 --- a/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java @@ -64,9 +64,6 @@ public void makeHttpRequestsShouldSkipInvalidImpAndAddErrorIfImpHasNoBannerOrVid final Result>> result = conversantBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput( - "Invalid MediaType. Conversant supports only Banner and Video. Ignoring ImpID=123")); assertThat(result.getValue()).hasSize(1); } @@ -81,8 +78,9 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { final Result>> result = conversantBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(2); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance"); + assertThat(result.getErrors()) + .hasSize(1) + .containsOnly(BidderError.badInput("Impression[0] missing ext.bidder object")); assertThat(result.getValue()).isEmpty(); } @@ -101,7 +99,7 @@ public void makeHttpRequestsShouldReturnErrorIfRequestHasSiteAndImpExtSiteIdIsNu // then assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("Missing site id")); + .containsOnly(BidderError.badInput("Impression[0] requires ext.bidder.site_id")); assertThat(result.getValue()).isEmpty(); } @@ -166,26 +164,6 @@ public void makeHttpRequestsShouldSetSiteIdFromExtSiteIdIfSiteIdIsNullOrEmpty() .containsOnly("site id"); } - @Test - public void makeHttpRequestsShouldSetSiteMobileFromImpExtIfPresent() { - // given - final BidRequest bidRequest = givenBidRequest( - requestBuilder -> requestBuilder.site(Site.builder().build()), - identity(), - extBuilder -> extBuilder.mobile(1)); - - // when - final Result>> result = conversantBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .extracting(BidRequest::getSite) - .extracting(Site::getMobile) - .containsOnly(1); - } - @Test public void makeHttpRequestsShouldSetImpDisplaymanagerAndDisplaymanagerver() { // given @@ -200,7 +178,7 @@ public void makeHttpRequestsShouldSetImpDisplaymanagerAndDisplaymanagerver() { .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .flatExtracting(BidRequest::getImp) .extracting(Imp::getDisplaymanager, Imp::getDisplaymanagerver) - .containsOnly(tuple("prebid-s2s", "1.0.1")); + .containsOnly(tuple("prebid-s2s", "2.0.0")); } @Test @@ -485,7 +463,9 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces final Result> result = conversantBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors()).isEmpty(); + assertThat(result.getErrors()) + .hasSize(1) + .containsOnly(BidderError.badServerResponse("Empty bid request")); assertThat(result.getValue()).isEmpty(); } @@ -499,7 +479,9 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso final Result> result = conversantBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors()).isEmpty(); + assertThat(result.getErrors()) + .hasSize(1) + .containsOnly(BidderError.badServerResponse("Empty bid request")); assertThat(result.getValue()).isEmpty(); } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-conversant-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-conversant-bid-request.json index a206472e3aa..b172612ec7d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-conversant-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-conversant-bid-request.json @@ -14,7 +14,7 @@ "h": 600 }, "displaymanager": "prebid-s2s", - "displaymanagerver": "1.0.1", + "displaymanagerver": "2.0.0", "ext": { "bidder": { "site_id": "siteId2" @@ -96,4 +96,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-conversant-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-conversant-bid-request.json index 2cf39df5f37..11a68f857ad 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-conversant-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-conversant-bid-request.json @@ -14,7 +14,7 @@ "h": 600 }, "displaymanager": "prebid-s2s", - "displaymanagerver": "1.0.1", + "displaymanagerver": "2.0.0", "ext": { "bidder": { "site_id": "siteId" @@ -31,7 +31,7 @@ "h": 600 }, "displaymanager": "prebid-s2s", - "displaymanagerver": "1.0.1", + "displaymanagerver": "2.0.0", "ext": { "bidder": { "site_id": "siteId" @@ -110,4 +110,4 @@ } } } -} \ No newline at end of file +} From dc27cf1101b4e7a7e11b3a975438a98163f11499 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Wed, 2 Dec 2020 15:21:56 +0200 Subject: [PATCH 021/129] Add http api for fetching accounts (#1015) * Add http api for fetching accounts * Replace ObjectNode with Account type in HttpAccountResponse model * Fixes after review --- .../settings/HttpApplicationSettings.java | 82 ++++++++++++++---- .../proto/response/HttpAccountsResponse.java | 14 +++ .../proto/response/HttpFetcherResponse.java | 4 +- .../settings/HttpApplicationSettingsTest.java | 85 ++++++++++++++++++- 4 files changed, 165 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/prebid/server/settings/proto/response/HttpAccountsResponse.java diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java index b89a181ede1..23d0d870b0b 100644 --- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java @@ -2,10 +2,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.json.DecodeException; @@ -14,6 +16,7 @@ import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredDataType; import org.prebid.server.settings.model.StoredResponseDataResult; +import org.prebid.server.settings.proto.response.HttpAccountsResponse; import org.prebid.server.settings.proto.response.HttpFetcherResponse; import org.prebid.server.util.HttpUtil; import org.prebid.server.vertx.http.HttpClient; @@ -59,7 +62,6 @@ */ public class HttpApplicationSettings implements ApplicationSettings { - private static final String NOT_SUPPORTED = "Not supported"; private static final Logger logger = LoggerFactory.getLogger(HttpApplicationSettings.class); private String endpoint; @@ -77,12 +79,62 @@ public HttpApplicationSettings(HttpClient httpClient, JacksonMapper mapper, Stri this.videoEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(videoEndpoint)); } - /** - * Not supported and returns failed result. - */ @Override public Future getAccountById(String accountId, Timeout timeout) { - return Future.failedFuture(new PreBidException("Not supported")); + + return fetchAccountsByIds(Collections.singleton(accountId), timeout) + .map(accounts -> accounts.stream() + .findFirst() + .orElseThrow(() -> + new PreBidException(String.format("Account with id : %s not found", accountId)))); + } + + private Future> fetchAccountsByIds(Set accountIds, Timeout timeout) { + if (CollectionUtils.isEmpty(accountIds)) { + return Future.succeededFuture(Collections.emptySet()); + } + final long remainingTimeout = timeout.remaining(); + if (timeout.remaining() <= 0) { + return Future.failedFuture(new TimeoutException("Timeout has been exceeded")); + } + + return httpClient.get(accountsRequestUrlFrom(endpoint, accountIds), HttpUtil.headers(), remainingTimeout) + .compose(response -> processAccountsResponse(response, accountIds)) + .recover(Future::failedFuture); + } + + private static String accountsRequestUrlFrom(String endpoint, Set accountIds) { + final StringBuilder url = new StringBuilder(endpoint); + url.append(endpoint.contains("?") ? "&" : "?"); + + if (!accountIds.isEmpty()) { + url.append("account-ids=[\"").append(joinIds(accountIds)).append("\"]"); + } + + return url.toString(); + } + + private Future> processAccountsResponse(HttpClientResponse response, Set accountIds) { + return Future.succeededFuture( + toAccountsResult(response.getStatusCode(), response.getBody(), accountIds)); + } + + private Set toAccountsResult(int statusCode, String body, Set accountIds) { + if (statusCode != HttpResponseStatus.OK.code()) { + throw new PreBidException(String.format("Error fetching accounts %s via http: " + + "unexpected response status %d", accountIds, statusCode)); + } + + final HttpAccountsResponse response; + try { + response = mapper.decodeValue(body, HttpAccountsResponse.class); + } catch (DecodeException e) { + throw new PreBidException(String.format("Error fetching accounts %s " + + "via http: failed to parse response: %s", accountIds, e.getMessage())); + } + final Map accounts = response.getAccounts(); + + return MapUtils.isNotEmpty(accounts) ? new HashSet<>(accounts.values()) : Collections.emptySet(); } /** @@ -139,15 +191,15 @@ private Future fetchStoredData(String endpoint, Set re final long remainingTimeout = timeout.remaining(); if (remainingTimeout <= 0) { - return failResponse(new TimeoutException("Timeout has been exceeded"), requestIds, impIds); + return failStoredDataResponse(new TimeoutException("Timeout has been exceeded"), requestIds, impIds); } - return httpClient.get(urlFrom(endpoint, requestIds, impIds), HttpUtil.headers(), remainingTimeout) - .compose(response -> processResponse(response, requestIds, impIds)) - .recover(exception -> failResponse(exception, requestIds, impIds)); + return httpClient.get(storeRequestUrlFrom(endpoint, requestIds, impIds), HttpUtil.headers(), remainingTimeout) + .compose(response -> processStoredDataResponse(response, requestIds, impIds)) + .recover(exception -> failStoredDataResponse(exception, requestIds, impIds)); } - private static String urlFrom(String endpoint, Set requestIds, Set impIds) { + private static String storeRequestUrlFrom(String endpoint, Set requestIds, Set impIds) { final StringBuilder url = new StringBuilder(endpoint); url.append(endpoint.contains("?") ? "&" : "?"); @@ -169,14 +221,14 @@ private static String joinIds(Set ids) { return String.join("\",\"", ids); } - private static Future failResponse(Throwable throwable, Set requestIds, - Set impIds) { + private static Future failStoredDataResponse(Throwable throwable, Set requestIds, + Set impIds) { return Future.succeededFuture( toFailedStoredDataResult(requestIds, impIds, throwable.getMessage())); } - private Future processResponse(HttpClientResponse response, Set requestIds, - Set impIds) { + private Future processStoredDataResponse(HttpClientResponse response, Set requestIds, + Set impIds) { return Future.succeededFuture( toStoredDataResult(requestIds, impIds, response.getStatusCode(), response.getBody())); } @@ -197,7 +249,7 @@ private static StoredDataResult toFailedStoredDataResult(Set requestIds, private StoredDataResult toStoredDataResult(Set requestIds, Set impIds, int statusCode, String body) { - if (statusCode != 200) { + if (statusCode != HttpResponseStatus.OK.code()) { return toFailedStoredDataResult(requestIds, impIds, "HTTP status code %d", statusCode); } diff --git a/src/main/java/org/prebid/server/settings/proto/response/HttpAccountsResponse.java b/src/main/java/org/prebid/server/settings/proto/response/HttpAccountsResponse.java new file mode 100644 index 00000000000..a893b2dc01a --- /dev/null +++ b/src/main/java/org/prebid/server/settings/proto/response/HttpAccountsResponse.java @@ -0,0 +1,14 @@ +package org.prebid.server.settings.proto.response; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.prebid.server.settings.model.Account; + +import java.util.Map; + +@AllArgsConstructor(staticName = "of") +@Value +public class HttpAccountsResponse { + + Map accounts; +} diff --git a/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java b/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java index 9608e9167d7..256e9d3273e 100644 --- a/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java +++ b/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java @@ -10,7 +10,7 @@ @Value public class HttpFetcherResponse { - private Map requests; + Map requests; - private Map imps; + Map imps; } diff --git a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java index dcea0d72709..2cf42d4bd65 100644 --- a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java @@ -16,6 +16,7 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; +import org.prebid.server.settings.proto.response.HttpAccountsResponse; import org.prebid.server.settings.proto.response.HttpFetcherResponse; import org.prebid.server.vertx.http.HttpClient; import org.prebid.server.vertx.http.model.HttpClientResponse; @@ -23,6 +24,7 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneId; +import java.util.Collections; import java.util.HashSet; import java.util.Map; @@ -94,13 +96,90 @@ public void creationShouldFailsOnInvalidVideoEndpoint() { } @Test - public void getAccountByIdShouldReturnEmptyResult() { + public void getAccountByIdShouldReturnFetchedAccount() throws JsonProcessingException { + // given + final Account account = Account.builder() + .id("someId") + .enforceCcpa(true) + .priceGranularity("testPriceGranularity") + .build(); + HttpAccountsResponse response = HttpAccountsResponse.of(Collections.singletonMap("someId", account)); + givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response)); + + // when + final Future future = httpApplicationSettings.getAccountById("someId", timeout); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result().getId()).isEqualTo("someId"); + assertThat(future.result().getEnforceCcpa()).isEqualTo(true); + assertThat(future.result().getPriceGranularity()).isEqualTo("testPriceGranularity"); + + verify(httpClient).get(eq("http://stored-requests?account-ids=[\"someId\"]"), any(), + anyLong()); + } + + @Test + public void getAccountByIdShouldReturnFaildedFutureIfResponseIsNotPresent() throws JsonProcessingException { + // given + HttpAccountsResponse response = HttpAccountsResponse.of(null); + givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response)); + // when - final Future future = httpApplicationSettings.getAccountById(null, null); + final Future future = httpApplicationSettings.getAccountById("notFoundId", timeout); // then assertThat(future.failed()).isTrue(); - assertThat(future.cause()).isInstanceOf(PreBidException.class).hasMessage("Not supported"); + assertThat(future.cause()) + .isInstanceOf(PreBidException.class) + .hasMessage("Account with id : notFoundId not found"); + } + + @Test + public void getAccountByIdShouldReturnErrorIdAccountNotFound() throws JsonProcessingException { + // given + HttpAccountsResponse response = HttpAccountsResponse.of(Collections.emptyMap()); + givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response)); + + // when + final Future future = httpApplicationSettings.getAccountById("notExistingId", timeout); + + // then + assertThat(future.failed()).isTrue(); + assertThat(future.cause()) + .isInstanceOf(PreBidException.class) + .hasMessage("Account with id : notExistingId not found"); + } + + @Test + public void getAccountByIdShouldReturnErrorIfResponseStatusIsDifferentFromOk() { + // given + givenHttpClientReturnsResponse(400, null); + + // when + final Future future = httpApplicationSettings.getAccountById("accountId", timeout); + + // then + assertThat(future.failed()).isTrue(); + assertThat(future.cause()) + .isInstanceOf(PreBidException.class) + .hasMessage("Error fetching accounts [accountId] via http: unexpected response status 400"); + } + + @Test + public void getAccountByIdShouldReturnErrorIfResponseHasInvalidStructure() { + // given + givenHttpClientReturnsResponse(200, "not valid response"); + + // when + final Future future = httpApplicationSettings.getAccountById("accountId", timeout); + + // then + assertThat(future.failed()).isTrue(); + assertThat(future.cause()) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Error fetching accounts [accountId] via http: " + + "failed to parse response: Failed to decode:"); } @Test From 59c90edff5559445e4255c7f673a917470786b74 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Wed, 2 Dec 2020 15:35:27 +0200 Subject: [PATCH 022/129] Add metrics to track account cache hits and misses (#1018) * Add metrics to track settings cache population if it is enabled * Add metrics to track account cache hits and misses --- docs/metrics.md | 3 + .../org/prebid/server/metric/MetricName.java | 11 ++- .../org/prebid/server/metric/Metrics.java | 20 ++++ .../server/metric/SettingsCacheMetrics.java | 58 +++++++++++ .../settings/CachingApplicationSettings.java | 77 ++++++++++++--- .../service/JdbcPeriodicRefreshService.java | 99 ++++++++++++------- .../spring/config/SettingsConfiguration.java | 46 +++++++-- .../org/prebid/server/metric/MetricsTest.java | 31 ++++++ .../CachingApplicationSettingsTest.java | 30 +++++- .../JdbcPeriodicRefreshServiceTest.java | 96 ++++++++++-------- 10 files changed, 372 insertions(+), 99 deletions(-) create mode 100644 src/main/java/org/prebid/server/metric/SettingsCacheMetrics.java diff --git a/docs/metrics.md b/docs/metrics.md index 185e143c427..13bd4b7ba53 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -64,6 +64,9 @@ where `[DATASOURCE]` is a data source name, `DEFAULT_DS` by defaul. - `timeout_notification.ok` - number of times bidders were successfully notified about timeouts - `timeout_notification.failed` - number of unsuccessful attempts to notify bidders about timeouts - `currency-rates.stale` - a flag indicating if currency rates obtained from external source are fresh (`0`) or stale (`1`) +- `settings.cache.(stored-request|amp-stored-request).refresh.(initialize|update).db_query_time` - timer tracking how long was settings cache population +- `settings.cache.(stored-request|amp-stored-request).refresh.(initialize|update).err` - number of errors during settings cache population +- `settings.cache.account.(hit|miss)` - number of times account was found or was missing in cache ## Auction per-adapter metrics - `adapter..no_cookie_requests` - number of requests made to `` that did not contain UID diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index 15fa3e82df6..49921d404ab 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -104,7 +104,16 @@ public enum MetricName { rejected, //currency rates - stale; + stale, + + // settings cache + stored_request("stored-request"), + amp_stored_request("amp-stored-request"), + account, + initialize, + update, + hit, + miss; private final String name; diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index cd6d659f380..021d745ec4e 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -32,6 +32,7 @@ public class Metrics extends UpdatableMetrics { private final Function adapterMetricsCreator; private final Function bidderCardinalityMetricsCreator; private final Function circuitBreakerMetricsCreator; + private final Function settingsCacheMetricsCreator; // not thread-safe maps are intentionally used here because it's harmless in this particular case - eventually // this all boils down to metrics lookup by underlying metric registry and that operation is guaranteed to be // thread-safe @@ -46,6 +47,7 @@ public class Metrics extends UpdatableMetrics { private final CacheMetrics cacheMetrics; private final TimeoutNotificationMetrics timeoutNotificationMetrics; private final CurrencyRatesMetrics currencyRatesMetrics; + private final Map settingsCacheMetrics; public Metrics(MetricRegistry metricRegistry, CounterType counterType, AccountMetricsVerbosity accountMetricsVerbosity, BidderCatalog bidderCatalog) { @@ -60,6 +62,7 @@ public Metrics(MetricRegistry metricRegistry, CounterType counterType, AccountMe bidderCardinalityMetricsCreator = cardinality -> new BidderCardinalityMetrics( metricRegistry, counterType, cardinality); circuitBreakerMetricsCreator = type -> new CircuitBreakerMetrics(metricRegistry, counterType, type); + settingsCacheMetricsCreator = type -> new SettingsCacheMetrics(metricRegistry, counterType, type); requestMetrics = new EnumMap<>(MetricName.class); accountMetrics = new HashMap<>(); adapterMetrics = new HashMap<>(); @@ -71,6 +74,7 @@ public Metrics(MetricRegistry metricRegistry, CounterType counterType, AccountMe cacheMetrics = new CacheMetrics(metricRegistry, counterType); timeoutNotificationMetrics = new TimeoutNotificationMetrics(metricRegistry, counterType); currencyRatesMetrics = new CurrencyRatesMetrics(metricRegistry, counterType); + settingsCacheMetrics = new HashMap<>(); } RequestStatusMetrics forRequestType(MetricName requestType) { @@ -113,6 +117,10 @@ CurrencyRatesMetrics currencyRates() { return currencyRatesMetrics; } + SettingsCacheMetrics forSettingsCacheType(MetricName type) { + return settingsCacheMetrics.computeIfAbsent(type, settingsCacheMetricsCreator); + } + public void updateSafariRequestsMetric(boolean isSafari) { if (isSafari) { incCounter(MetricName.safari_requests); @@ -464,6 +472,18 @@ public void createCurrencyRatesGauge(BooleanSupplier stateSupplier) { currencyRates().createGauge(MetricName.stale, () -> stateSupplier.getAsBoolean() ? 1 : 0); } + public void updateSettingsCacheRefreshTime(MetricName cacheType, MetricName refreshType, long timeElapsed) { + forSettingsCacheType(cacheType).forRefreshType(refreshType).updateTimer(MetricName.db_query_time, timeElapsed); + } + + public void updateSettingsCacheRefreshErrorMetric(MetricName cacheType, MetricName refreshType) { + forSettingsCacheType(cacheType).forRefreshType(refreshType).incCounter(MetricName.err); + } + + public void updateSettingsCacheEventMetric(MetricName cacheType, MetricName event) { + forSettingsCacheType(cacheType).incCounter(event); + } + private String resolveMetricsBidderName(String bidder) { return bidderCatalog.isValidName(bidder) ? bidder : METRICS_UNKNOWN_BIDDER; } diff --git a/src/main/java/org/prebid/server/metric/SettingsCacheMetrics.java b/src/main/java/org/prebid/server/metric/SettingsCacheMetrics.java new file mode 100644 index 00000000000..a78bf2223d0 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/SettingsCacheMetrics.java @@ -0,0 +1,58 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * Settings cache metrics support. + */ +class SettingsCacheMetrics extends UpdatableMetrics { + + private final Function refreshSettingsCacheMetricsCreator; + private final Map refreshSettingsCacheMetrics; + + SettingsCacheMetrics(MetricRegistry metricRegistry, CounterType counterType, MetricName type) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(type)))); + + refreshSettingsCacheMetricsCreator = refreshType -> + new RefreshSettingsCacheMetrics(metricRegistry, counterType, createPrefix(type), refreshType); + refreshSettingsCacheMetrics = new HashMap<>(); + } + + RefreshSettingsCacheMetrics forRefreshType(MetricName refreshType) { + return refreshSettingsCacheMetrics.computeIfAbsent(refreshType, refreshSettingsCacheMetricsCreator); + } + + private static String createPrefix(MetricName type) { + return String.format("settings.cache.%s", type.toString()); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + static class RefreshSettingsCacheMetrics extends UpdatableMetrics { + + RefreshSettingsCacheMetrics(MetricRegistry metricRegistry, + CounterType counterType, + String prefix, + MetricName type) { + + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix), Objects.requireNonNull(type)))); + } + + private static String createPrefix(String prefix, MetricName type) { + return String.format("%s.refresh.%s", prefix, type.toString()); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + } +} diff --git a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java index 229faa31c1c..a4817568ce5 100644 --- a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java @@ -6,6 +6,8 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; import org.prebid.server.settings.helper.StoredDataFetcher; import org.prebid.server.settings.helper.StoredItemResolver; import org.prebid.server.settings.model.Account; @@ -20,6 +22,7 @@ import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Consumer; /** * Adds caching functionality for {@link ApplicationSettings} implementation. @@ -36,9 +39,16 @@ public class CachingApplicationSettings implements ApplicationSettings { private final SettingsCache cache; private final SettingsCache ampCache; private final SettingsCache videoCache; + private final Metrics metrics; + + public CachingApplicationSettings(ApplicationSettings delegate, + SettingsCache cache, + SettingsCache ampCache, + SettingsCache videoCache, + Metrics metrics, + int ttl, + int size) { - public CachingApplicationSettings(ApplicationSettings delegate, SettingsCache cache, SettingsCache ampCache, - SettingsCache videoCache, int ttl, int size) { if (ttl <= 0 || size <= 0) { throw new IllegalArgumentException("ttl and size must be positive"); } @@ -49,6 +59,7 @@ public CachingApplicationSettings(ApplicationSettings delegate, SettingsCache ca this.cache = Objects.requireNonNull(cache); this.ampCache = Objects.requireNonNull(ampCache); this.videoCache = Objects.requireNonNull(videoCache); + this.metrics = Objects.requireNonNull(metrics); } /** @@ -56,7 +67,13 @@ public CachingApplicationSettings(ApplicationSettings delegate, SettingsCache ca */ @Override public Future getAccountById(String accountId, Timeout timeout) { - return getFromCacheOrDelegate(accountCache, accountToErrorCache, accountId, timeout, delegate::getAccountById); + return getFromCacheOrDelegate( + accountCache, + accountToErrorCache, + accountId, + timeout, + delegate::getAccountById, + event -> metrics.updateSettingsCacheEventMetric(MetricName.account, event)); } /** @@ -64,16 +81,24 @@ public Future getAccountById(String accountId, Timeout timeout) { */ @Override public Future getAdUnitConfigById(String adUnitConfigId, Timeout timeout) { - return getFromCacheOrDelegate(adUnitConfigCache, accountToErrorCache, adUnitConfigId, timeout, - delegate::getAdUnitConfigById); + return getFromCacheOrDelegate( + adUnitConfigCache, + accountToErrorCache, + adUnitConfigId, + timeout, + delegate::getAdUnitConfigById, + CachingApplicationSettings::noOp); } /** * Retrieves stored data from cache or delegates it to original fetcher. */ @Override - public Future getStoredData(String accountId, Set requestIds, Set impIds, + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { + return getFromCacheOrDelegate(cache, accountId, requestIds, impIds, timeout, delegate::getStoredData); } @@ -81,14 +106,20 @@ public Future getStoredData(String accountId, Set requ * Retrieves amp stored data from cache or delegates it to original fetcher. */ @Override - public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { + return getFromCacheOrDelegate(ampCache, accountId, requestIds, impIds, timeout, delegate::getAmpStoredData); } @Override - public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, Timeout timeout) { + return getFromCacheOrDelegate(videoCache, accountId, requestIds, impIds, timeout, delegate::getVideoStoredData); } @@ -100,15 +131,22 @@ public Future getStoredResponses(Set responseI return delegate.getStoredResponses(responseIds, timeout); } - private static Future getFromCacheOrDelegate(Map cache, Map accountToErrorCache, - String key, Timeout timeout, - BiFunction> retriever) { + private static Future getFromCacheOrDelegate(Map cache, + Map accountToErrorCache, + String key, + Timeout timeout, + BiFunction> retriever, + Consumer metricUpdater) { final T cachedValue = cache.get(key); if (cachedValue != null) { + metricUpdater.accept(MetricName.hit); + return Future.succeededFuture(cachedValue); } + metricUpdater.accept(MetricName.miss); + final String preBidExceptionMessage = accountToErrorCache.get(key); if (preBidExceptionMessage != null) { return Future.failedFuture(new PreBidException(preBidExceptionMessage)); @@ -129,7 +167,11 @@ private static Future getFromCacheOrDelegate(Map cache, Map getFromCacheOrDelegate( - SettingsCache cache, String accountId, Set requestIds, Set impIds, Timeout timeout, + SettingsCache cache, + String accountId, + Set requestIds, + Set impIds, + Timeout timeout, StoredDataFetcher, Set, Timeout, Future> retriever) { // empty string account ID doesn't make sense @@ -170,11 +212,14 @@ private static Future getFromCacheOrDelegate( }); } - private static Future cacheAndReturnFailedFuture(Throwable throwable, String key, + private static Future cacheAndReturnFailedFuture(Throwable throwable, + String key, Map cache) { + if (throwable instanceof PreBidException) { cache.put(key, throwable.getMessage()); } + return Future.failedFuture(throwable); } @@ -182,7 +227,9 @@ private static Map getFromCacheOrAddMissedIds(String accountId, Set ids, Map> cache, Set missedIds) { + final Map idToStoredItem = new HashMap<>(ids.size()); + for (String id : ids) { try { final StoredItem resolvedStoredItem = StoredItemResolver.resolve(null, accountId, id, cache.get(id)); @@ -191,6 +238,7 @@ private static Map getFromCacheOrAddMissedIds(String accountId, missedIds.add(id); } } + return idToStoredItem; } @@ -198,4 +246,7 @@ public void invalidateAccountCache(String accountId) { accountCache.remove(accountId); logger.debug("Account with id {0} was invalidated", accountId); } + + private static void noOp(ANY any) { + } } diff --git a/src/main/java/org/prebid/server/settings/service/JdbcPeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/JdbcPeriodicRefreshService.java index 3be91567235..035a03fa028 100644 --- a/src/main/java/org/prebid/server/settings/service/JdbcPeriodicRefreshService.java +++ b/src/main/java/org/prebid/server/settings/service/JdbcPeriodicRefreshService.java @@ -7,12 +7,15 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; import org.prebid.server.settings.CacheNotificationListener; import org.prebid.server.settings.helper.JdbcStoredDataResultMapper; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.vertx.Initializable; import org.prebid.server.vertx.jdbc.JdbcClient; +import java.time.Clock; import java.time.Instant; import java.util.Collections; import java.util.Date; @@ -42,11 +45,6 @@ public class JdbcPeriodicRefreshService implements Initializable { private static final Logger logger = LoggerFactory.getLogger(JdbcPeriodicRefreshService.class); - private final CacheNotificationListener cacheNotificationListener; - private final Vertx vertx; - private final JdbcClient jdbcClient; - private final long refreshPeriod; - /** * Example of initialize query: *
@@ -56,9 +54,8 @@ public class JdbcPeriodicRefreshService implements Initializable {
      * This query will be run once on startup to fetch _all_ known Stored Request data from the database.
      */
     private final String initQuery;
-
     /**
-     * Example of initialize query:
+     * Example of update query:
      * 
      * SELECT id, requestData, type
      * FROM stored_requests
@@ -68,21 +65,41 @@ public class JdbcPeriodicRefreshService implements Initializable {
      * Wildcard "?" would be used to pass last update date automatically.
      */
     private final String updateQuery;
-    private final TimeoutFactory timeoutFactory;
+    private final long refreshPeriod;
     private final long timeout;
+    private final MetricName cacheType;
+    private final CacheNotificationListener cacheNotificationListener;
+    private final Vertx vertx;
+    private final JdbcClient jdbcClient;
+    private final TimeoutFactory timeoutFactory;
+    private final Metrics metrics;
+    private final Clock clock;
+
     private Instant lastUpdate;
 
-    public JdbcPeriodicRefreshService(CacheNotificationListener cacheNotificationListener,
-                                      Vertx vertx, JdbcClient jdbcClient, long refreshPeriod, String initQuery,
-                                      String updateQuery, TimeoutFactory timeoutFactory, long timeout) {
+    public JdbcPeriodicRefreshService(String initQuery,
+                                      String updateQuery,
+                                      long refreshPeriod,
+                                      long timeout,
+                                      MetricName cacheType,
+                                      CacheNotificationListener cacheNotificationListener,
+                                      Vertx vertx,
+                                      JdbcClient jdbcClient,
+                                      TimeoutFactory timeoutFactory,
+                                      Metrics metrics,
+                                      Clock clock) {
+
+        this.initQuery = Objects.requireNonNull(StringUtils.stripToNull(initQuery));
+        this.updateQuery = Objects.requireNonNull(StringUtils.stripToNull(updateQuery));
+        this.refreshPeriod = refreshPeriod;
+        this.timeout = timeout;
+        this.cacheType = Objects.requireNonNull(cacheType);
         this.cacheNotificationListener = Objects.requireNonNull(cacheNotificationListener);
         this.vertx = Objects.requireNonNull(vertx);
         this.jdbcClient = Objects.requireNonNull(jdbcClient);
-        this.refreshPeriod = refreshPeriod;
-        this.initQuery = Objects.requireNonNull(StringUtils.stripToNull(initQuery));
-        this.updateQuery = Objects.requireNonNull(StringUtils.stripToNull(updateQuery));
         this.timeoutFactory = Objects.requireNonNull(timeoutFactory);
-        this.timeout = timeout;
+        this.metrics = Objects.requireNonNull(metrics);
+        this.clock = Objects.requireNonNull(clock);
     }
 
     @Override
@@ -94,36 +111,52 @@ public void initialize() {
     }
 
     private void getAll() {
-        jdbcClient.executeQuery(initQuery, Collections.emptyList(), JdbcStoredDataResultMapper::map, createTimeout())
-                .map(this::save)
-                .map(ignored -> setLastUpdate(Instant.now()))
-                .recover(JdbcPeriodicRefreshService::failResponse);
+        final long startTime = clock.millis();
+
+        jdbcClient.executeQuery(
+                initQuery,
+                Collections.emptyList(),
+                JdbcStoredDataResultMapper::map,
+                createTimeout())
+                .map(storedDataResult ->
+                        handleResult(storedDataResult, Instant.now(clock), startTime, MetricName.initialize))
+                .recover(exception -> handleFailure(exception, startTime, MetricName.initialize));
     }
 
-    private Void save(StoredDataResult storedDataResult) {
+    private Void handleResult(StoredDataResult storedDataResult,
+                              Instant updateTime,
+                              long startTime,
+                              MetricName refreshType) {
+
         cacheNotificationListener.save(storedDataResult.getStoredIdToRequest(), storedDataResult.getStoredIdToImp());
-        return null;
-    }
+        lastUpdate = updateTime;
+
+        metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime);
 
-    private Void setLastUpdate(Instant instant) {
-        lastUpdate = instant;
         return null;
     }
 
-    private static Future failResponse(Throwable exception) {
+    private Future handleFailure(Throwable exception, long startTime, MetricName refreshType) {
         logger.warn("Error occurred while request to jdbc refresh service", exception);
+
+        metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime);
+        metrics.updateSettingsCacheRefreshErrorMetric(cacheType, refreshType);
+
         return Future.failedFuture(exception);
     }
 
     private void refresh() {
-        final Instant updateTime = Instant.now();
-
-        jdbcClient.executeQuery(updateQuery, Collections.singletonList(Date.from(lastUpdate)),
-                JdbcStoredDataResultMapper::map, createTimeout())
-                .map(this::invalidate)
-                .map(this::save)
-                .map(ignored -> setLastUpdate(updateTime))
-                .recover(JdbcPeriodicRefreshService::failResponse);
+        final Instant updateTime = Instant.now(clock);
+        final long startTime = clock.millis();
+
+        jdbcClient.executeQuery(
+                updateQuery,
+                Collections.singletonList(Date.from(lastUpdate)),
+                JdbcStoredDataResultMapper::map,
+                createTimeout())
+                .map(storedDataResult ->
+                        handleResult(invalidate(storedDataResult), updateTime, startTime, MetricName.update))
+                .recover(exception -> handleFailure(exception, startTime, MetricName.update));
     }
 
     private StoredDataResult invalidate(StoredDataResult storedDataResult) {
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 d4c80580f46..7fa5b0fc547 100644
--- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java
@@ -10,6 +10,7 @@
 import org.apache.commons.lang3.ObjectUtils;
 import org.prebid.server.execution.TimeoutFactory;
 import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.metric.MetricName;
 import org.prebid.server.metric.Metrics;
 import org.prebid.server.settings.ApplicationSettings;
 import org.prebid.server.settings.CachingApplicationSettings;
@@ -229,7 +230,8 @@ public HttpPeriodicRefreshService ampHttpPeriodicRefreshService(
     }
 
     @Configuration
-    @ConditionalOnProperty(prefix = "settings.in-memory-cache.jdbc-update",
+    @ConditionalOnProperty(
+            prefix = "settings.in-memory-cache.jdbc-update",
             name = {"refresh-rate", "timeout", "init-query", "update-query", "amp-init-query", "amp-update-query"})
     static class JdbcPeriodicRefreshServiceConfiguration {
 
@@ -248,24 +250,50 @@ static class JdbcPeriodicRefreshServiceConfiguration {
         @Autowired
         TimeoutFactory timeoutFactory;
 
+        @Autowired
+        Metrics metrics;
+
+        @Autowired
+        Clock clock;
+
         @Bean
         public JdbcPeriodicRefreshService jdbcPeriodicRefreshService(
-                SettingsCache settingsCache,
+                @Qualifier("settingsCache") SettingsCache settingsCache,
                 @Value("${settings.in-memory-cache.jdbc-update.init-query}") String initQuery,
                 @Value("${settings.in-memory-cache.jdbc-update.update-query}") String updateQuery) {
 
-            return new JdbcPeriodicRefreshService(settingsCache, vertx, jdbcClient, refreshPeriod,
-                    initQuery, updateQuery, timeoutFactory, timeout);
+            return new JdbcPeriodicRefreshService(
+                    initQuery,
+                    updateQuery,
+                    refreshPeriod,
+                    timeout,
+                    MetricName.stored_request,
+                    settingsCache,
+                    vertx,
+                    jdbcClient,
+                    timeoutFactory,
+                    metrics,
+                    clock);
         }
 
         @Bean
         public JdbcPeriodicRefreshService ampJdbcPeriodicRefreshService(
-                SettingsCache settingsCache,
+                @Qualifier("ampSettingsCache") SettingsCache ampSettingsCache,
                 @Value("${settings.in-memory-cache.jdbc-update.amp-init-query}") String ampInitQuery,
                 @Value("${settings.in-memory-cache.jdbc-update.amp-update-query}") String ampUpdateQuery) {
 
-            return new JdbcPeriodicRefreshService(settingsCache, vertx, jdbcClient, refreshPeriod,
-                    ampInitQuery, ampUpdateQuery, timeoutFactory, timeout);
+            return new JdbcPeriodicRefreshService(
+                    ampInitQuery,
+                    ampUpdateQuery,
+                    refreshPeriod,
+                    timeout,
+                    MetricName.amp_stored_request,
+                    ampSettingsCache,
+                    vertx,
+                    jdbcClient,
+                    timeoutFactory,
+                    metrics,
+                    clock);
         }
     }
 
@@ -302,13 +330,15 @@ CachingApplicationSettings cachingApplicationSettings(
                 ApplicationSettingsCacheProperties cacheProperties,
                 @Qualifier("settingsCache") SettingsCache cache,
                 @Qualifier("ampSettingsCache") SettingsCache ampCache,
-                @Qualifier("videoSettingCache") SettingsCache videoCache) {
+                @Qualifier("videoSettingCache") SettingsCache videoCache,
+                Metrics metrics) {
 
             return new CachingApplicationSettings(
                     compositeApplicationSettings,
                     cache,
                     ampCache,
                     videoCache,
+                    metrics,
                     cacheProperties.getTtlSeconds(),
                     cacheProperties.getCacheSize());
         }
diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java
index a59d825ec65..d34a4b9d98e 100644
--- a/src/test/java/org/prebid/server/metric/MetricsTest.java
+++ b/src/test/java/org/prebid/server/metric/MetricsTest.java
@@ -996,6 +996,37 @@ public void shouldCreateCurrencyRatesGaugeMetric() {
         assertThat(metricRegistry.gauge("currency-rates.stale.count", () -> null).getValue()).isEqualTo(1L);
     }
 
+    @Test
+    public void updateSettingsCacheRefreshTimeShouldUpdateTimer() {
+        // when
+        metrics.updateSettingsCacheRefreshTime(MetricName.stored_request, MetricName.initialize, 123L);
+
+        // then
+        assertThat(metricRegistry
+                .timer("settings.cache.stored-request.refresh.initialize.db_query_time")
+                .getCount())
+                .isEqualTo(1);
+    }
+
+    @Test
+    public void updateSettingsCacheRefreshErrorMetricShouldIncrementMetric() {
+        // when
+        metrics.updateSettingsCacheRefreshErrorMetric(MetricName.stored_request, MetricName.initialize);
+
+        // then
+        assertThat(metricRegistry.counter("settings.cache.stored-request.refresh.initialize.err").getCount())
+                .isEqualTo(1);
+    }
+
+    @Test
+    public void updateSettingsCacheEventMetricShouldIncrementMetric() {
+        // when
+        metrics.updateSettingsCacheEventMetric(MetricName.account, MetricName.hit);
+
+        // then
+        assertThat(metricRegistry.counter("settings.cache.account.hit").getCount()).isEqualTo(1);
+    }
+
     private void verifyCreatesConfiguredCounterType(Consumer metricsConsumer) {
         final EnumMap> counterTypeClasses = new EnumMap<>(CounterType.class);
         counterTypeClasses.put(CounterType.counter, Counter.class);
diff --git a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java
index bc10de0852d..f57089c51be 100644
--- a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java
+++ b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java
@@ -11,6 +11,8 @@
 import org.prebid.server.exception.PreBidException;
 import org.prebid.server.execution.Timeout;
 import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.metric.MetricName;
+import org.prebid.server.metric.Metrics;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.StoredDataResult;
 import org.prebid.server.settings.model.StoredResponseDataResult;
@@ -43,6 +45,8 @@ public class CachingApplicationSettingsTest {
 
     @Mock
     private ApplicationSettings applicationSettings;
+    @Mock
+    private Metrics metrics;
 
     private CachingApplicationSettings cachingApplicationSettings;
 
@@ -52,8 +56,14 @@ public class CachingApplicationSettingsTest {
     public void setUp() {
         timeout = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault())).create(500L);
 
-        cachingApplicationSettings = new CachingApplicationSettings(applicationSettings, new SettingsCache(360, 100),
-                new SettingsCache(360, 100), new SettingsCache(360, 100), 360, 100);
+        cachingApplicationSettings = new CachingApplicationSettings(
+                applicationSettings,
+                new SettingsCache(360, 100),
+                new SettingsCache(360, 100),
+                new SettingsCache(360, 100),
+                metrics,
+                360,
+                100);
     }
 
     @Test
@@ -132,6 +142,22 @@ public void getAccountByIdShouldNotCacheNotPreBidException() {
                 .hasMessage("error");
     }
 
+    @Test
+    public void getAccountByIdShouldUpdateMetrics() {
+        // given
+        final Account account = Account.builder().id("accountId").priceGranularity("med").build();
+        given(applicationSettings.getAccountById(eq("accountId"), same(timeout)))
+                .willReturn(Future.succeededFuture(account));
+
+        // when
+        cachingApplicationSettings.getAccountById("accountId", timeout);
+        cachingApplicationSettings.getAccountById("accountId", timeout);
+
+        // then
+        verify(metrics).updateSettingsCacheEventMetric(eq(MetricName.account), eq(MetricName.miss));
+        verify(metrics).updateSettingsCacheEventMetric(eq(MetricName.account), eq(MetricName.hit));
+    }
+
     @Test
     public void getAdUnitConfigByIdShouldReturnResultFromCacheOnSuccessiveCalls() {
         // given
diff --git a/src/test/java/org/prebid/server/settings/service/JdbcPeriodicRefreshServiceTest.java b/src/test/java/org/prebid/server/settings/service/JdbcPeriodicRefreshServiceTest.java
index 2110ecd80a1..5e49e305238 100644
--- a/src/test/java/org/prebid/server/settings/service/JdbcPeriodicRefreshServiceTest.java
+++ b/src/test/java/org/prebid/server/settings/service/JdbcPeriodicRefreshServiceTest.java
@@ -11,6 +11,8 @@
 import org.mockito.junit.MockitoRule;
 import org.mockito.stubbing.Answer;
 import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.metric.MetricName;
+import org.prebid.server.metric.Metrics;
 import org.prebid.server.settings.CacheNotificationListener;
 import org.prebid.server.settings.model.StoredDataResult;
 import org.prebid.server.vertx.jdbc.JdbcClient;
@@ -24,7 +26,6 @@
 import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonList;
 import static java.util.Collections.singletonMap;
-import static org.assertj.core.api.Assertions.assertThatNullPointerException;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -37,21 +38,22 @@
 
 public class JdbcPeriodicRefreshServiceTest {
 
-    private static TimeoutFactory timeoutFactory = new TimeoutFactory(
-            Clock.fixed(Instant.now(), ZoneId.systemDefault()));
-
     @Rule
     public final MockitoRule mockitoRule = MockitoJUnit.rule();
 
     @Mock
     private CacheNotificationListener cacheNotificationListener;
     @Mock
+    private Vertx vertx;
+    @Mock
     private JdbcClient jdbcClient;
+    private final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
+    private final TimeoutFactory timeoutFactory = new TimeoutFactory(clock);
     @Mock
-    private Vertx vertx;
+    private Metrics metrics;
 
-    private Map expectedRequests = singletonMap("id1", "value1");
-    private Map expectedImps = singletonMap("id2", "value2");
+    private final Map expectedRequests = singletonMap("id1", "value1");
+    private final Map expectedImps = singletonMap("id2", "value2");
 
     @Before
     public void setUp() {
@@ -66,31 +68,10 @@ public void setUp() {
                 .willReturn(Future.succeededFuture(updateResult));
     }
 
-    @Test
-    public void creationShouldFailOnNullArgumentsAndBlankQuery() {
-        assertThatNullPointerException().isThrownBy(() -> createAndInitService(
-                null, null, null, 0, null, null, null, 0));
-        assertThatNullPointerException().isThrownBy(() -> createAndInitService(
-                cacheNotificationListener, null, null, 0, null, null, null, 0));
-        assertThatNullPointerException().isThrownBy(() -> createAndInitService(
-                cacheNotificationListener, vertx, null, 0, null, null, null, 0));
-        assertThatNullPointerException().isThrownBy(() -> createAndInitService(
-                cacheNotificationListener, vertx, jdbcClient, 0, null, null, null, 0));
-        assertThatNullPointerException().isThrownBy(() -> createAndInitService(
-                cacheNotificationListener, vertx, jdbcClient, 0, "init_query", null, null, 0));
-        assertThatNullPointerException().isThrownBy(() -> createAndInitService(
-                cacheNotificationListener, vertx, jdbcClient, 0, "init_query", "update_query", null, 0));
-        assertThatNullPointerException().isThrownBy(() -> createAndInitService(
-                cacheNotificationListener, vertx, jdbcClient, 0, "  ", null, timeoutFactory, 0));
-        assertThatNullPointerException().isThrownBy(() -> createAndInitService(
-                cacheNotificationListener, vertx, jdbcClient, 0, "init_query", " ", timeoutFactory, 0));
-    }
-
     @Test
     public void shouldCallSaveWithExpectedParameters() {
         // when
-        createAndInitService(cacheNotificationListener, vertx, jdbcClient, 1000,
-                "init_query", "update_query", timeoutFactory, 2000);
+        createAndInitService(1000);
 
         // then
         verify(cacheNotificationListener).save(expectedRequests, expectedImps);
@@ -103,8 +84,7 @@ public void shouldCallInvalidateAndSaveWithExpectedParameters() {
                 .willAnswer(withSelfAndPassObjectToHandler(1L));
 
         // when
-        createAndInitService(cacheNotificationListener, vertx, jdbcClient, 1000,
-                "init_query", "update_query", timeoutFactory, 2000);
+        createAndInitService(1000);
 
         // then
         verify(cacheNotificationListener).save(expectedRequests, expectedImps);
@@ -119,8 +99,7 @@ public void initializeShouldMakeOneInitialRequestAndTwoScheduledRequestsWithPara
                 .willAnswer(withSelfAndPassObjectToHandler(1L, 2L));
 
         // when
-        createAndInitService(cacheNotificationListener, vertx, jdbcClient, 1000,
-                "init_query", "update_query", timeoutFactory, 2000);
+        createAndInitService(1000);
 
         // then
         verify(jdbcClient).executeQuery(eq("init_query"), eq(emptyList()), any(), any());
@@ -130,21 +109,54 @@ public void initializeShouldMakeOneInitialRequestAndTwoScheduledRequestsWithPara
     @Test
     public void initializeShouldMakeOnlyOneInitialRequestIfRefreshPeriodIsNegative() {
         // when
-        createAndInitService(cacheNotificationListener, vertx, jdbcClient, -1,
-                "init_query", "update_query", timeoutFactory, 2000);
+        createAndInitService(-1);
 
         // then
         verify(vertx, never()).setPeriodic(anyLong(), any());
         verify(jdbcClient).executeQuery(anyString(), anyList(), any(), any());
     }
 
-    private static void createAndInitService(CacheNotificationListener cacheNotificationListener,
-                                             Vertx vertx, JdbcClient jdbcClient, long refresh,
-                                             String query, String updateQuery,
-                                             TimeoutFactory timeoutFactory, long timeout) {
-        final JdbcPeriodicRefreshService jdbcPeriodicRefreshService =
-                new JdbcPeriodicRefreshService(cacheNotificationListener, vertx, jdbcClient, refresh,
-                        query, updateQuery, timeoutFactory, timeout);
+    @Test
+    public void shouldUpdateTimerMetric() {
+        // when
+        createAndInitService(1000);
+
+        // then
+        verify(metrics).updateSettingsCacheRefreshTime(
+                eq(MetricName.stored_request), eq(MetricName.initialize), anyLong());
+    }
+
+    @Test
+    public void shouldUpdateTimerAndErrorMetric() {
+        // given
+        given(jdbcClient.executeQuery(eq("init_query"), anyList(), any(), any()))
+                .willReturn(Future.failedFuture("Query error"));
+
+        // when
+        createAndInitService(1000);
+
+        // then
+        verify(metrics).updateSettingsCacheRefreshTime(
+                eq(MetricName.stored_request), eq(MetricName.initialize), anyLong());
+        verify(metrics).updateSettingsCacheRefreshErrorMetric(
+                eq(MetricName.stored_request), eq(MetricName.initialize));
+    }
+
+    private void createAndInitService(long refresh) {
+
+        final JdbcPeriodicRefreshService jdbcPeriodicRefreshService = new JdbcPeriodicRefreshService(
+                "init_query",
+                "update_query",
+                refresh,
+                2000,
+                MetricName.stored_request,
+                cacheNotificationListener,
+                vertx,
+                jdbcClient,
+                timeoutFactory,
+                metrics,
+                clock);
+
         jdbcPeriodicRefreshService.initialize();
     }
 

From f72103169d34fa0d5cc1cf6ba8b21a02c5536baa Mon Sep 17 00:00:00 2001
From: Dmitriy 
Date: Wed, 2 Dec 2020 16:10:07 +0200
Subject: [PATCH 023/129] Add usage of GDPR and consent string for video
 (#1019)

---
 .../prebid/server/it/AppnexusVideoTest.java   |  3 +-
 .../test-video-appnexus-bid-request-1.json    | 33 +++++++++++++----
 .../test-video-appnexus-bid-request-2.json    | 35 +++++++++++++++----
 .../video/test-video-appnexus-request.json    | 12 +++----
 .../test-video-appnexus-response-empty.json   |  3 --
 5 files changed, 60 insertions(+), 26 deletions(-)
 delete mode 100644 src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-response-empty.json

diff --git a/src/test/java/org/prebid/server/it/AppnexusVideoTest.java b/src/test/java/org/prebid/server/it/AppnexusVideoTest.java
index 9a23e70aee1..3de4bd25d97 100644
--- a/src/test/java/org/prebid/server/it/AppnexusVideoTest.java
+++ b/src/test/java/org/prebid/server/it/AppnexusVideoTest.java
@@ -55,8 +55,7 @@ public void openrtb2VideoShouldRespondWithBidsFromAppnexus() throws IOException,
                 .post("/openrtb2/video");
 
         // then
-        // TODO remove "empty" when VideoRequest will proceed consentValue.
-        final String expectedAuctionResponse = jsonFrom("openrtb2/video/test-video-appnexus-response-empty.json");
+        final String expectedAuctionResponse = jsonFrom("openrtb2/video/test-video-appnexus-response.json");
 
         JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE);
     }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json
index af712452338..e920fd3d553 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json
@@ -248,17 +248,29 @@
     }
   },
   "device": {
-    "ua": "userAgent",
-    "ip": "193.168.244.0"
+    "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
+    "dnt": 33,
+    "lmt": 44,
+    "ip": "123.145.167.10",
+    "devicetype": 1,
+    "os": "mac os",
+    "h": 480,
+    "w": 640,
+    "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
+    "didsha1": "didsha1",
+    "didmd5": "didmd5",
+    "dpidsha1": "dpidsha1",
+    "dpidmd5": "dpidmd5",
+    "macsha1": "macsha1",
+    "macmd5": "macmd5"
   },
   "user": {
+    "buyeruid": "appnexus",
     "yob": 1991,
     "gender": "F",
-    "keywords": "Hotels, Travelling"
-  },
-  "regs": {
+    "keywords": "Hotels, Travelling",
     "ext": {
-      "us_privacy": "1YNN"
+      "consent": "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA"
     }
   },
   "at": 1,
@@ -266,6 +278,12 @@
   "cur": [
     "USD"
   ],
+  "regs": {
+    "ext": {
+      "gdpr": 1,
+      "us_privacy": "1YNN"
+    }
+  },
   "ext": {
     "prebid": {
       "targeting": {
@@ -283,6 +301,9 @@
       },
       "cache": {
         "vastxml": {}
+      },
+      "channel": {
+        "name": "web"
       }
     }
   }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json
index 991c4ddf098..bbe5da789b4 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json
@@ -41,17 +41,29 @@
     }
   },
   "device": {
-    "ua": "userAgent",
-    "ip": "193.168.244.0"
+    "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
+    "dnt": 33,
+    "lmt": 44,
+    "ip": "123.145.167.10",
+    "devicetype": 1,
+    "os": "mac os",
+    "h": 480,
+    "w": 640,
+    "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
+    "didsha1": "didsha1",
+    "didmd5": "didmd5",
+    "dpidsha1": "dpidsha1",
+    "dpidmd5": "dpidmd5",
+    "macsha1": "macsha1",
+    "macmd5": "macmd5"
   },
   "user": {
+    "buyeruid": "appnexus",
     "yob": 1991,
     "gender": "F",
-    "keywords": "Hotels, Travelling"
-  },
-  "regs": {
+    "keywords": "Hotels, Travelling",
     "ext": {
-      "us_privacy": "1YNN"
+      "consent": "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA"
     }
   },
   "at": 1,
@@ -59,6 +71,12 @@
   "cur": [
     "USD"
   ],
+  "regs": {
+    "ext": {
+      "gdpr": 1,
+      "us_privacy": "1YNN"
+    }
+  },
   "ext": {
     "prebid": {
       "targeting": {
@@ -76,7 +94,10 @@
       },
       "cache": {
         "vastxml": {}
+      },
+      "channel": {
+        "name": "web"
       }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json
index 8c7abb10755..8de091e7bcb 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json
@@ -23,13 +23,8 @@
     "page": "prebid.com"
   },
   "user": {
-    "buyeruids": {
-      "appnexus": "unique_id_an",
-      "rubicon": "unique_id_rubi"
-    },
-    "gdpr": {
-      "consentrequired": false,
-      "consentstring": "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA"
+    "ext": {
+      "consent": "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA"
     },
     "yob": 1991,
     "gender": "F",
@@ -37,11 +32,12 @@
   },
   "regs": {
     "ext": {
+      "gdpr": 1,
       "us_privacy": "1YNN"
     }
   },
   "tmax": 5000,
-  "device11": {
+  "device": {
     "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
     "ip": "123.145.167.10",
     "devicetype": 1,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-response-empty.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-response-empty.json
deleted file mode 100644
index b773ce30155..00000000000
--- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-response-empty.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-  "adPods": []
-}

From dcdca99c58fea242289229b85684a0e468bdc596 Mon Sep 17 00:00:00 2001
From: Sergii Chernysh 
Date: Wed, 2 Dec 2020 16:14:44 +0200
Subject: [PATCH 024/129] Get rid of default boolean configuration value in
 code (#1029)

---
 .../auction/VideoStoredRequestProcessor.java  | 39 +++++++++++--------
 .../spring/config/ServiceConfiguration.java   | 37 ++++++++++++------
 src/main/resources/application.yaml           |  1 +
 .../VideoStoredRequestProcessorTest.java      | 14 ++++++-
 4 files changed, 62 insertions(+), 29 deletions(-)

diff --git a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java
index 0158f6096fa..0b87f2a49d3 100644
--- a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java
+++ b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java
@@ -58,34 +58,41 @@ public class VideoStoredRequestProcessor {
     private static final String DEFAULT_CURRENCY = "USD";
     private static final String DEFAULT_BUYERUID = "appnexus";
 
-    private final ApplicationSettings applicationSettings;
-    private final VideoRequestValidator validator;
     private final boolean enforceStoredRequest;
     private final List blacklistedAccounts;
+    private final long defaultTimeout;
+    private final String currency;
     private final BidRequest defaultBidRequest;
+    private final VideoRequestValidator validator;
+    private final ApplicationSettings applicationSettings;
     private final Metrics metrics;
-    private final TimeoutResolver timeoutResolver;
     private final TimeoutFactory timeoutFactory;
-    private final long defaultTimeout;
-    private final String currency;
+    private final TimeoutResolver timeoutResolver;
     private final JacksonMapper mapper;
-    private JsonMergeUtil jsonMergeUtil;
-
-    public VideoStoredRequestProcessor(ApplicationSettings applicationSettings, VideoRequestValidator validator,
-                                       boolean enforceStoredRequest, List blacklistedAccounts,
-                                       BidRequest defaultBidRequest, Metrics metrics, TimeoutFactory timeoutFactory,
-                                       TimeoutResolver timeoutResolver, long defaultTimeout, String adServerCurrency,
+    private final JsonMergeUtil jsonMergeUtil;
+
+    public VideoStoredRequestProcessor(boolean enforceStoredRequest,
+                                       List blacklistedAccounts,
+                                       long defaultTimeout,
+                                       String adServerCurrency,
+                                       BidRequest defaultBidRequest,
+                                       VideoRequestValidator validator,
+                                       ApplicationSettings applicationSettings,
+                                       Metrics metrics,
+                                       TimeoutFactory timeoutFactory,
+                                       TimeoutResolver timeoutResolver,
                                        JacksonMapper mapper) {
-        this.applicationSettings = Objects.requireNonNull(applicationSettings);
-        this.validator = Objects.requireNonNull(validator);
+
         this.enforceStoredRequest = enforceStoredRequest;
         this.blacklistedAccounts = blacklistedAccounts;
+        this.defaultTimeout = defaultTimeout;
+        this.currency = StringUtils.isBlank(adServerCurrency) ? DEFAULT_CURRENCY : adServerCurrency;
         this.defaultBidRequest = Objects.requireNonNull(defaultBidRequest);
+        this.validator = Objects.requireNonNull(validator);
+        this.applicationSettings = Objects.requireNonNull(applicationSettings);
+        this.metrics = Objects.requireNonNull(metrics);
         this.timeoutFactory = Objects.requireNonNull(timeoutFactory);
         this.timeoutResolver = Objects.requireNonNull(timeoutResolver);
-        this.metrics = Objects.requireNonNull(metrics);
-        this.defaultTimeout = defaultTimeout;
-        this.currency = StringUtils.isBlank(adServerCurrency) ? DEFAULT_CURRENCY : adServerCurrency;
         this.mapper = Objects.requireNonNull(mapper);
 
         jsonMergeUtil = new JsonMergeUtil(mapper);
diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java
index 84edca874ea..8a39a5f498c 100644
--- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java
@@ -257,13 +257,19 @@ AmpRequestFactory ampRequestFactory(StoredRequestProcessor storedRequestProcesso
     @Bean
     VideoRequestFactory videoRequestFactory(
             @Value("${auction.max-request-size}") int maxRequestSize,
-            @Value("${auction.video.stored-required:#{false}}") boolean enforceStoredRequest,
+            @Value("${video.stored-request-required}") boolean enforceStoredRequest,
             VideoStoredRequestProcessor storedRequestProcessor,
             AuctionRequestFactory auctionRequestFactory,
-            TimeoutResolver timeoutResolver, JacksonMapper mapper) {
+            TimeoutResolver timeoutResolver,
+            JacksonMapper mapper) {
 
-        return new VideoRequestFactory(maxRequestSize, enforceStoredRequest, storedRequestProcessor,
-                auctionRequestFactory, timeoutResolver, mapper);
+        return new VideoRequestFactory(
+                maxRequestSize,
+                enforceStoredRequest,
+                storedRequestProcessor,
+                auctionRequestFactory,
+                timeoutResolver,
+                mapper);
     }
 
     @Bean
@@ -273,22 +279,31 @@ VideoResponseFactory videoResponseFactory(JacksonMapper mapper) {
 
     @Bean
     VideoStoredRequestProcessor videoStoredRequestProcessor(
-            ApplicationSettings applicationSettings,
-            @Value("${auction.video.stored-required:#{false}}") boolean enforceStoredRequest,
+            @Value("${video.stored-request-required}") boolean enforceStoredRequest,
             @Value("${auction.blacklisted-accounts}") String blacklistedAccountsString,
+            @Value("${video.stored-requests-timeout-ms}") long defaultTimeoutMs,
+            @Value("${auction.ad-server-currency:#{null}}") String adServerCurrency,
             BidRequest defaultVideoBidRequest,
+            ApplicationSettings applicationSettings,
             Metrics metrics,
             TimeoutFactory timeoutFactory,
             TimeoutResolver timeoutResolver,
-            @Value("${video.stored-requests-timeout-ms}") long defaultTimeoutMs,
-            @Value("${auction.ad-server-currency:#{null}}") String adServerCurrency,
             JacksonMapper mapper) {
 
         final List blacklistedAccounts = splitCommaSeparatedString(blacklistedAccountsString);
 
-        return new VideoStoredRequestProcessor(applicationSettings, new VideoRequestValidator(), enforceStoredRequest,
-                blacklistedAccounts, defaultVideoBidRequest, metrics, timeoutFactory, timeoutResolver, defaultTimeoutMs,
-                adServerCurrency, mapper);
+        return new VideoStoredRequestProcessor(
+                enforceStoredRequest,
+                blacklistedAccounts,
+                defaultTimeoutMs,
+                adServerCurrency,
+                defaultVideoBidRequest,
+                new VideoRequestValidator(),
+                applicationSettings,
+                metrics,
+                timeoutFactory,
+                timeoutResolver,
+                mapper);
     }
 
     @Bean
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 984c5955e9a..1cbc0ec041b 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -88,6 +88,7 @@ auction:
     expected-request-time-ms: 10
     only-winning-bids: false
 video:
+  stored-request-required: false
   stored-requests-timeout-ms: 90
 amp:
   default-timeout-ms: 900
diff --git a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java
index 82eaa36795a..c1eb0f71069 100644
--- a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java
+++ b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java
@@ -73,8 +73,18 @@ public class VideoStoredRequestProcessorTest extends VertxTest {
 
     @Before
     public void setUp() {
-        target = new VideoStoredRequestProcessor(applicationSettings, validator, false, emptyList(),
-                BidRequest.builder().build(), metrics, timeoutFactory, timeoutResolver, 2000L, "USD", jacksonMapper);
+        target = new VideoStoredRequestProcessor(
+                false,
+                emptyList(),
+                2000L,
+                "USD",
+                BidRequest.builder().build(),
+                validator,
+                applicationSettings,
+                metrics,
+                timeoutFactory,
+                timeoutResolver,
+                jacksonMapper);
     }
 
     @Test

From 02631d5570e60768621991839034e77c2dcca219 Mon Sep 17 00:00:00 2001
From: Sergii Chernysh 
Date: Wed, 2 Dec 2020 16:19:34 +0200
Subject: [PATCH 025/129] Plug in JVM metrics submission via metrics engine
 (#1031)

---
 pom.xml                                              |  5 +++++
 .../server/spring/config/MetricsConfiguration.java   | 12 +++++++++++-
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 23c05b24237..50b7b23f548 100644
--- a/pom.xml
+++ b/pom.xml
@@ -235,6 +235,11 @@
             metrics-core
             ${metrics.version}
         
+        
+            io.dropwizard.metrics
+            metrics-jvm
+            ${metrics.version}
+        
         
             io.dropwizard.metrics
             metrics-graphite
diff --git a/src/main/java/org/prebid/server/spring/config/MetricsConfiguration.java b/src/main/java/org/prebid/server/spring/config/MetricsConfiguration.java
index b6ad9702fe4..9e9f1b1db8d 100644
--- a/src/main/java/org/prebid/server/spring/config/MetricsConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/MetricsConfiguration.java
@@ -6,6 +6,8 @@
 import com.codahale.metrics.SharedMetricRegistries;
 import com.codahale.metrics.graphite.Graphite;
 import com.codahale.metrics.graphite.GraphiteReporter;
+import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
+import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
 import com.izettle.metrics.influxdb.InfluxDbHttpSender;
 import com.izettle.metrics.influxdb.InfluxDbReporter;
 import com.izettle.metrics.influxdb.InfluxDbSender;
@@ -113,7 +115,15 @@ Metrics metrics(@Value("${metrics.metricType}") CounterType counterType, MetricR
 
     @Bean
     MetricRegistry metricRegistry() {
-        return SharedMetricRegistries.getOrCreate(METRIC_REGISTRY_NAME);
+        final boolean alreadyExists = SharedMetricRegistries.names().contains(METRIC_REGISTRY_NAME);
+        final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(METRIC_REGISTRY_NAME);
+
+        if (!alreadyExists) {
+            metricRegistry.register("jvm.gc", new GarbageCollectorMetricSet());
+            metricRegistry.register("jvm.memory", new MemoryUsageGaugeSet());
+        }
+
+        return metricRegistry;
     }
 
     @Bean

From 8e3d686f46036ab83f6b3cd886faff9a3905568f Mon Sep 17 00:00:00 2001
From: Serhii Nahornyi 
Date: Wed, 2 Dec 2020 16:20:59 +0200
Subject: [PATCH 026/129] Review vendor-id property (#1035)

---
 src/main/resources/bidder-config/adpone.yaml          | 2 +-
 src/main/resources/bidder-config/adprime.yaml         | 2 +-
 src/main/resources/bidder-config/adtarget.yaml        | 4 ++--
 src/main/resources/bidder-config/adtelligent.yaml     | 2 +-
 src/main/resources/bidder-config/datablocks.yaml      | 4 ++--
 src/main/resources/bidder-config/kubient.yaml         | 2 +-
 src/main/resources/bidder-config/logicad.yaml         | 2 +-
 src/main/resources/bidder-config/marsmedia.yaml       | 4 ++--
 src/main/resources/bidder-config/mobilefuse.yaml      | 4 ++--
 src/main/resources/bidder-config/nanointeractive.yaml | 4 ++--
 src/main/resources/bidder-config/pubnative.yaml       | 4 ++--
 src/main/resources/bidder-config/smaato.yaml          | 4 ++--
 src/main/resources/bidder-config/smartadserver.yaml   | 2 +-
 src/main/resources/bidder-config/tappx.yaml           | 2 +-
 src/main/resources/bidder-config/ucfunnel.yaml        | 2 +-
 src/main/resources/bidder-config/vrtcal.yaml          | 4 ++--
 src/main/resources/bidder-config/yeahmobi.yaml        | 2 +-
 17 files changed, 25 insertions(+), 25 deletions(-)

diff --git a/src/main/resources/bidder-config/adpone.yaml b/src/main/resources/bidder-config/adpone.yaml
index ab182145225..c34a33e49a4 100644
--- a/src/main/resources/bidder-config/adpone.yaml
+++ b/src/main/resources/bidder-config/adpone.yaml
@@ -14,7 +14,7 @@ adapters:
       site-media-types:
         - banner
       supported-vendors:
-      vendor-id: 16
+      vendor-id: 799
     usersync:
       url: https://usersync.adpone.com/csync?redir=
       redirect-url: /setuid?bidder=adpone&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={uid}
diff --git a/src/main/resources/bidder-config/adprime.yaml b/src/main/resources/bidder-config/adprime.yaml
index 94963141bd6..5c72a95a685 100644
--- a/src/main/resources/bidder-config/adprime.yaml
+++ b/src/main/resources/bidder-config/adprime.yaml
@@ -16,7 +16,7 @@ adapters:
         - banner
         - video
       supported-vendors:
-      vendor-id: 104
+      vendor-id: 0
     usersync:
       url:
       redirect-url:
diff --git a/src/main/resources/bidder-config/adtarget.yaml b/src/main/resources/bidder-config/adtarget.yaml
index 03cf31f8010..82df7f78e53 100644
--- a/src/main/resources/bidder-config/adtarget.yaml
+++ b/src/main/resources/bidder-config/adtarget.yaml
@@ -16,10 +16,10 @@ adapters:
         - banner
         - video
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 779
     usersync:
       url: https://sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
       redirect-url: /setuid?bidder=adtarget&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={uid}
       cookie-family-name: adtarget
       type: redirect
-      support-cors: false
\ No newline at end of file
+      support-cors: false
diff --git a/src/main/resources/bidder-config/adtelligent.yaml b/src/main/resources/bidder-config/adtelligent.yaml
index 5eff25f26ac..c43f508cee2 100644
--- a/src/main/resources/bidder-config/adtelligent.yaml
+++ b/src/main/resources/bidder-config/adtelligent.yaml
@@ -16,7 +16,7 @@ adapters:
         - banner
         - video
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 410
     usersync:
       url: https://sync.adtelligent.com/csync?t=p&ep=0&gdpr={{gdpr}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
       redirect-url: /setuid?bidder=adtelligent&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={uid}
diff --git a/src/main/resources/bidder-config/datablocks.yaml b/src/main/resources/bidder-config/datablocks.yaml
index 944b61dd63b..e97fecb40b5 100644
--- a/src/main/resources/bidder-config/datablocks.yaml
+++ b/src/main/resources/bidder-config/datablocks.yaml
@@ -18,10 +18,10 @@ adapters:
         - video
         - native
       supported-vendors:
-      vendor-id: 14
+      vendor-id: 0
     usersync:
       url: https://sync.v5prebid.datablocks.net/s2ssync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r=
       redirect-url: /setuid?bidder=datablocks&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=${uid}
       cookie-family-name: datablocks
       type: redirect
-      support-cors: false
\ No newline at end of file
+      support-cors: false
diff --git a/src/main/resources/bidder-config/kubient.yaml b/src/main/resources/bidder-config/kubient.yaml
index dd602011e69..0e42f83de5b 100644
--- a/src/main/resources/bidder-config/kubient.yaml
+++ b/src/main/resources/bidder-config/kubient.yaml
@@ -16,7 +16,7 @@ adapters:
         - banner
         - video
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 794
     usersync:
       url:
       redirect-url:
diff --git a/src/main/resources/bidder-config/logicad.yaml b/src/main/resources/bidder-config/logicad.yaml
index 9761a40a805..659e8a9be15 100644
--- a/src/main/resources/bidder-config/logicad.yaml
+++ b/src/main/resources/bidder-config/logicad.yaml
@@ -14,7 +14,7 @@ adapters:
       site-media-types:
         - banner
       supported-vendors:
-      vendor-id: 14
+      vendor-id: 0
     usersync:
       url: https://cr-p31.ladsp.jp/cookiesender/31?r=true&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ru=
       redirect-url: /setuid?bidder=logicad&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID
diff --git a/src/main/resources/bidder-config/marsmedia.yaml b/src/main/resources/bidder-config/marsmedia.yaml
index d6098c1cff8..6bcff853d1b 100644
--- a/src/main/resources/bidder-config/marsmedia.yaml
+++ b/src/main/resources/bidder-config/marsmedia.yaml
@@ -16,10 +16,10 @@ adapters:
         - banner
         - video
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 776
     usersync:
       url: https://dmp.rtbsrv.com/dmp/profiles/cm?p_id=179&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect=
       redirect-url: /setuid?bidder=marsmedia&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=${UUID}
       cookie-family-name: marsmedia
       type: redirect
-      support-cors: false
\ No newline at end of file
+      support-cors: false
diff --git a/src/main/resources/bidder-config/mobilefuse.yaml b/src/main/resources/bidder-config/mobilefuse.yaml
index f612bda31e6..6cefd0b9b6b 100644
--- a/src/main/resources/bidder-config/mobilefuse.yaml
+++ b/src/main/resources/bidder-config/mobilefuse.yaml
@@ -14,10 +14,10 @@ adapters:
         - video
       site-media-types:
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 909
     usersync:
       url:
       redirect-url:
       cookie-family-name: mobilefuse
       type: redirect
-      support-cors: false
\ No newline at end of file
+      support-cors: false
diff --git a/src/main/resources/bidder-config/nanointeractive.yaml b/src/main/resources/bidder-config/nanointeractive.yaml
index 04543d81ac9..88099ec6440 100644
--- a/src/main/resources/bidder-config/nanointeractive.yaml
+++ b/src/main/resources/bidder-config/nanointeractive.yaml
@@ -14,10 +14,10 @@ adapters:
       site-media-types:
         - banner
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 72
     usersync:
       url: https://ad.audiencemanager.de/hbs/cookie_sync?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirectUri=
       redirect-url: /setuid?bidder=nanointeractive&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID
       cookie-family-name: nanointeractive
       type: redirect
-      support-cors: false
\ No newline at end of file
+      support-cors: false
diff --git a/src/main/resources/bidder-config/pubnative.yaml b/src/main/resources/bidder-config/pubnative.yaml
index 3d5fab4bad2..b7c8c0cc7bc 100644
--- a/src/main/resources/bidder-config/pubnative.yaml
+++ b/src/main/resources/bidder-config/pubnative.yaml
@@ -18,10 +18,10 @@ adapters:
         - video
         - native
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 512
     usersync:
       url:
       redirect-url:
       cookie-family-name: pubnative
       type: redirect
-      support-cors: false
\ No newline at end of file
+      support-cors: false
diff --git a/src/main/resources/bidder-config/smaato.yaml b/src/main/resources/bidder-config/smaato.yaml
index bfacf24c294..a91590e7561 100644
--- a/src/main/resources/bidder-config/smaato.yaml
+++ b/src/main/resources/bidder-config/smaato.yaml
@@ -16,10 +16,10 @@ adapters:
         - banner
         - video
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 82
     usersync:
       url:
       redirect-url:
       cookie-family-name: smaato
       type: redirect
-      support-cors: false
\ No newline at end of file
+      support-cors: false
diff --git a/src/main/resources/bidder-config/smartadserver.yaml b/src/main/resources/bidder-config/smartadserver.yaml
index a16630a94fa..00674a88e22 100644
--- a/src/main/resources/bidder-config/smartadserver.yaml
+++ b/src/main/resources/bidder-config/smartadserver.yaml
@@ -16,7 +16,7 @@ adapters:
         - banner
         - video
       supported-vendors:
-      vendor-id: 328
+      vendor-id: 45
     usersync:
       url: https://ssbsync-global.smartadserver.com/api/sync?callerId=5&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirectUri=
       redirect-url: /setuid?bidder=smartadserver&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=[ssb_sync_pid]
diff --git a/src/main/resources/bidder-config/tappx.yaml b/src/main/resources/bidder-config/tappx.yaml
index f0e3541dd0a..ba6019409ce 100644
--- a/src/main/resources/bidder-config/tappx.yaml
+++ b/src/main/resources/bidder-config/tappx.yaml
@@ -14,7 +14,7 @@ adapters:
         - banner
         - video
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 628
     usersync:
       url:
       redirect-url:
diff --git a/src/main/resources/bidder-config/ucfunnel.yaml b/src/main/resources/bidder-config/ucfunnel.yaml
index 98f2309600e..3f011765857 100644
--- a/src/main/resources/bidder-config/ucfunnel.yaml
+++ b/src/main/resources/bidder-config/ucfunnel.yaml
@@ -16,7 +16,7 @@ adapters:
         - banner
         - video
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 607
     usersync:
       url: https://sync.aralego.com/idsync?gdpr={{gdpr}}}&gdpr_consent={{gdpr_consent}}&usprivacy={{us_privacy}}&redirect=
       redirect-url: /setuid?bidder=ucfunnel&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=SspCookieUserId
diff --git a/src/main/resources/bidder-config/vrtcal.yaml b/src/main/resources/bidder-config/vrtcal.yaml
index 1ad90ba09e6..2a632b7b440 100644
--- a/src/main/resources/bidder-config/vrtcal.yaml
+++ b/src/main/resources/bidder-config/vrtcal.yaml
@@ -13,10 +13,10 @@ adapters:
         - banner
       site-media-types:
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 706
     usersync:
       url:
       redirect-url:
       cookie-family-name: vrtcal
       type: redirect
-      support-cors: false
\ No newline at end of file
+      support-cors: false
diff --git a/src/main/resources/bidder-config/yeahmobi.yaml b/src/main/resources/bidder-config/yeahmobi.yaml
index 4e1a9eb6023..4c982fd50b4 100644
--- a/src/main/resources/bidder-config/yeahmobi.yaml
+++ b/src/main/resources/bidder-config/yeahmobi.yaml
@@ -15,7 +15,7 @@ adapters:
         - native
       site-media-types:
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 803
     usersync:
       url:
       redirect-url:

From 7c32a9d1da979fcc0b88508a8031bbd7b8a798df Mon Sep 17 00:00:00 2001
From: Serhii Nahornyi 
Date: Wed, 2 Dec 2020 16:25:20 +0200
Subject: [PATCH 027/129] Add silvermob bidder (#1034)

---
 .../bidder/silvermob/SilvermobBidder.java     | 167 ++++++++++
 .../request/silvermob/ExtImpSilvermob.java    |  18 ++
 .../config/bidder/SilvermobConfiguration.java |  56 ++++
 .../resources/bidder-config/silvermob.yaml    |  24 ++
 .../static/bidder-params/silvermob.json       |  17 +
 .../bidder/silvermob/SilvermobBidderTest.java | 303 ++++++++++++++++++
 .../org/prebid/server/it/SilvermobTest.java   |  63 ++++
 .../test-auction-silvermob-request.json       |  61 ++++
 .../test-auction-silvermob-response.json      |  53 +++
 .../test-cache-silvermob-request.json         |  15 +
 .../test-cache-silvermob-response.json        |   7 +
 .../silvermob/test-silvermob-bid-request.json |  74 +++++
 .../test-silvermob-bid-response.json          |  18 ++
 .../server/it/test-application.properties     |   4 +
 14 files changed, 880 insertions(+)
 create mode 100644 src/main/java/org/prebid/server/bidder/silvermob/SilvermobBidder.java
 create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/silvermob/ExtImpSilvermob.java
 create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/SilvermobConfiguration.java
 create mode 100644 src/main/resources/bidder-config/silvermob.yaml
 create mode 100644 src/main/resources/static/bidder-params/silvermob.json
 create mode 100644 src/test/java/org/prebid/server/bidder/silvermob/SilvermobBidderTest.java
 create mode 100644 src/test/java/org/prebid/server/it/SilvermobTest.java
 create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-request.json
 create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-response.json
 create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-cache-silvermob-request.json
 create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-cache-silvermob-response.json
 create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-request.json
 create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-response.json

diff --git a/src/main/java/org/prebid/server/bidder/silvermob/SilvermobBidder.java b/src/main/java/org/prebid/server/bidder/silvermob/SilvermobBidder.java
new file mode 100644
index 00000000000..bf7d1021f4e
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidder/silvermob/SilvermobBidder.java
@@ -0,0 +1,167 @@
+package org.prebid.server.bidder.silvermob;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Imp;
+import com.iab.openrtb.response.BidResponse;
+import com.iab.openrtb.response.SeatBid;
+import io.vertx.core.MultiMap;
+import io.vertx.core.http.HttpMethod;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.bidder.Bidder;
+import org.prebid.server.bidder.model.BidderBid;
+import org.prebid.server.bidder.model.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.exception.PreBidException;
+import org.prebid.server.json.DecodeException;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.proto.openrtb.ext.ExtPrebid;
+import org.prebid.server.proto.openrtb.ext.request.silvermob.ExtImpSilvermob;
+import org.prebid.server.proto.openrtb.ext.response.BidType;
+import org.prebid.server.util.HttpUtil;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Silvermob {@link Bidder} implementation.
+ */
+public class SilvermobBidder implements Bidder {
+
+    private static final TypeReference> SILVERMOB_EXT_TYPE_REFERENCE =
+            new TypeReference>() {
+            };
+
+    private static final String URL_HOST_MACRO = "{{Host}}";
+    private static final String URL_ZONE_ID_MACRO = "{{ZoneID}}";
+
+    private final String endpointUrl;
+    private final JacksonMapper mapper;
+
+    public SilvermobBidder(String endpointUrl, JacksonMapper mapper) {
+        this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
+        this.mapper = Objects.requireNonNull(mapper);
+    }
+
+    @Override
+    public Result>> makeHttpRequests(BidRequest request) {
+        final List errors = new ArrayList<>();
+        final List> requests = new ArrayList<>();
+
+        for (Imp imp : request.getImp()) {
+            try {
+                requests.add(createRequestForImp(imp, request));
+            } catch (PreBidException e) {
+                errors.add(BidderError.badInput(e.getMessage()));
+            }
+        }
+
+        return Result.of(requests, errors);
+    }
+
+    private HttpRequest createRequestForImp(Imp imp, BidRequest request) {
+        final ExtImpSilvermob extImp = parseImpExt(imp);
+
+        final BidRequest outgoingRequest = request.toBuilder()
+                .imp(Collections.singletonList(imp))
+                .build();
+        return HttpRequest.builder()
+                .method(HttpMethod.POST)
+                .uri(resolveEndpoint(extImp))
+                .headers(resolveHeaders(request.getDevice()))
+                .payload(outgoingRequest)
+                .body(mapper.encode(outgoingRequest))
+                .build();
+    }
+
+    private ExtImpSilvermob parseImpExt(Imp imp) {
+        final ExtImpSilvermob extImp;
+        try {
+            extImp = mapper.mapper().convertValue(imp.getExt(), SILVERMOB_EXT_TYPE_REFERENCE).getBidder();
+        } catch (IllegalArgumentException e) {
+            throw new PreBidException(String.format("error unmarshalling imp.ext.bidder: %s", e.getMessage()));
+        }
+        if (StringUtils.isBlank(extImp.getHost())) {
+            throw new PreBidException("host is a required silvermob ext.imp param");
+        }
+
+        if (StringUtils.isBlank(extImp.getZoneId())) {
+            throw new PreBidException("zoneId is a required silvermob ext.imp param");
+        }
+        return extImp;
+    }
+
+    private String resolveEndpoint(ExtImpSilvermob extImp) {
+        return endpointUrl
+                .replace(URL_HOST_MACRO, extImp.getHost())
+                .replace(URL_ZONE_ID_MACRO, HttpUtil.encodeUrl(extImp.getZoneId()));
+    }
+
+    private static MultiMap resolveHeaders(Device device) {
+        final MultiMap headers = HttpUtil.headers()
+                .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5");
+
+        if (device != null) {
+            HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa());
+            HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6());
+            HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp());
+        }
+
+        return headers;
+    }
+
+    @Override
+    public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) {
+        try {
+            return Result.of(extractBids(httpCall), Collections.emptyList());
+        } catch (DecodeException | PreBidException e) {
+            return Result.withError(BidderError.badServerResponse(e.getMessage()));
+        }
+    }
+
+    private List extractBids(HttpCall httpCall) {
+        final BidResponse bidResponse;
+        try {
+            bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
+        } catch (DecodeException e) {
+            throw new PreBidException(String.format("Error unmarshalling server Response: %s", e.getMessage()));
+        }
+        if (bidResponse == null) {
+            throw new PreBidException("Response in not present");
+        }
+        if (CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
+            throw new PreBidException("Empty SeatBid array");
+        }
+        return bidsFromResponse(bidResponse, httpCall.getRequest().getPayload());
+    }
+
+    private static List bidsFromResponse(BidResponse bidResponse, BidRequest bidRequest) {
+        return bidResponse.getSeatbid().stream()
+                .map(SeatBid::getBid)
+                .flatMap(Collection::stream)
+                .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur()))
+                .collect(Collectors.toList());
+    }
+
+    private static BidType getBidType(String impId, List imps) {
+        for (Imp imp : imps) {
+            if (imp.getId().equals(impId)) {
+                if (imp.getVideo() != null) {
+                    return BidType.video;
+                }
+                if (imp.getXNative() != null) {
+                    return BidType.xNative;
+                }
+            }
+        }
+        return BidType.banner;
+    }
+}
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/silvermob/ExtImpSilvermob.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/silvermob/ExtImpSilvermob.java
new file mode 100644
index 00000000000..53585f07445
--- /dev/null
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/silvermob/ExtImpSilvermob.java
@@ -0,0 +1,18 @@
+package org.prebid.server.proto.openrtb.ext.request.silvermob;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Value;
+
+/**
+ * Defines the contract for bidRequest.imp[i].ext.silvermob
+ */
+@AllArgsConstructor(staticName = "of")
+@Value
+public class ExtImpSilvermob {
+
+    @JsonProperty("zoneid")
+    String zoneId;
+
+    String host;
+}
diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SilvermobConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SilvermobConfiguration.java
new file mode 100644
index 00000000000..06d30545f85
--- /dev/null
+++ b/src/main/java/org/prebid/server/spring/config/bidder/SilvermobConfiguration.java
@@ -0,0 +1,56 @@
+package org.prebid.server.spring.config.bidder;
+
+import org.prebid.server.bidder.BidderDeps;
+import org.prebid.server.bidder.silvermob.SilvermobBidder;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
+import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
+import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
+import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
+import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
+import org.prebid.server.spring.env.YamlPropertySourceFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+import javax.validation.constraints.NotBlank;
+
+@Configuration
+@PropertySource(value = "classpath:/bidder-config/silvermob.yaml", factory = YamlPropertySourceFactory.class)
+public class SilvermobConfiguration {
+
+    private static final String BIDDER_NAME = "silvermob";
+
+    @Value("${external-url}")
+    @NotBlank
+    private String externalUrl;
+
+    @Autowired
+    private JacksonMapper mapper;
+
+    @Autowired
+    @Qualifier("silvermobConfigurationProperties")
+    private BidderConfigurationProperties configProperties;
+
+    @Bean("silvermobConfigurationProperties")
+    @ConfigurationProperties("adapters.silvermob")
+    BidderConfigurationProperties configurationProperties() {
+        return new BidderConfigurationProperties();
+    }
+
+    @Bean
+    BidderDeps silvermobBidderDeps() {
+        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
+
+        return BidderDepsAssembler.forBidder(BIDDER_NAME)
+                .withConfig(configProperties)
+                .bidderInfo(BidderInfoCreator.create(configProperties))
+                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
+                .bidderCreator(() -> new SilvermobBidder(configProperties.getEndpoint(), mapper))
+                .assemble();
+    }
+}
diff --git a/src/main/resources/bidder-config/silvermob.yaml b/src/main/resources/bidder-config/silvermob.yaml
new file mode 100644
index 00000000000..a5a3738e2e5
--- /dev/null
+++ b/src/main/resources/bidder-config/silvermob.yaml
@@ -0,0 +1,24 @@
+adapters:
+  silvermob:
+    enabled: false
+    endpoint: http://{{Host}}.silvermob.com/marketplace/api/dsp/bid/{{ZoneID}}
+    pbs-enforces-gdpr: true
+    pbs-enforces-ccpa: true
+    modifying-vast-xml-allowed: true
+    deprecated-names:
+    aliases:
+    meta-info:
+      maintainer-email: support@silvermob.com
+      app-media-types:
+        - banner
+        - video
+        - native
+      site-media-types:
+      supported-vendors:
+      vendor-id: 0
+    usersync:
+      url:
+      redirect-url:
+      cookie-family-name: silvermob
+      type: redirect
+      support-cors: false
diff --git a/src/main/resources/static/bidder-params/silvermob.json b/src/main/resources/static/bidder-params/silvermob.json
new file mode 100644
index 00000000000..0ded8714cb5
--- /dev/null
+++ b/src/main/resources/static/bidder-params/silvermob.json
@@ -0,0 +1,17 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "SilverMob Adapter Params",
+  "description": "A schema which validates params accepted by the SilverMob adapter",
+  "type": "object",
+  "properties": {
+    "zoneid": {
+      "type": "string",
+      "description": "Zone ID"
+    },
+    "host": {
+      "type": "string",
+      "description": "Host"
+    }
+  },
+  "required": ["zoneid", "host"]
+}
diff --git a/src/test/java/org/prebid/server/bidder/silvermob/SilvermobBidderTest.java b/src/test/java/org/prebid/server/bidder/silvermob/SilvermobBidderTest.java
new file mode 100644
index 00000000000..df4d82cf394
--- /dev/null
+++ b/src/test/java/org/prebid/server/bidder/silvermob/SilvermobBidderTest.java
@@ -0,0 +1,303 @@
+package org.prebid.server.bidder.silvermob;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.iab.openrtb.request.Banner;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Imp;
+import com.iab.openrtb.request.Native;
+import com.iab.openrtb.request.Video;
+import com.iab.openrtb.response.Bid;
+import com.iab.openrtb.response.BidResponse;
+import com.iab.openrtb.response.SeatBid;
+import io.vertx.core.MultiMap;
+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 org.prebid.server.proto.openrtb.ext.request.silvermob.ExtImpSilvermob;
+import org.prebid.server.util.HttpUtil;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import static java.util.Collections.singletonList;
+import static java.util.function.Function.identity;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
+
+public class SilvermobBidderTest extends VertxTest {
+
+    private static final String ENDPOINT_URL = "http://{{Host}}.test/some/path/{{ZoneID}}";
+
+    private SilvermobBidder silvermobBidder;
+
+    @Before
+    public void setUp() {
+        silvermobBidder = new SilvermobBidder(ENDPOINT_URL, jacksonMapper);
+    }
+
+    @Test
+    public void creationShouldFailOnInvalidEndpointUrl() {
+        assertThatIllegalArgumentException().isThrownBy(() -> new SilvermobBidder("invalid_url", jacksonMapper));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
+        // when
+        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).hasSize(1)
+                .allMatch(error ->
+                        error.getMessage().startsWith("error unmarshalling imp.ext.bidder: Cannot deserialize instance")
+                                && error.getType() == BidderError.Type.bad_input);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnErrorIfHostIsEmptyOrNull() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
+                        ExtImpSilvermob.of("testZoneId", "")))));
+        // when
+        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors())
+                .containsExactly(BidderError.badInput("host is a required silvermob ext.imp param"));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnErrorIfZoneIdIsEmptyOrNull() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
+                        ExtImpSilvermob.of(null, "testHost")))));
+        // when
+        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors())
+                .containsExactly(BidderError.badInput("zoneId is a required silvermob ext.imp param"));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldCreateSingleRequestForEveryImpression() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder ->
+                bidRequestBuilder.imp(Arrays.asList(givenImp(identity()), givenImp(identity()))), identity());
+        // when
+        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getValue()).hasSize(2);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnRequestForEveryValidImpressionAndErrorForNotValidImp() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder ->
+                bidRequestBuilder.imp(Arrays.asList(givenImp(impBuilder ->
+                                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
+                                        ExtImpSilvermob.of(null, "testHost"))))),
+                        givenImp(identity()))), identity());
+        // when
+        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getValue()).hasSize(1);
+        assertThat(result.getErrors()).hasSize(1);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldCreateCorrectURL() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build()));
+
+        // when
+        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(HttpRequest::getUri)
+                .containsOnly("http://testHostId.test/some/path/testZoneId");
+    }
+
+    @Test
+    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
+        // given
+        final HttpCall httpCall = givenHttpCall(null, "invalid");
+
+        // when
+        final Result> result = silvermobBidder.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).hasSize(1)
+                .allMatch(error -> error.getMessage().startsWith("Error unmarshalling server "
+                        + "Response: Failed to decode: Unrecognized token")
+                        && error.getType() == BidderError.Type.bad_server_response);
+        assertThat(result.getValue()).isEmpty();
+    }
+
+    @Test
+    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
+        // given
+        final HttpCall httpCall = givenHttpCall(null,
+                mapper.writeValueAsString(null));
+
+        // when
+        final Result> result = silvermobBidder.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Response in not present"));
+        assertThat(result.getValue()).isEmpty();
+    }
+
+    @Test
+    public void makeBidsShouldReturnErrorIfBidResponseSeatBidIsNullOrEmpty() throws JsonProcessingException {
+        // given
+        final HttpCall httpCall = givenHttpCall(null,
+                mapper.writeValueAsString(BidResponse.builder().build()));
+
+        // when
+        final Result> result = silvermobBidder.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty SeatBid array"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
+        // given
+        final HttpCall httpCall = givenHttpCall(
+                BidRequest.builder()
+                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
+                        .build(),
+                mapper.writeValueAsString(
+                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
+
+        // when
+        final Result> result = silvermobBidder.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
+        // given
+        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
+                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
+                        .build(),
+                mapper.writeValueAsString(
+                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
+
+        // when
+        final Result> result = silvermobBidder.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldSetAdditionalHeadersIfDeviceFieldsAreNotEmpty() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                requestBuilder -> requestBuilder
+                        .device(Device.builder().ua("user_agent").ip("test_ip").build()),
+                identity());
+
+        // when
+        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(HttpRequest::getHeaders)
+                .flatExtracting(MultiMap::entries)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsExactlyInAnyOrder(
+                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), "application/json;charset=utf-8"),
+                        tuple(HttpUtil.ACCEPT_HEADER.toString(), "application/json"),
+                        tuple(HttpUtil.USER_AGENT_HEADER.toString(), "user_agent"),
+                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"),
+                        tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "test_ip"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
+        // given
+        final HttpCall httpCall = givenHttpCall(
+                BidRequest.builder()
+                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
+                        .build(),
+                mapper.writeValueAsString(
+                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
+
+        // when
+        final Result> result = silvermobBidder.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
+    }
+
+    private static BidRequest givenBidRequest(
+            Function bidRequestCustomizer,
+            Function impCustomizer) {
+
+        return bidRequestCustomizer.apply(BidRequest.builder()
+                .imp(singletonList(givenImp(impCustomizer))))
+                .build();
+    }
+
+    private static BidRequest givenBidRequest(Function impCustomizer) {
+        return givenBidRequest(identity(), impCustomizer);
+    }
+
+    private static Imp givenImp(Function impCustomizer) {
+        return impCustomizer.apply(Imp.builder()
+                .id("123")
+                .banner(Banner.builder().id("banner_id").build())
+                .ext(mapper.valueToTree(ExtPrebid.of(null,
+                        ExtImpSilvermob.of("testZoneId", "testHostId")))))
+                .build();
+    }
+
+    private static BidResponse givenBidResponse(Function bidCustomizer) {
+        return BidResponse.builder()
+                .cur("USD")
+                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
+                        .build()))
+                .build();
+    }
+
+    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
+        return HttpCall.success(
+                HttpRequest.builder().payload(bidRequest).build(),
+                HttpResponse.of(200, null, body),
+                null);
+    }
+}
diff --git a/src/test/java/org/prebid/server/it/SilvermobTest.java b/src/test/java/org/prebid/server/it/SilvermobTest.java
new file mode 100644
index 00000000000..6ce714dd722
--- /dev/null
+++ b/src/test/java/org/prebid/server/it/SilvermobTest.java
@@ -0,0 +1,63 @@
+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.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.io.IOException;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToIgnoreCase;
+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 io.restassured.RestAssured.given;
+import static java.util.Collections.singletonList;
+
+@RunWith(SpringRunner.class)
+public class SilvermobTest extends IntegrationTest {
+
+    @Test
+    public void openrtb2AuctionShouldRespondWithBidsFromSilvermob() throws IOException, JSONException {
+        // given
+        // Silvermob bid response for imp
+        WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/silvermob-exchange"))
+                .withHeader("Accept", equalTo("application/json"))
+                .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8"))
+                .withHeader("User-Agent", equalToIgnoreCase("test-user-agent"))
+                .withHeader("X-Forwarded-For", equalToIgnoreCase("123.123.123.123"))
+                .withHeader("x-openrtb-version", equalToIgnoreCase("2.5"))
+                .withRequestBody(equalToJson(jsonFrom("openrtb2/silvermob/test-silvermob-bid-request.json")))
+                .willReturn(aResponse().withBody(
+                        jsonFrom("openrtb2/silvermob/test-silvermob-bid-response.json"))));
+
+        // pre-bid cache
+        WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache"))
+                .withRequestBody(equalToJson(jsonFrom("openrtb2/silvermob/test-cache-silvermob-request.json")))
+                .willReturn(aResponse().withBody(
+                        jsonFrom("openrtb2/silvermob/test-cache-silvermob-response.json"))));
+
+        // when
+        final Response response = given(SPEC)
+                .header("Referer", "http://www.example.com")
+                .header("X-Forwarded-For", "193.168.244.1")
+                .header("User-Agent", "userAgent")
+                .header("Origin", "http://www.example.com")
+                // this uids cookie value stands for {"uids":{"silvermob":"SM-UID"}}
+                .cookie("uids", "eyJ1aWRzIjp7InNpbHZlcm1vYiI6IlNNLVVJRCJ9fQ==")
+                .body(jsonFrom("openrtb2/silvermob/test-auction-silvermob-request.json"))
+                .post("/openrtb2/auction");
+
+        // then
+        final String expectedAuctionResponse = openrtbAuctionResponseFrom(
+                "openrtb2/silvermob/test-auction-silvermob-response.json",
+                response, singletonList("silvermob"));
+
+        JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE);
+    }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-request.json b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-request.json
new file mode 100644
index 00000000000..bbbde910291
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-request.json
@@ -0,0 +1,61 @@
+{
+  "id": "some-request-id",
+  "imp": [
+    {
+      "id": "testimpid",
+      "banner": {
+        "w": 320,
+        "h": 250
+      },
+      "ext": {
+        "silvermob": {
+          "zoneid": "testZoneId",
+          "host": "testHostValue"
+        }
+      },
+      "tagid": "impId021"
+    }
+  ],
+  "device": {
+    "ua": "test-user-agent",
+    "ip": "123.123.123.123"
+  },
+  "site": {
+    "publisher": {
+      "id": "publisherId"
+    }
+  },
+  "at": 1,
+  "tmax": 1000,
+  "cur": [
+    "USD"
+  ],
+  "source": {
+    "fd": 1,
+    "tid": "tid"
+  },
+  "ext": {
+    "prebid": {
+      "targeting": {
+        "pricegranularity": {
+          "precision": 2,
+          "ranges": [
+            {
+              "max": 20,
+              "increment": 0.1
+            }
+          ]
+        }
+      },
+      "cache": {
+        "bids": {}
+      },
+      "auctiontimestamp": 1000
+    }
+  },
+  "regs": {
+    "ext": {
+      "gdpr": 0
+    }
+  }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-response.json b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-response.json
new file mode 100644
index 00000000000..de2439c7f86
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-response.json
@@ -0,0 +1,53 @@
+{
+  "id": "some-request-id",
+  "seatbid": [
+    {
+      "bid": [
+        {
+          "id": "testid",
+          "impid": "testimpid",
+          "price": 0.01,
+          "adid": "2068416",
+          "cid": "8048",
+          "crid": "24080",
+          "ext": {
+            "prebid": {
+              "type": "banner",
+              "targeting": {
+                "hb_pb": "0.00",
+                "hb_cache_id_silvermob": "3c0769d8-0dd9-465c-8bf3-f570605ba698",
+                "hb_bidder_silvermob": "silvermob",
+                "hb_bidder": "silvermob",
+                "hb_cache_id": "3c0769d8-0dd9-465c-8bf3-f570605ba698",
+                "hb_pb_silvermob": "0.00",
+                "hb_cache_host": "{{ cache.host }}",
+                "hb_cache_host_silvermob": "{{ cache.host }}",
+                "hb_cache_path": "{{ cache.path }}",
+                "hb_cache_path_silvermob": "{{ cache.path }}"
+              },
+              "cache": {
+                "bids": {
+                  "url": "{{ cache.resource_url }}3c0769d8-0dd9-465c-8bf3-f570605ba698",
+                  "cacheId": "3c0769d8-0dd9-465c-8bf3-f570605ba698"
+                }
+              }
+            }
+          }
+        }
+      ],
+      "seat": "silvermob",
+      "group": 0
+    }
+  ],
+  "cur": "USD",
+  "ext": {
+    "responsetimemillis": {
+      "silvermob": "{{ silvermob.response_time_ms }}",
+      "cache": "{{ cache.response_time_ms }}"
+    },
+    "prebid": {
+      "auctiontimestamp": 1000
+    },
+    "tmaxrequest": 1000
+  }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-cache-silvermob-request.json b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-cache-silvermob-request.json
new file mode 100644
index 00000000000..ca8e3ab2f6a
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-cache-silvermob-request.json
@@ -0,0 +1,15 @@
+{
+  "puts": [
+    {
+      "type": "json",
+      "value": {
+        "crid": "24080",
+        "adid": "2068416",
+        "price": 0.01,
+        "id": "testid",
+        "impid": "testimpid",
+        "cid": "8048"
+      }
+    }
+  ]
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-cache-silvermob-response.json b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-cache-silvermob-response.json
new file mode 100644
index 00000000000..c0100536be1
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-cache-silvermob-response.json
@@ -0,0 +1,7 @@
+{
+  "responses": [
+    {
+      "uuid": "3c0769d8-0dd9-465c-8bf3-f570605ba698"
+    }
+  ]
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-request.json
new file mode 100644
index 00000000000..d713ab3ff08
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-request.json
@@ -0,0 +1,74 @@
+{
+  "id": "some-request-id",
+  "imp": [
+    {
+      "id": "testimpid",
+      "banner": {
+        "w": 320,
+        "h": 250
+      },
+      "tagid": "impId021",
+      "ext": {
+        "bidder": {
+          "zoneid": "testZoneId",
+          "host": "testHostValue"
+        }
+      }
+    }
+  ],
+  "site": {
+    "domain": "example.com",
+    "page": "http://www.example.com",
+    "publisher": {
+      "id": "publisherId"
+    },
+    "ext": {
+      "amp": 0
+    }
+  },
+  "device": {
+    "ua": "test-user-agent",
+    "ip": "123.123.123.123"
+  },
+  "user": {
+    "buyeruid": "SM-UID"
+  },
+  "at": 1,
+  "tmax": 1000,
+  "cur": [
+    "USD"
+  ],
+  "source": {
+    "fd": 1,
+    "tid": "tid"
+  },
+  "regs": {
+    "ext": {
+      "gdpr": 0
+    }
+  },
+  "ext": {
+    "prebid": {
+      "targeting": {
+        "pricegranularity": {
+          "precision": 2,
+          "ranges": [
+            {
+              "max": 20,
+              "increment": 0.1
+            }
+          ]
+        },
+        "includewinners": true,
+        "includebidderkeys": true
+      },
+      "cache": {
+        "bids": {}
+      },
+      "auctiontimestamp": 1000,
+      "channel": {
+        "name": "web"
+      }
+    }
+  }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-response.json
new file mode 100644
index 00000000000..ca4e6ee1db4
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-response.json
@@ -0,0 +1,18 @@
+{
+  "id": "tid",
+  "seatbid": [
+    {
+      "bid": [
+        {
+          "crid": "24080",
+          "adid": "2068416",
+          "price": 0.01,
+          "id": "testid",
+          "impid": "testimpid",
+          "cid": "8048"
+        }
+      ],
+      "type": "banner"
+    }
+  ]
+}
diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties
index 6b2dfcaf412..77a14c83601 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,10 @@ adapters.sharethrough.enabled=true
 adapters.sharethrough.endpoint=http://localhost:8090/sharethrough-exchange
 adapters.sharethrough.pbs-enforces-gdpr=true
 adapters.sharethrough.usersync.url=//sharethrough-usersync
+adapters.silvermob.enabled=true
+adapters.silvermob.endpoint=http://localhost:8090/silvermob-exchange
+adapters.silvermob.pbs-enforces-gdpr=true
+adapters.silvermob.usersync.url=//silvermob-usersync
 adapters.tappx.enabled=true
 adapters.tappx.endpoint=http://
 adapters.tappx.pbs-enforces-gdpr=true

From ea7c2d6641e309ff5b55ed1c16cf2336ab1b0fb5 Mon Sep 17 00:00:00 2001
From: Serhii Nahornyi 
Date: Wed, 2 Dec 2020 16:26:19 +0200
Subject: [PATCH 028/129] DMX Bidfloor fix (#1039)

---
 .../prebid/server/bidder/dmx/DmxBidder.java   |  8 +-
 .../openrtb/ext/request/dmx/ExtImpDmx.java    |  2 -
 .../server/bidder/dmx/DmxBidderTest.java      | 81 ++++++++++++-------
 3 files changed, 56 insertions(+), 35 deletions(-)

diff --git a/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java b/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java
index dad5b15c175..28b345c5069 100644
--- a/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java
+++ b/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java
@@ -156,20 +156,16 @@ private static Imp fetchParams(Imp imp, ExtImpDmx extImp) {
 
         final String tagId = extImp.getTagId();
         if (StringUtils.isNotBlank(tagId)) {
-            updatedImp = Imp.builder()
-                    .id(imp.getId())
+            updatedImp = imp.toBuilder()
                     .tagid(tagId)
-                    .ext(imp.getExt())
                     .secure(SECURE)
                     .build();
         }
 
         final String dmxId = extImp.getDmxId();
         if (StringUtils.isNotBlank(dmxId)) {
-            updatedImp = Imp.builder()
-                    .id(imp.getId())
+            updatedImp = imp.toBuilder()
                     .tagid(dmxId)
-                    .ext(imp.getExt())
                     .secure(SECURE)
                     .build();
         }
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/dmx/ExtImpDmx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/dmx/ExtImpDmx.java
index f056c075814..aadd640dea1 100644
--- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/dmx/ExtImpDmx.java
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/dmx/ExtImpDmx.java
@@ -20,9 +20,7 @@ public class ExtImpDmx {
     @JsonProperty("memberid")
     String memberId;
 
-    @JsonProperty("publisher_id")
     String publisherId;
 
-    @JsonProperty("seller_id")
     String sellerId;
 }
diff --git a/src/test/java/org/prebid/server/bidder/dmx/DmxBidderTest.java b/src/test/java/org/prebid/server/bidder/dmx/DmxBidderTest.java
index 663d5ce69e3..b11e21a00ee 100644
--- a/src/test/java/org/prebid/server/bidder/dmx/DmxBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/dmx/DmxBidderTest.java
@@ -1,7 +1,6 @@
 package org.prebid.server.bidder.dmx;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
 import com.iab.openrtb.request.Banner;
 import com.iab.openrtb.request.BidRequest;
 import com.iab.openrtb.request.Format;
@@ -24,6 +23,7 @@
 import org.prebid.server.proto.openrtb.ext.request.dmx.ExtImpDmx;
 import org.prebid.server.proto.openrtb.ext.response.BidType;
 
+import java.math.BigDecimal;
 import java.util.List;
 import java.util.function.Function;
 
@@ -32,7 +32,6 @@
 import static java.util.function.Function.identity;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
-import static org.assertj.core.api.Assertions.tuple;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
 
@@ -96,22 +95,58 @@ public void makeHttpRequestsShouldReturnErrorIfUserIdIsEmpty() {
     @Test
     public void makeHttpRequestsShouldModifyImpIfBannerFormatIsNotEmpty() {
         // given
+        final Imp givenImp = Imp.builder()
+                .id("id")
+                .bidfloor(BigDecimal.ONE)
+                .banner(Banner.builder()
+                        .format(singletonList(Format.builder().w(300).h(500).build()))
+                        .build())
+                .ext(mapper.valueToTree(ExtPrebid.of(null,
+                        ExtImpDmx.builder()
+                                .tagId("tagId")
+                                .dmxId("dmxId")
+                                .memberId("memberId")
+                                .publisherId("publisherId")
+                                .sellerId("sellerId")
+                                .build())))
+                .build();
         final BidRequest bidRequest = BidRequest.builder()
-                .imp(singletonList(
-                        Imp.builder()
-                                .id("id")
-                                .banner(Banner.builder()
-                                        .format(singletonList(Format.builder().w(300).h(500).build()))
-                                        .build())
-                                .ext(mapper.valueToTree(ExtPrebid.of(null,
-                                        ExtImpDmx.builder()
-                                                .tagId("tagId")
-                                                .dmxId("dmxId")
-                                                .memberId("memberId")
-                                                .publisherId("publisherId")
-                                                .sellerId("sellerId")
-                                                .build())))
-                                .build()))
+                .imp(singletonList(givenImp))
+                .user(User.builder().id("userId").build())
+                .build();
+
+        // when
+        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
+
+        // then
+        final Imp expectedImp = givenImp.toBuilder()
+                .tagid("dmxId")
+                .secure(1)
+                .build();
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+                .flatExtracting(BidRequest::getImp)
+                .containsExactly(expectedImp);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldWriteTagIdToImpIfItIsPresentAndDmxIsMissing() {
+        // given
+        final Imp givenImp = Imp.builder()
+                .banner(Banner.builder()
+                        .format(singletonList(Format.builder().build()))
+                        .build())
+                .ext(mapper.valueToTree(ExtPrebid.of(null,
+                        ExtImpDmx.builder()
+                                .tagId("tagId")
+                                .dmxId("")
+                                .memberId("memberId")
+                                .publisherId("publisherId")
+                                .build())))
+                .build();
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(givenImp))
                 .user(User.builder().id("userId").build())
                 .build();
 
@@ -120,19 +155,11 @@ public void makeHttpRequestsShouldModifyImpIfBannerFormatIsNotEmpty() {
 
         // then
         assertThat(result.getErrors()).isEmpty();
-        final JsonNode expectedImpExt = mapper.valueToTree(ExtPrebid.of(null,
-                ExtImpDmx.builder()
-                        .tagId("tagId")
-                        .dmxId("dmxId")
-                        .memberId("memberId")
-                        .publisherId("publisherId")
-                        .sellerId("sellerId")
-                        .build()));
         assertThat(result.getValue()).hasSize(1)
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .flatExtracting(BidRequest::getImp)
-                .extracting(Imp::getId, Imp::getTagid, Imp::getExt, Imp::getSecure)
-                .containsOnly(tuple("id", "dmxId", expectedImpExt, 1));
+                .extracting(Imp::getTagid)
+                .containsExactly("tagId");
     }
 
     @Test

From 1b4ba3cc36fa83957f3b07a9887495237eaf4cc5 Mon Sep 17 00:00:00 2001
From: Serhii Nahornyi 
Date: Wed, 2 Dec 2020 16:29:56 +0200
Subject: [PATCH 029/129] Add Adform video bid response support (#1038)

---
 .../server/bidder/adform/AdformBidder.java    | 25 +++++++++--
 .../server/bidder/adform/model/AdformBid.java |  2 +
 .../bidder/adform/AdformBidderTest.java       | 43 ++++++++++++++++---
 3 files changed, 61 insertions(+), 9 deletions(-)

diff --git a/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java b/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java
index b71583b401c..479fcfe764c 100644
--- a/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java
+++ b/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java
@@ -291,24 +291,43 @@ private List toBidderBid(List adformBids, List imps)
 
         for (int i = 0; i < adformBids.size(); i++) {
             final AdformBid adformBid = adformBids.get(i);
-            if (StringUtils.isEmpty(adformBid.getBanner()) || !Objects.equals(adformBid.getResponse(), BANNER)) {
+            final String adm = resolveAdm(adformBid);
+            if (StringUtils.isBlank(adm)) {
                 continue;
             }
+            final BidType bidType = resolveBidType(adformBid.getResponse());
             final Imp imp = imps.get(i);
             bidderBids.add(BidderBid.of(Bid.builder()
                             .id(imp.getId())
                             .impid(imp.getId())
                             .price(adformBid.getWinBid())
-                            .adm(adformBid.getBanner())
+                            .adm(adm)
                             .w(adformBid.getWidth())
                             .h(adformBid.getHeight())
                             .dealid(adformBid.getDealId())
                             .crid(adformBid.getWinCrid())
                             .build(),
-                    BidType.banner,
+                    bidType,
                     currency));
         }
 
         return bidderBids;
     }
+
+    private String resolveAdm(AdformBid adformBid) {
+        if (Objects.equals(adformBid.getResponse(), "banner")) {
+            return adformBid.getBanner();
+        }
+
+        if (Objects.equals(adformBid.getResponse(), "vast_content")) {
+            return adformBid.getVastContent();
+        }
+
+        return "";
+    }
+
+    private BidType resolveBidType(String response) {
+        return Objects.equals(response, BANNER)
+                ? BidType.banner : BidType.video;
+    }
 }
diff --git a/src/main/java/org/prebid/server/bidder/adform/model/AdformBid.java b/src/main/java/org/prebid/server/bidder/adform/model/AdformBid.java
index f6fc6789f9f..64729679896 100644
--- a/src/main/java/org/prebid/server/bidder/adform/model/AdformBid.java
+++ b/src/main/java/org/prebid/server/bidder/adform/model/AdformBid.java
@@ -24,4 +24,6 @@ public class AdformBid {
     String dealId;
 
     String winCrid;
+
+    String vastContent;
 }
diff --git a/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java b/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java
index fb5852b825e..0c2ca714f6b 100644
--- a/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java
@@ -32,7 +32,6 @@
 import org.prebid.server.proto.openrtb.ext.response.BidType;
 import org.prebid.server.util.HttpUtil;
 
-import java.math.BigDecimal;
 import java.util.Base64;
 import java.util.List;
 import java.util.Map;
@@ -248,11 +247,10 @@ public void makeBidsShouldReturnEmptyListIfResponseTypeIsNotBanner() throws Json
     }
 
     @Test
-    public void makeBidsShouldReturnBidderBid() throws JsonProcessingException {
+    public void makeBidsShouldReturnBidderBidIfResponseIsBannerAndBannerIsPresent() throws JsonProcessingException {
         // given
         final String adformResponse = mapper.writeValueAsString(AdformBid.builder().banner("admBanner")
-                .response("banner").winCur("currency").dealId("dealId").height(300).width(400).winCrid("gross")
-                .winBid(BigDecimal.ONE).build());
+                .response("banner").winCur("currency").build());
 
         final HttpCall httpCall = givenHttpCall(adformResponse);
         final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("id").build())).build();
@@ -263,8 +261,41 @@ public void makeBidsShouldReturnBidderBid() throws JsonProcessingException {
         // then
         assertThat(result.getErrors()).isEmpty();
         assertThat(result.getValue()).hasSize(1).containsOnly(BidderBid.of(
-                Bid.builder().id("id").impid("id").price(BigDecimal.ONE).adm("admBanner").w(400).h(300).dealid("dealId")
-                        .crid("gross").build(), BidType.banner, "currency"));
+                Bid.builder().id("id").impid("id").adm("admBanner").build(), BidType.banner, "currency"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnVideoBidIfResponseEqualsVastContent() throws JsonProcessingException {
+        // given
+        final String adformResponse = mapper.writeValueAsString(
+                AdformBid.builder().winCur("currency").vastContent("admVastContent").response("vast_content").build());
+
+        final HttpCall httpCall = givenHttpCall(adformResponse);
+        final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("id").build())).build();
+
+        // when
+        final Result> result = adformBidder.makeBids(httpCall, bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1).containsOnly(BidderBid.of(Bid.builder().id("id").impid("id")
+                .adm("admVastContent").build(), BidType.video, "currency"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnEmptyResultIfVastContentOrBannerIsNotPresent() throws JsonProcessingException {
+        // given
+        final String adformResponse = mapper.writeValueAsString(AdformBid.builder().build());
+
+        final HttpCall httpCall = givenHttpCall(adformResponse);
+        final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("id").build())).build();
+
+        // when
+        final Result> result = adformBidder.makeBids(httpCall, bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).isEmpty();
     }
 
     @Test

From a5ac56e7580f2c9db2686370c3445c6cf88a5500 Mon Sep 17 00:00:00 2001
From: Serhii Nahornyi 
Date: Wed, 2 Dec 2020 16:34:08 +0200
Subject: [PATCH 030/129] Review smartyAds bidder (#1036)

---
 .../bidder/smartyads/SmartyAdsBidder.java     |  65 ++++++++---
 .../bidder/smartyads/SmartyAdsBidderTest.java | 109 +++++++++++++++++-
 2 files changed, 151 insertions(+), 23 deletions(-)

diff --git a/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java b/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java
index 910d39bac0e..25a4637cd08 100644
--- a/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java
+++ b/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java
@@ -4,11 +4,13 @@
 import com.iab.openrtb.request.BidRequest;
 import com.iab.openrtb.request.Device;
 import com.iab.openrtb.request.Imp;
+import com.iab.openrtb.response.Bid;
 import com.iab.openrtb.response.BidResponse;
 import com.iab.openrtb.response.SeatBid;
 import io.vertx.core.MultiMap;
 import io.vertx.core.http.HttpMethod;
 import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.bidder.Bidder;
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.bidder.model.BidderError;
@@ -29,6 +31,9 @@
 import java.util.Objects;
 import java.util.stream.Collectors;
 
+/**
+ * Between {@link Bidder} implementation.
+ */
 public class SmartyAdsBidder implements Bidder {
 
     private static final TypeReference> SMARTYADS_EXT_TYPE_REFERENCE =
@@ -37,6 +42,7 @@ public class SmartyAdsBidder implements Bidder {
     private static final String URL_HOST_MACRO = "{{Host}}";
     private static final String URL_SOURCE_ID_MACRO = "{{SourceId}}";
     private static final String URL_ACCOUNT_ID_MACRO = "{{AccountID}}";
+    private static final int FIRST_SEAT_BID_INDEX = 0;
 
     private final String endpointUrl;
     private final JacksonMapper mapper;
@@ -48,7 +54,6 @@ public SmartyAdsBidder(String endpointUrl, JacksonMapper mapper) {
 
     @Override
     public Result>> makeHttpRequests(BidRequest request) {
-        final List errors = new ArrayList<>();
         final List validImps = new ArrayList<>();
 
         ExtImpSmartyAds extImpSmartyAds = null;
@@ -63,22 +68,33 @@ public Result>> makeHttpRequests(BidRequest request
 
         final BidRequest outgoingRequest = request.toBuilder().imp(validImps).build();
 
-        return Result.of(Collections.singletonList(
+        return Result.withValues(Collections.singletonList(
                 HttpRequest.builder()
                         .method(HttpMethod.POST)
                         .uri(resolveUrl(extImpSmartyAds))
                         .headers(resolveHeaders(request.getDevice()))
                         .payload(outgoingRequest)
                         .body(mapper.encode(outgoingRequest))
-                        .build()), errors);
+                        .build()));
     }
 
     private ExtImpSmartyAds parseImpExt(Imp imp) {
+        final ExtImpSmartyAds extImpSmartyAds;
         try {
-            return mapper.mapper().convertValue(imp.getExt(), SMARTYADS_EXT_TYPE_REFERENCE).getBidder();
+            extImpSmartyAds = mapper.mapper().convertValue(imp.getExt(), SMARTYADS_EXT_TYPE_REFERENCE).getBidder();
         } catch (IllegalArgumentException e) {
             throw new PreBidException("ext.bidder not provided");
         }
+        if (StringUtils.isBlank(extImpSmartyAds.getHost())) {
+            throw new PreBidException("host is a required ext.bidder param");
+        }
+        if (StringUtils.isBlank(extImpSmartyAds.getAccountId())) {
+            throw new PreBidException("accountId is a required ext.bidder param");
+        }
+        if (StringUtils.isBlank(extImpSmartyAds.getSourceId())) {
+            throw new PreBidException("sourceId is a required ext.bidder param");
+        }
+        return extImpSmartyAds;
     }
 
     private static Imp updateImp(Imp imp) {
@@ -88,11 +104,11 @@ private static Imp updateImp(Imp imp) {
     private String resolveUrl(ExtImpSmartyAds extImp) {
         return endpointUrl
                 .replace(URL_HOST_MACRO, extImp.getHost())
-                .replace(URL_SOURCE_ID_MACRO, extImp.getSourceId())
-                .replace(URL_ACCOUNT_ID_MACRO, extImp.getAccountId());
+                .replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(extImp.getSourceId()))
+                .replace(URL_ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(extImp.getAccountId()));
     }
 
-    private MultiMap resolveHeaders(Device device) {
+    private static MultiMap resolveHeaders(Device device) {
         final MultiMap headers = HttpUtil.headers();
         headers.add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5");
 
@@ -112,29 +128,42 @@ private MultiMap resolveHeaders(Device device) {
     @Override
     public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) {
         try {
-            final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
-            return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList());
-        } catch (DecodeException | PreBidException e) {
+            return Result.of(extractBids(httpCall), Collections.emptyList());
+        } catch (PreBidException e) {
             return Result.withError(BidderError.badServerResponse(e.getMessage()));
         }
     }
 
-    private List extractBids(BidRequest bidRequest, BidResponse bidResponse) {
-        if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
-            return Collections.emptyList();
+    private List extractBids(HttpCall httpCall) {
+        final BidResponse bidResponse;
+        try {
+            bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
+        } catch (DecodeException e) {
+            throw new PreBidException("Bad Server Response");
         }
-        return bidsFromResponse(bidRequest, bidResponse);
+        if (bidResponse == null) {
+            throw new PreBidException("Bad Server Response");
+        }
+        if (CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
+            throw new PreBidException("Empty SeatBid array");
+        }
+        return bidsFromResponse(httpCall.getRequest().getPayload(), bidResponse);
     }
 
-    private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) {
-        final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0);
-        return firstSeatBid.getBid().stream()
+    private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) {
+        final SeatBid firstSeatBid = bidResponse.getSeatbid().get(FIRST_SEAT_BID_INDEX);
+        final List bidsFromSeat = firstSeatBid.getBid();
+        if (CollectionUtils.isEmpty(bidsFromSeat)) {
+            return Collections.emptyList();
+        }
+
+        return bidsFromSeat.stream()
                 .filter(Objects::nonNull)
                 .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur()))
                 .collect(Collectors.toList());
     }
 
-    protected BidType getBidType(String impId, List imps) {
+    private static BidType getBidType(String impId, List imps) {
         for (Imp imp : imps) {
             if (imp.getId().equals(impId)) {
                 if (imp.getVideo() != null) {
diff --git a/src/test/java/org/prebid/server/bidder/smartyads/SmartyAdsBidderTest.java b/src/test/java/org/prebid/server/bidder/smartyads/SmartyAdsBidderTest.java
index 3d03d777b50..6dcffbdab37 100644
--- a/src/test/java/org/prebid/server/bidder/smartyads/SmartyAdsBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/smartyads/SmartyAdsBidderTest.java
@@ -3,12 +3,14 @@
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.iab.openrtb.request.Banner;
 import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
 import com.iab.openrtb.request.Imp;
 import com.iab.openrtb.request.Native;
 import com.iab.openrtb.request.Video;
 import com.iab.openrtb.response.Bid;
 import com.iab.openrtb.response.BidResponse;
 import com.iab.openrtb.response.SeatBid;
+import io.vertx.core.MultiMap;
 import org.junit.Before;
 import org.junit.Test;
 import org.prebid.server.VertxTest;
@@ -20,16 +22,19 @@
 import org.prebid.server.bidder.model.Result;
 import org.prebid.server.proto.openrtb.ext.ExtPrebid;
 import org.prebid.server.proto.openrtb.ext.request.smartyads.ExtImpSmartyAds;
+import org.prebid.server.util.HttpUtil;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Function;
 
 import static java.util.Collections.singletonList;
 import static java.util.function.Function.identity;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.tuple;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
@@ -65,6 +70,49 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
         assertThat(result.getErrors().get(0).getMessage()).startsWith("ext.bidder not provided");
     }
 
+    @Test
+    public void makeHttpRequestsShouldReturnErrorIfExtBidderAccountIdParamIsMissed() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder .ext(mapper.valueToTree(ExtPrebid.of(null,
+                        ExtImpSmartyAds.of("", "testSourceId", "testHost")))));
+
+        // when
+        final Result>> result = smartyAdsBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors())
+                .containsExactly(BidderError.badInput("accountId is a required ext.bidder param"));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnErrorIfExtBidderSourceIdParamIsMissed() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder .ext(mapper.valueToTree(ExtPrebid.of(null,
+                        ExtImpSmartyAds.of("testAccountId", "", "testHost")))));
+
+        // when
+        final Result>> result = smartyAdsBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).containsExactly(BidderError.badInput("sourceId is a required ext.bidder param"));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnErrorIfExtBidderHostParamIsMissed() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder .ext(mapper.valueToTree(ExtPrebid.of(null,
+                        ExtImpSmartyAds.of("testAccountId", "testSourceId", "")))));
+
+        // when
+        final Result>> result = smartyAdsBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).containsExactly(BidderError.badInput("host is a required ext.bidder param"));
+    }
+
     @Test
     public void makeHttpRequestsShouldCreateCorrectURL() {
         // given
@@ -112,9 +160,7 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
         final Result> result = smartyAdsBidder.makeBids(httpCall, null);
 
         // then
-        assertThat(result.getErrors()).hasSize(1);
-        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
-        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
+        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Bad Server Response"));
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -128,7 +174,7 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces
         final Result> result = smartyAdsBidder.makeBids(httpCall, null);
 
         // then
-        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Bad Server Response"));
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -142,7 +188,7 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso
         final Result> result = smartyAdsBidder.makeBids(httpCall, null);
 
         // then
-        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty SeatBid array"));
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -165,6 +211,27 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws
                 .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
     }
 
+    @Test
+    public void makeBidsShouldReturnEmptyValueIfBidsAreNotPresent() throws JsonProcessingException {
+        // given
+        final HttpCall httpCall = givenHttpCall(
+                BidRequest.builder()
+                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
+                        .build(),
+                mapper.writeValueAsString(BidResponse.builder()
+                        .seatbid(singletonList(SeatBid.builder()
+                                .bid(null)
+                                .build()))
+                        .build()));
+
+        // when
+        final Result> result = smartyAdsBidder.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).isEmpty();
+    }
+
     @Test
     public void makeBidsShouldWorkWithOnlyFirstSeatBid() throws JsonProcessingException {
         // given
@@ -196,6 +263,38 @@ public void makeBidsShouldWorkWithOnlyFirstSeatBid() throws JsonProcessingExcept
                 .containsOnly(BidderBid.of(Bid.builder().impid("456").build(), banner, "USD"));
     }
 
+    @Test
+    public void makeHttpRequestsShouldSetAdditionalHeadersIfDeviceFieldsAreNotEmpty() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                requestBuilder -> requestBuilder
+                        .device(Device.builder()
+                                .ua("user_agent")
+                                .ip("test_ip")
+                                .dnt(23)
+                                .language("testLang")
+                                .build()),
+                identity());
+
+        // when
+        final Result>> result = smartyAdsBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(HttpRequest::getHeaders)
+                .flatExtracting(MultiMap::entries)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsExactlyInAnyOrder(
+                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), "application/json;charset=utf-8"),
+                        tuple(HttpUtil.ACCEPT_HEADER.toString(), "application/json"),
+                        tuple(HttpUtil.ACCEPT_LANGUAGE_HEADER.toString(), "testLang"),
+                        tuple(HttpUtil.DNT_HEADER.toString(), "23"),
+                        tuple(HttpUtil.USER_AGENT_HEADER.toString(), "user_agent"),
+                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"),
+                        tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "test_ip"));
+    }
+
     @Test
     public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
         // given

From 15dc15bdc270957e333175e614abd727793fac02 Mon Sep 17 00:00:00 2001
From: Dmitriy 
Date: Wed, 2 Dec 2020 16:40:23 +0200
Subject: [PATCH 031/129] Review Yieldmo bidder (#1041)

---
 .../server/bidder/yieldmo/YieldmoBidder.java  |  4 ++
 .../bidder/yieldmo/YieldmoBidderTest.java     | 49 +++++++++++++++++--
 .../org/prebid/server/it/YieldmoTest.java     |  4 ++
 3 files changed, 53 insertions(+), 4 deletions(-)

diff --git a/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java b/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java
index 4d6a237d217..393f1685dc8 100644
--- a/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java
+++ b/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java
@@ -1,6 +1,7 @@
 package org.prebid.server.bidder.yieldmo;
 
 import com.iab.openrtb.request.Imp;
+import org.prebid.server.bidder.Bidder;
 import org.prebid.server.bidder.OpenrtbBidder;
 import org.prebid.server.bidder.yieldmo.proto.YieldmoImpExt;
 import org.prebid.server.exception.PreBidException;
@@ -10,6 +11,9 @@
 
 import java.util.List;
 
+/**
+ * Yieldmo {@link Bidder} implementation.
+ */
 public class YieldmoBidder extends OpenrtbBidder {
 
     public YieldmoBidder(String endpointUrl, JacksonMapper mapper) {
diff --git a/src/test/java/org/prebid/server/bidder/yieldmo/YieldmoBidderTest.java b/src/test/java/org/prebid/server/bidder/yieldmo/YieldmoBidderTest.java
index bc77c289379..48695b219dc 100644
--- a/src/test/java/org/prebid/server/bidder/yieldmo/YieldmoBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/yieldmo/YieldmoBidderTest.java
@@ -8,6 +8,7 @@
 import com.iab.openrtb.response.Bid;
 import com.iab.openrtb.response.BidResponse;
 import com.iab.openrtb.response.SeatBid;
+import io.netty.handler.codec.http.HttpHeaderValues;
 import org.junit.Before;
 import org.junit.Test;
 import org.prebid.server.VertxTest;
@@ -17,23 +18,27 @@
 import org.prebid.server.bidder.model.HttpRequest;
 import org.prebid.server.bidder.model.HttpResponse;
 import org.prebid.server.bidder.model.Result;
+import org.prebid.server.bidder.yieldmo.proto.YieldmoImpExt;
 import org.prebid.server.proto.openrtb.ext.ExtPrebid;
 import org.prebid.server.proto.openrtb.ext.request.yieldmo.ExtImpYieldmo;
+import org.prebid.server.util.HttpUtil;
 
 import java.util.List;
+import java.util.Map;
 import java.util.function.Function;
 
 import static java.util.Collections.singletonList;
 import static java.util.function.Function.identity;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.tuple;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
 
 public class YieldmoBidderTest extends VertxTest {
 
     private static final String ENDPOINT_URL = "https://test.endpoint.com";
-
+    private static final String PLACEMENT_VALUE = "placementId";
     private YieldmoBidder yieldmoBidder;
 
     @Before
@@ -49,8 +54,8 @@ public void creationShouldFailOnInvalidEndpointUrl() {
     @Test
     public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
         // given
-        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
-                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
+        final BidRequest bidRequest = givenBidRequest(impBuilder ->
+                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
 
         // when
         final Result>> result = yieldmoBidder.makeHttpRequests(bidRequest);
@@ -61,6 +66,42 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
         assertThat(result.getValue()).isEmpty();
     }
 
+    @Test
+    public void makeHttpRequestsShouldReturnExtPlacementFromYieldmoPlacement() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(Function.identity());
+
+        // when
+        final Result>> result = yieldmoBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).hasSize(0);
+
+        final YieldmoImpExt expectedExt = YieldmoImpExt.of(PLACEMENT_VALUE);
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getExt)
+                .containsOnly(mapper.valueToTree(expectedExt));
+    }
+
+    @Test
+    public void makeHttpRequestShouldReturnCorrectHeaders() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(Function.identity());
+
+        // when
+        final Result>> result = yieldmoBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).hasSize(0);
+        assertThat(result.getValue()).flatExtracting(httpRequest -> httpRequest.getHeaders().entries())
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
+                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()));
+    }
+
     @Test
     public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
         // given
@@ -178,7 +219,7 @@ private static Imp givenImp(Function impCustomiz
         return impCustomizer.apply(Imp.builder()
                 .id("123")
                 .banner(Banner.builder().build())
-                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldmo.of("placementId")))))
+                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldmo.of(PLACEMENT_VALUE)))))
                 .build();
     }
 
diff --git a/src/test/java/org/prebid/server/it/YieldmoTest.java b/src/test/java/org/prebid/server/it/YieldmoTest.java
index 9718ec40452..0e707cd5376 100644
--- a/src/test/java/org/prebid/server/it/YieldmoTest.java
+++ b/src/test/java/org/prebid/server/it/YieldmoTest.java
@@ -11,6 +11,8 @@
 import java.io.IOException;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToIgnoreCase;
 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;
@@ -25,6 +27,8 @@ public void openrtb2AuctionShouldRespondWithBidsFromYieldmo() throws IOException
         // given
         // Yieldmo bid response for imp 001
         WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/yieldmo-exchange"))
+                .withHeader("Accept", equalTo("application/json"))
+                .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8"))
                 .withRequestBody(equalToJson(jsonFrom("openrtb2/yieldmo/test-yieldmo-bid-request-1.json")))
                 .willReturn(aResponse().withBody(jsonFrom("openrtb2/yieldmo/test-yieldmo-bid-response-1.json"))));
 

From 19c51fc1dc12c016626f9b86f1b49f5a936adb15 Mon Sep 17 00:00:00 2001
From: Dmitriy 
Date: Wed, 2 Dec 2020 16:44:47 +0200
Subject: [PATCH 032/129] Review Vrtcal bidder (#1042)

---
 .../server/bidder/vrtcal/VrtcalBidder.java    |  4 ++
 .../bidder/vrtcal/VrtcalBidderTest.java       | 55 +++++++++++++++----
 .../java/org/prebid/server/it/VrtcalTest.java |  4 ++
 3 files changed, 52 insertions(+), 11 deletions(-)

diff --git a/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java b/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java
index 5c436cbb863..c60f7cd8715 100644
--- a/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java
+++ b/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java
@@ -1,6 +1,7 @@
 package org.prebid.server.bidder.vrtcal;
 
 import com.iab.openrtb.request.Imp;
+import org.prebid.server.bidder.Bidder;
 import org.prebid.server.bidder.OpenrtbBidder;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.proto.openrtb.ext.request.vrtcal.ExtImpVrtcal;
@@ -8,6 +9,9 @@
 
 import java.util.List;
 
+/**
+ * Vrtcal {@link Bidder} implementation.
+ */
 public class VrtcalBidder extends OpenrtbBidder {
 
     public VrtcalBidder(String endpointUrl, JacksonMapper mapper) {
diff --git a/src/test/java/org/prebid/server/bidder/vrtcal/VrtcalBidderTest.java b/src/test/java/org/prebid/server/bidder/vrtcal/VrtcalBidderTest.java
index 2a0f8e2735e..1e87c87458c 100644
--- a/src/test/java/org/prebid/server/bidder/vrtcal/VrtcalBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/vrtcal/VrtcalBidderTest.java
@@ -6,6 +6,7 @@
 import com.iab.openrtb.response.Bid;
 import com.iab.openrtb.response.BidResponse;
 import com.iab.openrtb.response.SeatBid;
+import io.netty.handler.codec.http.HttpHeaderValues;
 import org.junit.Before;
 import org.junit.Test;
 import org.prebid.server.VertxTest;
@@ -17,14 +18,17 @@
 import org.prebid.server.bidder.model.Result;
 import org.prebid.server.proto.openrtb.ext.ExtPrebid;
 import org.prebid.server.proto.openrtb.ext.request.vrtcal.ExtImpVrtcal;
+import org.prebid.server.util.HttpUtil;
 
 import java.util.List;
+import java.util.Map;
 import java.util.function.Function;
 
 import static java.util.Collections.singletonList;
 import static java.util.function.Function.identity;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.tuple;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
 
 public class VrtcalBidderTest extends VertxTest {
@@ -46,10 +50,8 @@ public void creationShouldFailOnInvalidEndpointUrl() {
     @Test
     public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
         // given
-        final BidRequest bidRequest = BidRequest.builder()
-                .imp(singletonList(Imp.builder()
-                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))).build()))
-                .build();
+        final BidRequest bidRequest = givenBidRequest(impBuilder ->
+                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
 
         // when
         final Result>> result = vrtcalBidder.makeHttpRequests(bidRequest);
@@ -63,13 +65,8 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
     @Test
     public void makeHttpRequestsShouldNotModifyIncomingRequest() {
         // given
-        final BidRequest bidRequest = BidRequest.builder()
-                .imp(singletonList(Imp.builder()
-                        .ext(mapper.valueToTree(ExtPrebid.of(null,
-                                ExtImpVrtcal.of("JustAnUnusedVrtcalParam"))))
-                        .build()))
-                .id("request_id")
-                .build();
+        final BidRequest bidRequest = givenBidRequest(impBuilder ->
+                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpVrtcal.of("JustAnUnusedVrtcalParam")))));
 
         // when
         final Result>> result = vrtcalBidder.makeHttpRequests(bidRequest);
@@ -81,6 +78,23 @@ public void makeHttpRequestsShouldNotModifyIncomingRequest() {
                 .containsOnly(bidRequest);
     }
 
+    @Test
+    public void makeHttpRequestShouldReturnCorrectHeaders() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(Function.identity());
+
+        // when
+        final Result>> result = vrtcalBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).hasSize(0);
+        assertThat(result.getValue()).flatExtracting(httpRequest -> httpRequest.getHeaders().entries())
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
+                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()));
+    }
+
     @Test
     public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
         // given
@@ -140,6 +154,25 @@ public void makeBidsShouldAlwaysReturnBannerBid() throws JsonProcessingException
                 .containsOnly(BidderBid.of(Bid.builder().build(), banner, "USD"));
     }
 
+    private static BidRequest givenBidRequest(
+            Function bidRequestCustomizer,
+            Function impCustomizer) {
+
+        return bidRequestCustomizer.apply(BidRequest.builder()
+                .imp(singletonList(givenImp(impCustomizer))))
+                .build();
+    }
+
+    private static BidRequest givenBidRequest(Function impCustomizer) {
+        return givenBidRequest(identity(), impCustomizer);
+    }
+
+    private static Imp givenImp(Function impCustomizer) {
+        return impCustomizer.apply(Imp.builder()
+                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpVrtcal.of("JustAnUnusedVrtcalParam")))))
+                .build();
+    }
+
     private static BidResponse givenBidResponse(Function bidCustomizer) {
         return BidResponse.builder()
                 .cur("USD")
diff --git a/src/test/java/org/prebid/server/it/VrtcalTest.java b/src/test/java/org/prebid/server/it/VrtcalTest.java
index c4a2ab8975e..daf120dfed6 100644
--- a/src/test/java/org/prebid/server/it/VrtcalTest.java
+++ b/src/test/java/org/prebid/server/it/VrtcalTest.java
@@ -11,6 +11,8 @@
 import java.io.IOException;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToIgnoreCase;
 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;
@@ -25,6 +27,8 @@ public void openrtb2AuctionShouldRespondWithBidsFromVrtcal() throws IOException,
         // given
         // Vrtcal bid response for imp 001
         WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/vrtcal-exchange"))
+                .withHeader("Accept", equalTo("application/json"))
+                .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8"))
                 .withRequestBody(equalToJson(jsonFrom("openrtb2/vrtcal/test-vrtcal-bid-request-1.json")))
                 .willReturn(aResponse().withBody(jsonFrom("openrtb2/vrtcal/test-vrtcal-bid-response-1.json"))));
 

From f86612c3bef7077e27475b98d8a56a66c503169b Mon Sep 17 00:00:00 2001
From: bretg 
Date: Wed, 2 Dec 2020 10:20:23 -0500
Subject: [PATCH 033/129] Remove GDPR reqs (#1046)

This document is now on the Prebid.org GDrive at https://docs.google.com/document/d/1g0zAYc_EfqyilKD8N2qQ47uz0hdahY-t8vfb-vxZL5w/edit#heading=h.8zebax5ncz0t
---
 .../PrebidServerJava_GDPR_Requirements.pdf     | Bin 241252 -> 0 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 docs/developers/PrebidServerJava_GDPR_Requirements.pdf

diff --git a/docs/developers/PrebidServerJava_GDPR_Requirements.pdf b/docs/developers/PrebidServerJava_GDPR_Requirements.pdf
deleted file mode 100644
index 33792ab1d4cca271e52c09006396d39c7f7fc5a1..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 241252
zcmc$_b97~2w=Ejmc6Myr6&n@XuGmh+wry9Eif!ArZTseTzVDoO-n|d^ulLqoyUjM{
z9&3&oT10x)qf0(1Zj@&IOb0D~xijg^Cu6~G_|-~ccve>*dBvM~Yp
z`C&|KjQ?2?@c-on!_4yEHlplqX98eQS2QvG-iC>dnUgty{X0`J3o9oRhwstKz{y0!
z#K_k8+v{JWf`NmzBj6tpgl(;yt!*3u-~I{?CdL*V5hLD--UlXRb02a3IJQM*8ing{+-iIFJ>hcOeQDWfS1i!qlWE4wkPu@O59BO@#S|GS6IKO8tZIhYt&!?6v;QVl
z=6^{0&qw|*RiXsoWc)8Jas8u){|2$X{x`t-7$*9r`bdffNcz7)7&FrRz=w_9%Xa(z=xTvUtRd?pQ$U
zaRZ?lzvoC}n7G+00YGk{>*4QsG$?OSEp!*dT)a%1DLc41$NQ|!&MLgz-+O>W*Saq#
zPJdc@3@3oHY_lTbyP%`;A32)l#y^6$on}fbQ9Tl#)4{X&*}q*^SWwSaDNGT+SiXL%
z7Rstc9xJQ~K_skPY^+?(Cm<)!cyt2IBAqa&iz(jrp?Kc5z7mt!=Ea2AeVA8tAO{HD0fs?i-q77u-!vJu0
z#Bd|xWT@=qb1GTs5IS%05o7LysGGIoNNRhqKa)LwShRgC=gu9ItkzYgl?11BIPhwY
z*zhJ|JwJPN!~{ODj7of-@DFyeFR%<7gL?u03U=%!b`KJTk70j%0ieq3hYMZ>BT_V2r75Y3G_%cpoj?rUJQY1OTJ(LpAdce9E
zU#rm^5xw6Xa18LdZbmnxn+4Fh4juf#z3eQD&&u+GRWQe#PZbFYVIiY%2#oeDIy{+U
zfUttDA@o=XN%b)}uAKoZh=Gd!-0x>36nXuZq_+BggXX4CmF6ZSb_X7CF45lFhIWHX
z@lQ#ZXv@`^9r|;1Ovcg;uF+(6ZJ@_Me*{=oR)Iv}B3(m$;$@@lNl-#tMy5shyaAwm
za~G&?FE#O>JmxBt3*w~Qgux83HOop^xCA_atu2cb1PIA)`)|6AzU@`fR3W73A=iyWZ?9hY
zL?O?VlKkaVzKyrWHc#KeR`n+5bc*c)4a_rHq@2dJaI7caJpy^o6%aE9q`r+JZL{XJ8-xQZ3u(QX^iHnM3|@st227;su%l9v^zA(0B!=TQz#4pQ(6Jd
zt!wTvHnclPalgLld--s(fnH>=gnas)o)ts8LuS7W1N5EOwF=`5hl)fv*B-E5Sk`dr
zqGCMJ85%koY}K3f(TfHdMer3IE*=F;u9KDUjVYO+$P4RVcnkKNyCWI0%~T!pF0N-W
zc4HedrN8|#sl$eT#aLCw=dkrmVYc*v|7KyF2BOk3H*1A@^(Qx>If#pm`~l-{hReGm
zc!4%XMMVbIE8P#KuZdgqLat)0z$3Hps+
zK|D0Q`$P6IlBrIIOf>83k;MUmffx_#38E`Fxi2nmbyw-aelg-}d-tWaZDiyZg>&Gwl|Se2=QubwW3J
zJ~FaQ=Z5m$X^&<&?FQu2<>-A?{Dv9V8_-B+>;N@EeaOVa9kvva-mhKE;{gQKPP*WW
z_#oPrJ~WBHX12hOdl#fcwrB3uv=t?5x;HO?$Io`PUM-4RNlas{OY~+kyJ;$se4Ys`
zu}#ECI~j7!(B9cmFC^C^g#3~T=h7i~t#U(<$3)0n5${>45X`oGn4vu_COdjUxVY_8
zn8JeeX5Q?F(#c+#R8q)9tz@veK_Q}33i44wKCgfI(R5c&odxPq=GMpGur%j
znaQ+D5Utz#`foEd9V;`k$Zk?&q8-PoNHL#q2Nl*4a1zEA!T$4f-O5v~nuN661_F#f
zzmjC<|AczBNue#wv@w*yi7FEYK#X>(GOwdr#>I96mF3A*Ayg+}Wvx|ApTi>G8v%OL
zXeZD68ED;RrorT-ipf?JiIBF<;_I&Z^57NF)f6tJN-Op5Crari(#(Tm@vyW`E`~7c
z?ZmKBWN4qngj3A;P2e2dgY|z69#oz3f=lWp3J?ES@}&7NSzlPpSov8lG7dt#GDe0Z
zGEA6+nTzv0LaGdLRebk&Yhy{{AGVkp5!JMi>~Fi;!iPN65PtN#IO#@b>YZ}qMc*r&
zM`-br_t8*M-E}Xz4nNx-F-m3N)Dqw9YQ2?=D7L-8Qmn#B&}wbP#JP~FvoI1&v!O6}
zp(77Sm`UC$;|L#OsSkVgrSqw4O%bTo@bWxy7WELq_p;GrTU{IeyZVh{QbnNtpHvp%06#|A{fCiH+
z=*%y~zeKNLRK$H&dufuG`^`x$DukkW?lA4cFO3sj
z*3oV_;+ETTQPwP~EJpbP@m_1NE|_}{W&@Ax0-UTWYWcdraYr15Q3qo0L}!VAkw)9-
z{iAGhGQ%>|(JoB6^P;_v(<4vKIuk1;!5c362{zg{oaWc!XMO+86WX5wyw*3D4g3K*
zeKArZYxn9=VF&fN+0HJcA9mP%Gv&-1GeEH1eX)WuSwLOl$Yma_;)rF=WwvWNCb(-h
z#+8mpi#zCN=^M3+^zp&osIaURHwA5}g-pO+_8@u&pjgk7`Oxsi(Gj&T;Veyg!BW=M
zv!48!W4|gT@)$m&VLyw}#0odS9N)8;H-2bK9Vw?LDd`|vYw8?OGBnWS>%0Zf?Di2W
zm>U4Tm<0rz4b(Z#RYFQ_T!{3vtFzAx0(z9u;PdOVof+Yns_Q7w=W>BIyw#v;r4!F_
zSe*79b@mv8(_%Q%AUMFTR)$kAJO?zaO*(5MtK3kLkW&FFdI%z*)#pI=k8}kxb)I*`
z(;C@Ph9etr`jg_3qh1U_3Cb$CpV{ooRq&}P(8$P7sTAseId>cVCD|=FzLkkn+
z)!f`s=f%jpo~H++6yi6Q4-Ua7XnGT2Qqn`P)`|InDLO`AodQ6SAq$!vv@6{V%5g%>T51{~PVt_hAVK3p?k3b
zvI1lAK?3zj{Yat29tXg}3g=Ep;U&49Tv7aMqqy~dz%yTR<^$++*(-qY1;q#8n|=;q
zBiZ*OV}jwYLr8ozopxN3=Ca(;9lalUh155lsxDM&HC?KzzzYI^kf3OO3uZU0%)VNj
zzrO)Ro9YqKk#
zbYOLn^>jde-(Oq)#f$oBPmbB@1Z`5xvjM0|s0FAdVk^;S=pBMU`oix{l?zdzIkb?&
zPnc!s%}l#m$`R?2NIT$}_e8055hEWD(f}cc1~}2+i*5K<@S}Zd#D_EIC)}fo;RmGE
z5@0jxu06CX(yjUu17z4!V7KJ2FXt_^m>bH0?nJh@W{NAeccw95ss+lVIT-egJP=%B
z=!q%*DIvg)i|cq{?-{ILeCM^ue-qq!c#o!vWga?5Am|KbjRwgV>cH&>$d!9PFj1CG
zKoBcRcF|gUo!wVPk~axnpxI0Iho{hsN*Xs&q->YsQ
zN?^@B605XY-UojW4O>o+41&kK#r&N1`krJNLe)~h6LjL8v}xjHdrxybO$d}NSz`E>4SU+gb-YOd%L=)Tn>Y2ecJ-u__o}XMu+@5;uO{oPhrq)BJXS2x8bV`^d59VwN(6Fdg4u&;qu6Z0JXJR
zg24a0qQ)1p7bXzSo4iG`s%>VTg$K?x4oK~bg5h3
z_M=99O}^QL?$LW(@kd;m60_d-n2X+blKg`se8h-^{wKGPup2gqXJLh~Q$nmUf(zv6yn%0GI_VLMt__aB7u*46U
zBi$@MVYEAb^>|=p^892pOebbSPAQsN264rv~p5mz6%C%Wi
z#L!*765$PHIQg(yt?WI1QVqM)r;RzR#3P@%^2T
zQSXbeXmj;zw_VroNQ-^G=4!erJ&=KZ`pJ;qEIcdi7DczJ+@W%0X>roiEPUQC@qwYQe8JtQ}nN
zZSX)tbaJX60e0v3_cLV6KEJU?4nnsi?{;Kgo6(}Y^WMjkRQ+|*LHkx1$4Ly_O57j`
zhz0t*Y0=tef7#{CgZh#S>~UcOZ?Pbb8@Fv|9{a-LNvMf!1reXvjBkmS_AS^6r6Z25
zxNweb_VLXdRSmhPYwh65!Wb89UKwp6^FiKHd5s))r>t)EdVBH7greMKQZ;8H%~E`d;@aSL*LZsz1aZkcy8p1VFgNW_BY2A&d?S8Ll7z;hk;-mGc*EFsLwxKCX6tNeUr{Fn<8(zK67jo>
zQ?C|oCfIQagoetUHxp3YzmeAjuSR12TZ0sIWwKR@fB4p%<8NgHNzhjI5=9Gz2hMbHYxrj%$k%H^$kq4B`hkh39aN`MRt!6VYu
z6h)j>I}wj$9E2MmPp+YFOy(QjO!O}-o8`6Wtk-RoZq-{Qy-3WC`y}?xTd8;{IFz19
z>*0X^)Idlo*za+u%_Kh>c6aFH6{C+I8a1gE)}^;iCX8z$vxp~-&rgU?n2mRcHzm`_
za1x&_7-7}Oz)Q!=#!JG>v63Q4&rIWGT8Xz}QL3BCOoS<8Wush3#Yw}-;HG3FW212v
zvHWSNbQ(U7tE5&^C99M3s(;!zk6-qtm?P`W^zbm>r1%y8`c$7^A76iC!Dg{o|5_hJ
z!CG>1VqkWn@p4{TCu%m7jqlocey)7QRh{DN2#gy9!5&{SOmr(h-#u39hlu^mW=CSV
zt=cF_tQg9w)2e!!e3^KeL=c~y=uwDRgo+b$h&{2EJ{O<{w@C>(g6t4UTL8hV0NQsT
zqyh@70Ggoy8iP39Jzn@LzV!nKN*KPM3{jO$Awiph74Q`fQT)O-oz=?pVBZCyD-7=y
zii5Sc^qbDQ2ihS$GkFqa{>R|TOsQXdU6C1u9lV>s!B&DB$w-4S_e;SOR9VkCYPg4F
zz>oQu$8|BQwHCw&(?`>LgbUfBNR#_uH_-NJ&#EW&tF|M4ZrA}Z~hj(
z=9=&0xyERRmlp!{0zKR2bHn@ui@FB)0_W%h`P(OC9!zfT7q$IcbQ(s^P7t>_51)G*
zj5e5TI*j5{v6&z{7}9`?wlA|4r8BVFfT1N}U6j&5gcyet_GoxxpTL2k1#wcCVFm@i
zX9#-b$0bD<1)_eWfCL=rZ?Jg4Ez$~@KzEEWi+idSQB17(h|#`%ZITsZc$4wjb?u>`4mFt2pJ1gnPh=Z|cX
zw-Ml5UU$F#SamlTUef6P1c7)=Nf2xilo9<~6nAW&VBg-YZ6!luW=Z0aXL66Ab}3zw
z>I4o(%nr$M(xAe4)>{nsjCSEoGGF4W9)9M)!h}3h(C9w1eeY)={@|`|y&XopAb~#d
zeYD`$rqr=6k}rVSFyg-WE$SxSmn8FuhC_5FMF%PD9j}5Am5FjbPI(b`c?ks0k6H`B
zrg?w`6nv2wj#!@sSQIH2Yq(f~cD%TeIc-$I4XYnQewY&@+t0MBB1zUXIFi0HY&|C`
z)}-PYBGxQ~B5c-#s$!11iE{(?bo5%Lb7s|kdNU&pCK`jiWjEE)QFX42?kauM6;copZbJX@z5Zb7}Rc)^7d9Wo+8&_Zd^I
zSvWVnrQ(V%*}xfpY|`)Sf^XcT>57Q_z3~_FQ-VJr>o2Ts;8yqD-*6vU`PsR9T#xY2
zfb6`jIl|+nd!FZPFFC}&c5WD#ehFAKh8{g(J$>3xSE4-zN1p1UOJUW_+C)OaLc>F`goBLrh3Hr)vbJL;Q@mWNc$`v!>Z`
z#CeH9r_pO*bZgyiPqs^^;q|bJ?U!xKL*u1$Q}x#dzU!v*6W`Nj)6VCXz~7sm5O!-H
zw=t3!{>Nw4#)~kDoLj4{8poweKKZBfji>QCUpJdCzazcpy$FU5uj?c=JZkPk<8T*<
zvyz~?7dJbON@04}xZ;nW?1RrnfZaw8S9mkb<}X7aq2ZtSfT@s7nMmd%WtbR|OUWZm
z&oP!QY+~9HGM=Ry_DpATNWJ2yxSaq_Ulbs=(ay&Y?RObkeS{;q$pYI
zly|OJEjfjZluX6M%w3{vLHi8&&Q0FD7~rQ^6Dh8BIumAM%(I2?C$7m1|5$U)C~!4>
zJeS)s%S5**M5Fu}#~`iQCXK>$IzSU4Mri#-9mW`0~7Eyny_M$9>;6k__<6RwC&XOO8q
zXX?pZ@y22PfvvO|craHMS+H%MVE#)1;8a>p0NP
zxY89b;~?99fsVn*%QoqCp?uy~)-T_@!Q;@|K_&u}4~AgtQMUmZ$ASBnBnNl3sPeUN
z4N%R|XY;SraCc%U8VwK?ru6Q9N{?M$0VZ2z2=_wecX=A#mMZZQaWynPk?d`zMH%JM
z^52DpUv`QyPpmm)<((Ggso6|vpld1c=7>lPr4f1Eia3?2Y)ut3`rXT5<2*V#sN=VR
zRq6E7F|MMO8tmz9kFy~seL9r~Z2S?AZcmkx*D~r_K^f+xMzz3$L46Lb;MqdUF?KM5
z{=Bp$#N*=n3PMfi&2(LbkQFcat6V0p*XZq?mbZ2@^l7E@z&%pdSOxR$FPS!XleYuy
z9qvchOj0~Xsk3sD_?=#vaW-NZIVdKS$t_R=P-DoX+Fo~)r6MNjMDg-#^(_1bZpS{Y
z&{XU@?Xp7svSh%Vc>F3nD&q1{GypxKCPK*m?|?QBr&L%cCto(fVf67R-O_0k%5!Q
za5?H+V<$QTBO_gg
zt`^Hus<)hU+IT?Hk>xO*nn`7wTFJO(6rJ3-Zb&&gzyzBw@SG_r)rS4RETXLMWuC9t
zfV_lih%|XjF&VdfIC-*yub8M=y;i+wot0-9$%^urKmW`}d8tEmsY7OoPj1OOt;#d8
z%CoSFM}8@*;%su>x#YAJ)j96fuHz?RKH@Co@qdLXp`QiGY2E@gX`nvT#m<+aG-{XgY4l45j^ylZ?sXX)MyPzQs^fp|
zd9&3IuqTmi4ON27K4jc=4|Lm4Y-t$QBGi2Tlv6*|(%u@sz->;|j+U2GPqX?XYz2hB{A*IKG&D%Pt)ZdBZ0Lb+F$xmB0>IBm}Rpx?H29?`*e1+ji}
zl>NAKqoj|XV8w{jwvH8#(BTG?^VU#2uMPA@i4_NTMsk6y5MJ4<9l9a9*X2j+tu{VY
zIIkUxM|q4BUmzlJ3E9E<*mr7MyM{CzyD<6u(ms{X&}_@=O<=0)Rf>e1cFC&3v!yao
zSxz}RgN38Lx*<^DcKllx>dJgbasPx3M@?jDG&N7lydVOj5LhmQ8SW4Ju#~$7p6-oA
zp%2?HZGi;4{eiawR_%GQY5S~R-S@^Z+9keR>3@4idmhs8EVkmGbD>X)(mB9$L)Qhd
zMWZK#**U)yr}JE;>(B5OvWXdzeKyEP4ffV*bK%U8==H$Vah
zXM`^9=1n|7PJAHal$m8K+KeHBmeMD$T8D1xC3lNj7qhO9vN5eTH}$-F*rgtpM6_T8
zK9@M~M`*@;vWMBaM1C&+j2w`{_2@eLSQ+bj!U7@D$4eB(y*w0k!`JJ}>5(^PFB^I~
zqFto6D1@-Bcf}mdgz96e(1A`cV4v@P_>0HpB4AaLvAnv8>FL`)K6ITBrHI*$wf?0&
zzd-a%(|Nmhjn5w_L&_4CWkqUsGoUS@Hal@I?ln}jXNI=c?@|}DJan#QkzcR=+QvSe
zaanoI?t$q^czyZgCxuOdk%MZV~6kfW7U(O!9pmjBwws955=naR`xG~UKwJa>gNTR&ts1PCx~9|A>1=l
zyabA{;;=$|6vNj~W1KX(!+3~Y@~nN1ebrgnOCsFRkaH1>Y!ZgJE|gF7J=aofWfu6t
zym^rX3yg_kE+-i4+zNA!4Kdd_vzt41?-(Bm#JQ>KiFcMx9g>@b@!4e&7qZSP{N2jV
zNNZ8=&dF#=lvM_0sg&A6+@g@8lES9K>cSj0zjQ6OGd9;>{BvH#;FUi;TU^o`e|m;A
zioapv&&e(=oF$(mpZOH51q@|tbiupmJ6R27J-+4LZ3{l5x+6r#)6vG;kj)*F?8~9_
z8X6F_#k3cc{m!sJeuy1mP5%JZyS_jgARsZ@hC9zg`G_@^iiwT7rWKpjY>lN)78+TY
zHJWwbP2Le@LoCn*xQYjELJ&lpkf$H~R^ktf-4ZlBx$i`WK#?pXZMHgnk|
zh?50HGD2#D>jBf*dkjS~@E2%rpLeCuehzX3#l;_@2hSnfM}~f`IgcRbHp!SGt5=s|
zi0L+4)VB-}RUbRw>;BB+9{Qd&89Z_iu}sQlzDYtDC&Mf{7y1b~%fueGFbDNIF1iK7
z58lRzg3zOKj-D!RGjuEFFBZ&*8vP}_}VR5@n#YT=7Qp|F2BW2iMZOZz>YaZG#|Ds%j_YF9z_&mO`*y1>wyh~;!YVb(K1lcQqiKrf}mt3toS*Sy;rXf
z&jKU)X3h)e)p8uJtiPaYHk`9lXyY5X`G9N1Q`Nc*)HzB2e1ISBYy?{Pa=+l_~C$qhk=7%nzG
z*SvxhwMCIGZaNAxOKjo3AiUF^-g#`VtY0f<0vc2j^9V^|CPG-UvqgXq4N*UqkT5lh
zxSE~2PieMSytMPITk$I?S`IBgwr;AH_qyBX^Iu*?9i1%b1yZpJHj0ucJmlBl=qh1wU!LmQAQ7lJD`>p%I?GNOb>F>gsdbiSP)
zGxjJLy6RX2O3#+$YEh0qolQ;2PsM~bo|90^q-Uwqi6jfeqPNox=XfnTN;fy_B
zO}PLx#qtwu=2}QHKx61@QAt1VaxXN7gsp~@&P$my0g90-JV;zrJcI}#J+~WxVGacy
z5zQVzj>02k3>sFbK?|}##ANNn4JOh|lC~xR4eu&0ik)9T(*tOMX4=0Mcs|XGYcqta
zu8x`**EW2Q?mtvD!-!koTYkpC1reth5ydyToo2k9ddc5Ss9@zvBa)6{04b+#pMEZv
zGoq!QC9;d6D-S+>hjJA3=}}J?;1wH6DwsPs8~^kA5`sd=JaE|d=YWHB0+**2!2H^Mw7nCf3WnK=h@&c}SV{9pBa)~gp^MSYg}cDM
zgUSQ&3Rx}2wOcsl_-<5IzsZM1P>)$Z>T_B`(e}?IMEFS#(%P6k$D%L!sk3;Uj;#U{
zT^_4>zY+~rDf@>b{glY&6rD(%=Vg@c3y#4)@I0Iq_%7xd5IlLUx<1qXa@y+%kg2B!=|?w~
z73+O$i7S{2P7O6QJp%dQc3IC|0Dh0+HH#CKDU{UXMQ9l*pRt}Km<>vCua``#qahKw
zdIQLy<-`8oQH)UoRE5lCcrQr=b3_DS(v4TujckzmNxI(Cs@wL)Anj2ktMG;AV#&~$6b)$3=sTGtt$^U^~#!}`Vr*4`hw80N}fX&o*z
z61CZA?Kj6K9NCxNCo|v-f73S(sNA2
zRl#TBHZUr)kXsg(X{Y0HQ%NOI9|W3;p-YJ7;TXj{IgL22zXPiL%9jTUSsqOtFy9jk
zqatKsab(HkA&iXvsOE_D_EAbl<{22Q$4v)B4#tYyV@JwaM^ce8(&7ujIq>#R#gyFe
zv6hj7!(mk!7^K#zM%c047CI*Z$Ipc!Z*XvJJ73}r5=YL;>;c2g+j?{TSxxCn#zQK)
zabjDfNCM)f8oe_JYOzEr)Ag6Dg^Cg4g{`Q-zG;IwINC+RK*mOmvP4jG#uL3IWKyZl
zv45hW!{wM-vDItmqn*^K+36llzjaxA)@?gP?5b1tx<5?bRMIMLxAPImn2%$z!AbkN
zFMmi_?1*?v_4nyln9{i3xa^Kp;KlZACp_;@zv*K_h~x3+Ahnh-y|9v8&+>Bxll0T>
zVUE7+=MGa!B62|0LT%%+lm7dCrUOxhV2mwDB{_09BN&f|HjI++FrqF^N>1h>23Cl~
z%({t|HXi^-)jgXi6tO2;~r$m5=KcfZS-=&
z<>-^sA5<9
znB%A8EJIVd{BBhAcy3FS2&m!|@-k)IHB)gUFwZsEKf9j|%GMc%)dGfeHkIQsyKWo%
z*I+i&JB_X%&kPk=!WX~jt&BZd+_4*~$*e8;scRHj
z7I|pF%i37W8I849JU9{K*e_|%=X5d&CR~)A0~FxD&kOIa~~YWOy=dIy>?PMu(cXb{sqdtYr;
z`G8&PH$|R@8mC=ojf|r}L_X)Wrj~*LO
zQ|bMvi6czw@Y(xtf6mJ+ih@tV>Ju)S%=<RgmB+Gx}0b#3~N@l5qND!7jux%%{QtXd!0*C>~!9+MKp@2GbrJQXzOq|KxHJ-4r{Dlv=vL{jCKR-<@_@4f
zZm0Ob0}`@*WjJmw?`qjD@Vw!CsT%+;!bfu;03h%h7!Fz2f?PvQ0K&xt{mwB@7F-+D
zKdx#Iy=z1ctxfWZPd)io39{tyF|__?xAZQx2xKYj6H+DfX^Y6N+L#N(2R-#)D{{ht!fSANS36`qK@
zU(CAWddk}Vf=nS`1pXhdsaGo6y)AlB>%%WYILeuK0u0wsGq$Ss=P^;pZpH#x;*WFg
ziMd_c16&`Be&P|I72m0;LGec4h3ORT4pS_WUxNwy)`dHI3@nCyEXtlYRY_j26z*nD
z>43;1du9K$pFE=SGYkEE`hKp9`r4}JC^6iTKgU6POaANC=XL36Ev0j-)Bkk_@}U6n
z)GP&%qhl&aQST3b+5@8c$HWcJ0D~3AiTpP4s_LXf5=-P%Ms!U6CgIZvix{hghLJ)1ODXKjg(#GhDE&Y~W
zfpbe?nsHsSf5;=yU!_ej-#0iniC6LcT?<-UWs97lbVpY^KgoO6DlPlN=|ovDXItV*
zWX%Vn8+zBuAM2rI9km|Z!-SUys90A?4;>|2Tv;pXylB)1-d&tk+FF(Pi_PogVIL$$
zTBtg_JsK)jolALYU|-p1PV360-K-K~FH^jGF}di!bgg!6*X?|~c{7_Qg;X>n%6%)Y
zEi@iHb0$ywSK;&3(zg6aTo!^eizVOj`T6_&OhBW*S5xJ@AE*^wmbyXEIG!%Q+@k-p
zNNTPrnn|^}Ol(Tcb8@xe(q%Tn*{R+jQx_ZjR&2H0)N2kR;_p_8#^svo7s6SP?N~1)2JA*_;8sfz8_v7xQ7z5Qq!HB^p)CEv
z*z0W^^C~T^p&yxHCo$X6FZ2TM?BYMV6!i<#`Gs>`uv@EMJ|S35P_mp0Y;JH=nobcf
z{9)efT0w|x5qPHwbVvE^+@`-5+&X_DecB?&%c6MxLRmfZB~jAYNO5;R#EU9|I~h)y
z;}mtG{K@77bo80|`dSBhFF&Y$jO(e`AFMQiJ|UDJM-UsPTw{2lx~E&b@%2SrhB@tdCc;E^Bx=eUJZo!JpXSc`I
zsOgPQJUf{mHWnH_mSzH77j0_qzhv!C(!qUm^td3d&yXJVDF3VoWIQ{#5*C44B=OFX
zvrfKnygn5SFKo4CELc?^>xGr8E(pBJ1^MycSG&v~5hnC+GBjph=?+f`XLxujyA-U8
zR`!&O`*pr?-hFWOTl_3JTyU|sJ^TI1(dBFFO(;M{ZhiV;yiIkv=URi0ZvC)P
zTB6t%`_CQGBZzUzAa6-nSW_In2QnXL)8o4tunl@&Lo7goR_n9NsZf!#yxLDka
z3(BINn)fB?IBnGp40PEs`W*M|Wo2*SJcSF^$fo|bSWUWU>68@=SpPbd$;_%9^H2D@
zrOU9iqWu`0OzWOGXJZjxDPdBxr(f4+k$}6FPDkgGqSeT*nTb(>OVPaUQX%cap{Z$%
zcb16%P>Zi-;k@{w;L5Sp<$Z~u6|rU`Y)M0GJvoV|QQALaYcoj1-?JL5<@9|78|{In
z6soYbI3HLVi=JZZvQ7M$+{&J!Wwmf{X;u5M1jZ2^QElkRzNQ5hG*^SPqN(8X3zvSL
zn6#o!3kT1uyQuADuw5h&vRUwK_6_-B0
zbZs(;Yz<@2G~HS@i}`XQNX%xJH~m6ZsKaK#R{PXLHS_1Mgf;C(ZqbZJQETmGcABLw
z?W=A&67luKHEUaoZ7tW;=}Q|m42}d?74mnejEQ83F%(bO+Qybjjqq7JkF1S_RpXO=
z>FX7`Mg+BrB6LyVcy6>AYghZh%=U$o?^hgcxcK$(3#_bzEAuKxb`vRD=ww~aQn@&@
zc&@H9XZjBAM_elAdSuy@iBvV=<4kE244gWICRGoWj`~dag?xO}fycqHSu>%Lqf|^i|K4M&wamp)Pb3
zYN@9Ae=v5AF}g(Ef^OTkZQHipecFB6wol`2+qP}ncAvIwTXViUlgUi(pPTGd>R0Vl
z)lT-RTI+e%s6*fy>*)BJ)B7nhxad)!QDNhQ#NiP(TqA8AtNm&nGHD&%7-Jl)L^K_q
zHWuF8satEz`1IB3>6qm+_V&5OH6bcT`;_I`Zbu)ISbp3hoFu)(DcaO;1i3G5KB;WC
z$}N)Frm$6qJ#7xJ*V@||TNh5wBaY6uA5yB;HTIo>2Qh@3Pee+W)r`88=bO@7jhYHc
zVgB98cooNKxA4eXv%WDA<_0i@?};#$Qh$H(Ucb;bfz~HNbKEsn8{CcY=Z34^d+_Lc
zzEeRBi4v2m9--foy{>+qoS(|yvJI{g7axF1{{nq?2#3V|VwG|+9LngFkROm*gE_eI
zha1zqKiG?X!gTxb3r+z;^!tt`p^V^v{fqEC>5*5wV1YgbhyOGQ|95cYKeB-T0XXuH
z8{zo>IFVoR(^kPuXn+^bh|VmAkk=iAFr_REy~ogXPaydk4y8n-gMn6ixhenzTF<7l
z!T@x2rZp)5{QX~mI6pu{#>rJV%oI!@Ni3ZOggJG6_+xXPonA4FmXEhM<<
z(()UyNUF)djr{(b3x`;eYwP^4W2ePeG^z|NayiW|S6fl4La!St$Wx2+;FjL%(0gSfMohs!Ly^u}^)M6CX*aa83{
zZ_tggwXhUljtp&gd&>`bixkIsUHi=FAe#4Z0<8gkj~
z>7zRQ!s++>dh^mdgH3+pvBXkECLjf)L#P-SCc?(dK3-&vGrpKEkWT!$q2te=vEvb%
zV!D*Tz%U}Jf`Ya`e|WET!*%^3rEq|W
zQV{(;&g&v41Q#;+a8M=}e|JnEXKFnT)4~Bo%EL?m*+{
z{)9y%#^UqE+1QnW+zS3V!yJM0amjk;-%{0zIS6thz(AsTcBYBBboe|JO;iTQ9m
zd94Lfsnb)1^|^iw^5^IHnaBVh$=Xq$Uq$!h@Xub2Tl9TV@A9?QymC;5Ri~7zs27N*
zH7v71KLxRscd*tZ+FByXGWbEpy~V2shG=KP63a6TM6&?xbK$B1QbV>AX}nNY5DfUP
z0<#H2n9xcic@6<-0D51rWy%%}0~g!?MyX?$@e!ufrJCCC~X|6!wI*Gu0D9A7O(PbcR{CWdsp7eqorY
z9${OD^RQy{W;mrA+XS?j$+V5QQ5Q(UcGbMO{*p5W8-~=S9!8^
z?!tP
zxW?H4H!VrmU+-6QfO-O7Vt&x!Hd=4hHe&ce5cc8tO9mP7;cfSoI_tz|OZrBL<8ODM
z>${_&UlClFJ40d@ynU>%=b%&Dn>|cs{fx@tg;^1t(SdVW5S_V^;hN(mB}~515J?76
zkFbqIlthzISA@kM6VMpivHJ*?K{M49#3cATuzREvibVP4WP{
zapt{$F!4a}%3
zSez(hGWDjulH*~d43IoK-mu-#E8%K;~{JdE;zQ1&*QVWx6&=o
z^LTb=I%N{3lyeoqEcLz&w4sd158Y5n&L+-brEdyMet@o941LCRLD(-(o=AHjpY8L!
zb;&A7?vUqpbRON?(cI8m>|8veoz$0If4BP{a>Q&%*`2gymsk38c31miZ-qEbUENxq
z;{LsOWg-~j_Cx>c+cem=?Hed8{;@k5(cMoUNZuwMA9AONY^O{`_BDt3ZA|C(1`hd^kN%LNVW)L_dA?g^fsd(o@iG*B2?l8
zwveG(5qx;`JKhAaYpdXT{=v4p2P_3z;cjUksI>V}4_Lj@{Krrrg%)qaL^y<`K7jmZU^5n;+U|T!=TEcbIPF)?<^;+Fb;FMFJE1Iz&isAe_l7yCA+5
zo=tZ1b>z2aq8jR%)X|$X#+hcn99QSh(6{lI@sDA*E(C9k+bN{3Ker(kdAPq|Hjr`8
zPUbXD;Z(;l*qAQz_}?Jv8KSap|H!$McI*-CWXVag8BOo057|UD3WS+ucsBg$}(MH|wOZau}%Vsq(3
zbRxcm3u){1=cj@DCMP<6p)l_Ne7BQF92Zsvu%2>{A996eges;6kv>~@Ot!p;ntu=-
z_Bd!9akg>438!yjYmDC~6C{7}ok^hLH7Dj=L6?MbIi_B+)J>|ZyXlT95dbpg!m
zzq~WLKMChBabT>ExC7`++>6nFInIjBfn`;y+BF6j3iv{}V)tK6Ts_q#iT?cswgpSd
zeLk64_$9G9vH~}wiKog?tDD>5+V1V^;OF;DP{4SNej`k?Kp>^NGVk?7uze8Ei&yf=B=iBTCm4@OUnNH)xJ97qF?ltiNIrhgBiEFS}#d@YKWU}{f
ztESmBh*kPHZw0o?7u#A#w=(F4XNEcGCHign30q`O^eg(wzt7D%o&Y|11)=ZJbck5)
zC+sW~863+XU@4>tc_5RrV(lE;$&;JllF)`)6)DK
z3vZ}cZ@%h$>DdyBZ<Rb9kqS;Z#nCG4fWzK5};(Yl3y^W2`<
z=H2npsoBvwwCEw;#k`GTAL%|aI!rlCIVy-G$mCZR>Lo|eirKYP`H<0ttRKoblDTVp
zXnQ<;CvZvsH1ovp$Z#icv~{?(SADDDiRc;YxpS9u=Xd<|H}&A*WBoP4H^_I;r?56c
z6JJwTuXH6-W6`Rdr;w+#L*4b_yz9JX8L-S+fvH(rh42@yV!HZ^T?w@c8sZJrYL&{_
zj&x02AyId%{a~zJ5xneVQ}1w-WzSP0Ex^^}YYnC)qS`ETjkYSmccLcZXkYlw_s%QL
z*$!Z!{P|B|bxq@Ho8*$99+5+Vn!`a%2>;Wd*U@2q@R
z!*mjH5IUBX_zgRAwDRu??uG8U{3i;;Cj#O#&Hv_A+9|6PJH~6DsepE>F>=LlnladF
z>F{;EIk#LkTb``2wt8P_5t&JLngQ>X=-0!J$}2s!7LmCH@x^2x2t#=zOBazygl5NO
z=1-WkA>y86ibJ#bGk?YePcb;i1i~X+nZanqu+(MnpfG)ufoMH*o55~vrLl5Zwidji
z%HC*o{FhIkwSfYE_k!e4Ekkh5xj{~-*4vS2>WTjM#Uy=dso}Xg*!uFMS*p1Y_t`?3
ztSY0X@7C#kgRCw?bw}Iz{&8d^FhkGKhJHjB*EfmIab!8>7v>@(g`S0f{uUq$b`6t-
zp~yh0JI;mpTjUWC27CCac;e6J|Mbau(z{zk_`EoAOnR`-_P_hAdAOQs<=%Ps
z8F+hrxPD+bK|Y~BNiA8KU-bUu+VScB&{kO-%U%{s`Ps_O{$nOx%9{Yfhv;mSGe@Y=-^xUo(r@g6&@upfN|HpZ
zd(x-sfbSPjd<#bxT<8Go7xGjG?-yjS2VoO}f6IXvn#}|SF$jYNQff$*^(XO`pbPY7
z!15I#q#x1&VmnmRkn0mnu}|`rI7?jni@+~foC!u^zyd31;RDJhxSok%q#ukbK$;2J
z%nL>jAgSC@I$**Cdi1Hj3f5>yku+fQ3j7TYIYx62moG*X6{ZBwZpyAL`ZvKm9|J^8
zI7tj#xFLp5@^2!%MI;9@`#ci;eEjsGXB)JC9OhW$A^APann+86tts!Cuv>!W-0$SX
zt)jro0Kq*I6$y;Qe+<@|qCR=Dm?GREP&tW&G0AV^fXQ3(`UEJaI6M*#V@laE>qA$k
zG(2MaxI(f7E|061crX|TkfpT$XjS1VLytM
zamF^yFBv~l&N2CW$hX8#al3d^6?rQudgwTbwpd{DB$WUb8WnJL6?An~cuTovHOZ$X
zq39*C=_QeBwm`Og4l7D(vSbMr$mjwE9Ql7lmg_th%{=*%{DHJ;f&@QVHiZ)7)4bUF
zABy=9wseM~NU=nV5<%ET0QUf&Q9zxP77ZO{4vGehb?}M9iI4IsjSZ$7OlR=cfWY3L
z+lCuIP^UfZO`p^jpAf@on^Nw}bqv_AB69#J{+A9{17REAd;sQMVM|A%Wi~tCVfB
zz9RzWCu`u(;GJU(^^=#f&*&c+zl?V3?AP2cX+Cnk`ut3Pj8|3_FU{SuzXtrwdh2m7
z-S64k2kaUIS932J-6GWo?aK1Zim)$(zHTXUVxIf-xgcT(aqEC=l{ND_;}~5~y`Z}R
z0wMu(qNmWki8rUn9H;QPnbR`C
zBKftqadV1P#?EVG9Dr$)#5Es|WO_r`H4=>xj7IWHCn>3lq!~Rsuk>+GNv%B
zgQp43!ki>VhdTDj+`B0IFJx`vmYCX=4=AY1{o?%({1-@NPfg-@L{AuzbOIaoDHi{AFV=mOhXehfl;Id+&nI@n}UmiHHt&li((~YJsX8wqe0}
zAZhVluT--of*7#vu3yDw+*$z;SBs&03y+hMH~AVwINTUi_9%Ty3xEU
zFI+q-oy^Pcc{NeU=jj7+6xh1eM|||Q1+#u6-nj*c#!XIa3E~f$=wB?^Orz5bXt`%P
zifB2K@x-zu>&4ZgyRvgBvXL%^hCrvh2=NGkyVO)-fTKG!NlSFx?7APb)Wq|rtap~(
z2n26Um4G$7!XqWmp`+C$O18h$aPThA38L0l
z9Hhc`pnEIlzTlsl4!DO4i=3KEqs_(E%8d1J%(b~)a?fiVc)lP5Q~TExXfCvO9aB#v
zykQ`Zhc9FPG+e8`h`kk;@yg)t=DpvEI7~
z1yH3z$Z{ouAa1K-Acp)FUfZRs3dt1-teyznjfkStY^7&lZQgzB@@+pAC#=78Z@L=J
zvHs~`H8
zAR_eSt^21zY}mOvhV5!wKn!A1)?cqCN=h+Qashu;O@N)WWM<@6(KNmomX*1XInTuO
zR!Vr8C>79cpV0gnrFi+oDbBC5Lv73VNpaook++0L2FP#ELW>gqB+7NX=da^7es=AWw(3zsZ%z)
z<*_GE?9Bv}YHyRg&A3eRF1X7nYEtY5r^mWouC-RlIZI+k=vee27zLsr^22VbY&e|)
zGR}s3CwAlMVg^o~1RJX8YyaG1PQXy!lwsk=VD5*?njc79&Cew#H@`?-O-4*YXh6t9
zdXgoUUIOvU#HK`BYwA&QCHGeh&mUc-qLI`}#9>71diPO@4a|lSiTvS$Q$_lP42m>~
zjEe-*PB4odnFipBYGX)!1&l>dE`B9TVN9CNmW3`J9`-3U7{q67E|xNh@w&s#cOm3k
z(Tgl+aKM^zTA~(&M~f`&%hMjI6)N&BtjE-Rd_ch*Fp)iCVL=CQtx~P#;R%C_bB!`;
zJ(ImIaNuEJ3l1B)+`#Y=gx&l_2$u_mMt5
zN9ZcR*W)Il3wY}h{H--g)u~@z65sCUrL54GNA-~9lf;wAX##zeJUnN6M#zd+-
zGU;9VOgI5hMMeJI?M~MCg`naUk#tQ~OkT7kDbroV7LX)EwrC)|g^s^qwxB@at>fG+
z;$=|DMmjSejptgZ)v0`iXyO5ZoTTbkMK&BtBiYUsy|c)~nPj&>Vrz2&d78O;E3X&A
zxrY+C2htsG{7B(;PvK@b#DvKVcq`voc3P}VoK!Sau{2D!xlEO
z@rdX=3@;9*+)6Gz&GZ;PI4ZI8uc97FvhfnqA`p#YSXf4PnU|}3Q0*D6VmYw1{ULl8
z{Fw{tXwG0LoFvA;G!{=|sC}j7s7a`*xy6sNwk4hY6PdphZ*z;xxIjl0zc@`SF>J{i
zmcJ2SqCWIE@_Pivps;ucWbLUd
zc#;hIG2Xp83YmI>-jk4=#^#Kw)AE{DT?>gF1?9)V@1adXP96ycEF79sgFxzD=9Kr)znuMEne_CrB?r56ws^Q}kPb+QTP9SxNSrv-%5UHnl1QNaiDt!dAtczQ5GW@K?
z7=T(;su84FbD~~(pT(Z+NIwf(~YJu?y9gR
z>zRG8;gp<)Rka@xUx`FHG^TDGrdqS`aYIABM-zEoNtYmrGPdJtEaLi@dU1ZrXp(*S
zb7pRld1%^DYPPrQ_lc);X~~JEtk>gl4<+e&MkKw?-^S!4bc>FN!xy+1AUX-lFZwV6
z>3v_cMgQ?7>L{qcxsce3``&z^UDuiGZ=o+P>p$t16W|H(zMi#D@f_8PJRh}6`9aJ+
zdYH+$tG}N84BeO0_YowdO^i(*AMM!j@8!c7W63e7(*?JMnZHUpBdA!;>%x%igxy$`
zKhPN;*jTtq!~Qk=iuuuW3b?oxE0w%07^%i>Jr*xh?HcPt^xh*Gol3brrD-77GaayL
zEQKdi%u-{4#=DX@27Jrh25F;GI);P}947pn2bo>nZl-9mAm^EQ4iex`I?vB;RjV1>
zG4~eVW4LsF)_=DR@X^Od$8{*4_mKp(H8~ZYwBfE^s4tK(
zVMlyKLaZhx92rnqpyq;p?3dS~=TQ
z+%N5iox{?t3TAf*=PTca^usGgvlf6vHLO+|l5+64#hB@XS9L&u;~BJc3Z^i*5#8h6
zCt3uwtWG}k-|sS4p@WJuf&3Lgw^X<;wW~L%bG~I;b&Qg%Utw1Q{4!t-b>gpW7
z4y^S=(f5-3&!u}zqr@~m_j4Eu_3-t#!`t_#4oR#0gCR31=IdR$nSj-}j8(fr`lJqD
ztS-MuWTyQp3eAI0*4#9z!{GH0a2&*aZt3_S?UDGm%f)2I4i53&GFpm3Pa7X;_bgkI
zearB|+_wlv`czgC{E=YCNw&sBO~KL?Fybi3aGWx_6l)g#Hc{fs`(sEz9ROCeaJ#(f
zP&g#B7#TG5;L*Thzy+W_k>y}#LhYy!8y%l%jrh0MfVn_(_gtZSNJ!>UK=nV%?HTW}
z^jXJM8npc&x-lg^bA7~nM)6PGRpcE_ieYf3a~t-L@=@pVeKKka&2PPOzaziN#bEA4
z8s_xIkYNDzT$SJhFkR2k1}v1kgMniwY>vro!v8e&n6dXSmJ_3nvi_~dZ|fn=n4m~n
zSX$_1P?zw2&>VWAn{>BO$w7^TVJlR6H|kW5ox61N#5Y+*#FpYb#O4rj%BFmtXx!YX
zRpg8l&i(*A5e8oozY*>W%P77Ril%>oPUrq|E<*9a{EJt@b@9n5y)d-}c%dOG4$KRz
z_<)j80h78Gc@pdmi~*DZG$#P3D^PB5UyJXd-qcMHw~wO!>5}(3dsWd#@>3#TIZAm-
zQ;o(?n6f09#b8ZdyGXBQv-VR;eJZzLK)~baSrh-Tfl1eb@-oPbEn9pq{D?p41ei(e
zR8Cpt45J}#hREA8@U^Uf+*Z1y&y{aoP(F4M{*l6#+JypN8WC&wr
zzHJ0yue&>xU$9~>KK;=gfIt)Wc~dg3D?%fSu^liAkQE!}==Urkjz7fl)gF_i_K0pq
zf$gKO3epU|GljS;yaHldt{|rx{WZqX1j{dt^QH<_u(APOOr`pYbKGa~POT@(uMagF
zM{L7I7v#c%-xHLoM4K=qi3X3(;gk?#YPKZv3}UAH)@x1yErvvD$Uu$qm&UIox1Wq_
zkj+7PHK0*Y)Bu^M@xs`h1%%xYVk-2me~n3<&X6!+k0@??*DQ6uLmqP>4fl+$iRNpr
z2E3sI;3#jv)3vO0LLARuoU*zg9d
zLv&|XwvK`ej{sT#I(nXf4oYE^ptX;)4zDH7Gho#4pJ_`_bnBX|rPO@rxqqZ7%1^?3
zPTjqR(P={#?-o?QR><3eoFJIx8T#?h=5E67DAgAH+(1o$@M*4of9EH(q=yg}$XNh`
zi+f!+6T;04iVJKHUKA+~7P1dk#{jX1GWCFvnUlw#&>ws<60mQ2pL+N|4amkI`6j6Xd
zKBEJCc~gPK6psx6;=3&b-Y|XYQRWKx#;#agJs;qIUTJ_37lIjxU+Z#*T!G-{((P{<
zgivN^-*o>_DDH$@5EtQX!ED_WooFBl3k*AU`RSi{z#11Acd(=
z57DY|iDd?6R1wSf#oKhGt7a|^yO5ST+aeqqZ>j|eFRHb!**+T!{Sl5P!z;ggVEp&H
z3+kuU=nKH_5}^ecv@}`({>^mLeK%U~eZ;;BsK#Ms^_f&$BPrJTA_CA1vpwd~7H*m2znl
zQ&LXGy+*^P*b05-)JAgUa=@68qk#S#H+HcCx)Ulg6OPb7GF4##51&tM8Xx<)ea0}L
zf@@}4)a3r+bW)0D7KH*)Du-IqCaXd)skO@>ubpCKrGwq|TEVeFE)6}6b7wEemcTbH
zQQm;F?wX#^ZB(HP%khiC3Cbq@dTB|uV+typfmQuv#x&kCLgszo+b2G3MM@6c7r5YT
zhCklXJdsw~xtC(#H)C{E6jHts`FIsk2HVtDrc27HNre=K#;;%VT|=Yoh4rST@1H#?
zxx1A`TVEp~&tqe4T+$O0GP1HV1Ai6Yt_&l?q!jGd)hd@QRQeH%Zv`0C$(P@jjS~~<
zOSCDhDo7@Ugd09b^y(|QWc3(Wm}x$SjMHbuvun4o{Nc-|_HWg_y-6Tp6Kl8HAlf;Z
zek2qa5apUR4|@w}f6JOagNP;`f`?#_M!i~QoLlfN|K#zin#`@!u#xB>O1Ea%I@cm(f(cdu&GvU<@Yg_=_z)%H#rgBFp-FEUHV41e~R&HSn1b2
z|N3XT1Bic>cw1k+yOIZ$6gd=Uq3{*266J{h3JzE4#HaL6~oji^|wZ
zs@X#AEo=`gwN2wNPPKxDlC%0-%D8ezq9?H^?@3>~tbb6I#14D#WYwG}eGj)-KS$z5
zc_v+R$R(AUF%mx_5LHZOD5n_hh%|2FI8@c2rVgkNsfXW}%D@eh2$$a{S`G>bINcBX
zHPlzsJR}^90$Sz`t$0q-C>`IdE#Z}cpE`2jK*#i9x9lw|%C!22XPkjWd
zR2|{Ft$xg!7*Y+EyX)kIc8mZIRK5LS}7%gjq3nT0Cf
zx~^Zp$X&w{o?>}CH0NG)5dPToWx-bML=c-t%35*hVtP2|!ovo5RJchM-Y9*EF3i5N
zEwHR$<&AkSKqZ5T#_h!6oVOraw~K9-cIzZlx5SaOV2k#ZrdXgGUly5@n2mjUWNe9P
zv}=N)*62;vVH{#`N0KDJx`++8I?5m#PFSs~D*DEK66>wE^p}=SDr6b0MUa(3ZiUda
z)1ws$yrYftKGL~nui1x4f-5FA4+ub2jlmOU)qA`lgi@^qS)oT>W+qLfHM5`=Pl(4Z
zS8|#rYrye3bd4qxL)JIgbJ+`B?8)NwTA>Zv$nvyX-s_7MTjM%6w0g-eRviV?OI(L<
z>m3w|4Rt}d^mkfq811~;$?sNhmVoV{h7Oe9_Vr-
zb>c-h&BffQH?nnZD4Cr=07!-u=sXTrlPGWZ`uO37fgt-A5KX0w!HA4gf$xNKNh)be
zS&Ty4a>8bgWkwyn?0^ayF)o5`SujsTLSH{vqeO$rVH8i8R*;=}8wLpjmh3g`m>wbf
z$)o~_E|~;d#je|)DKsTEH=UfhFvDXr+Sg<>sqPxmq1v17yJvh6r*kqqav`6{=dO_6
z60!W=yIS7A1{%~M4Pb}Bi#kVE*z@4!)T)`0>&o8X*l@uH%ngI#Mf?kiXN2H+>3;Zn
zmO+nyhg=W9lV)~Au!ed~a0dsIik=bcmZHZF8*&6GwTMrWi
z&gBi<iE-|^~&4~8F%o_zp3dE!(X`p_G
z^P?vDzz$F{3aXRM8#tW1Fmjoi`#`Ay>5jOlM~5``F5!N)3&G%Sf`A>O7#C+1!AZ`t
zIcTxJh!7G^dacz(-rP7x|BOi=CT*Lkud}ZeiC`I4gQ(S1lK$|zWm!eW2u9{l7M833
z!?47_mctfiRs2_YLz{PvAj%4NYjJ({FJp!Gqs5WMmEpx;%e?aNyy_X+u;~@s0m!h`
z5zV6X=*eT&e%5o>{Fw_g^FZ;6WJ=kWonoUJJ8(1fR|~r1G0N_BIIuIh3pZ7jgQGpj
zT2d5>I-5W`Yh+_2T68GM*gj!kT?XAI$SOURUc;}tjJB3cc?;mA2>rrYyDUBC`suR9
zOo5E|p@8beQu{3)=l3A-M>?a!9(>Seu)thMkN{5i8oy*dOpoJ}3sW%cr^T^1A9@Dj
zpo(}-9I886kQXw^Mr{}FUxmiJSjs^nMS(hGJTeNFhwZwr_`rMb2TS38E~3L7x-#hD
zWbfiM5|))x24jMJu>NI
z1lH{Q*U2A>@VgI_{ZAVEpL)01f`!GaDvnBgRa|9!`8?Q#mwB{HPi#?t;hE(chrRXbti33BvXJAbgmle$9W
z&^lR=N*Yl&}usp1C4oV0%
z*GzdGHxjyG*zi6M3Cw2$zu<$yuV9&hchIw`NWc&5xG_$@iE5=}SY!GrpM_?J}
z7UkkF8yH~JQ@x9KOC*6^_gKFnYA})Cow^3Ij>Tr+Wj(8jmK}j
za*TbJO+vSxF!5F>!4STB@x7P)&*etIad?LwPIgmoY>R>;vOh%Fdw%1U(X!-2!#Y*X{|nRW0^Y!&ep!>Z6NC3v$$?
zLm=|{buSR!SeDSaa%$Evsnj@n`HHdTQP3y{T(hbrkh7e8s|Rn&DvYS+o#`f4*?Szf
zrJKymP#94r=kJb}!jt${tHnOABvjYJ`5ms5n8V&hH-cl}W)9*{i5$}Xy^}osT(|jU
z>DMxIGx>LTl*l%VzB3Qt(Pyi$Fnz`mtIdSkQQ^kmdtH7o`T~)W2+RocVNKw}TnDXg
zVs>SLhGQ4HwJ%#XDfnu_%Tqd%GF;QpW82cI3tqb|3sk>3GB~KVhn|-0pPseTuz*cxi=*`qN_eRv`OB1d_ix>Y+XHWRhO@?^{=a6A
zjq947Grs#!%JO<%2Uj{ORy4XkHV^9&HSKQilQDgVegT<;Ssn%ZEIZ3Qq@1>8v-KW4
zwYQWpF(jTcz4VMUis#m6)X^9rRH0=O+96V?>WRk3<;SC0s=lV2w95G}6fy+k)9f3Y
zeMc)N7t|3dY}~wm9uShX=sOCAJv4&}UN-1lZk|15KnD(b>5W_67__nL
zz$$2GZ^#7WP%mCl(+Juol-nlacLUVg4rgubD5akfo)LQNU3W?&&ed2zf5)BK6j*h+
z1==O}=^PqUk;<<6EZbb^ZTWmHjJZ#bR;PIAT}NPzkB5^LJ1l73J7PPKOY=Bz3t?Va
zHPk~}8asBP8H1*Z-_USZQJl;0Y}(5(Fu#+d0lCaaQ;@Q2Bv3w=a?4E23l85$1f%G7
zYnnW4l-Y_edeu_6P<2d4={7{F)BviLweUnpn2q>4(*1K?&!^c9W7zjOEZcsujg>hf
z`g^~QAr_9(Y2mcJt(ga8D#B$5dJT2`ODHkg%o~ZvuIai72wa#E^H@HWMduo}-rt}JX#ia3?_w9l^TL)%qy7-nq-r4F7Y#S$CI=%7tuD$u#uzD|hc*#sE7-Wiv*Vr3E70cNB
zyxL^Iar2th^RPc8N!y~3TOa<&B=23cSfx06hSx2Lx?GwXW@)=?%{T3Xx%x0+l)u*a
z5nHW0z#cUKtk(E8MJXsdeJ?8q22hbl=aw5Ih6ynTppAh~_>Rd}+*oWM@3M3m5=v2yZroX}|gS3NHv`D#qyoxr0$VGn3mB2j3Q&RgyuB
z+*M1YLiY~|%CR`*3L?E*REgo!;Gm(z6m@s*No?1S%%Iz|Mach2SvP+}lomDk7UbQt
z{MTrZX(cz1mQcQww^VXbcqV_|rO7TY;8~nA={MFr92<{Jv8ka9Tc&U>ny_BmLgy!e
zdlxUI0%|HMA>&co7Q^X{m)N49lUiVTZ}7}9mEsmF#TzuL;N(%7)se~8G@hq=ZJL<@
z<)_b|V1k7^v#*UoBRvy1wQ+0-0dfZK#o!=qMmcKj3ofLmQQq9$cwYtKkVK6f3Q|GH@vI2tIFMR7y;W?3|K20&g}(ZQ#c(pk^T1EFR)iH@35^-e*eVVTNl&g@K`
ztejRqR3+I^?eq%1vi<@zt&7;`B1PeSRKko(3QhSv^+CI$$UYjNPY{x!k#!YV3XWFZ3kst+FH}Ok~j5vQk6G{bZl?ezEZvFuCII<{#uEKX11hmO3GG%<2&1mCPzsCtF_*B(2Y%
zB&gKTrLPHrPfQ@9_zTdBaBiQIK@{$jR%<68JK1P!V
zl#cl}HvX$_`o4P#crcaoH`wT*_o~rDS*6xMC%{_&DSjLhEgp2o5t~PoqBs?->59
z?}tI&WfM=!iW1Z>vxt*#qnEddvtn%78G2yquMcs8>)4Rg|cuL=ODzCbs@MI1(
zFOw`nUXd0-U4W+mscyl}6Rl55W%CTxxy(RS0FdIW$6|6QGu|AkVeS<6TirwwYz(I(
zi+`5l<*`B&AG%P0B~+e|bgrfJx4q$QbTF1qwN%W?!CaMZ$n~hV?3|h9+cY!Z{Z&7K
zv02GX}<=Rcnd~L9v
zKo9B;|2@WDWQM(kr>y{C^YF~N(dr=9QUGWfsQ&BH4g(L#x4x%;p^e
zuRb-m6N`)2%rOCVgapBZ*eI7tfiW0h#{7QW$59|T^^H$IRNybXTj6qaGHF!;it1g#
zio9_X6)Ey>I6bRYTof=kqEBNHzfrZ9J(VDxg-z=oQQ6D*2@163V8M#$IwrLwxyHHh
z7;OR%|AW#1c<-0E&j(y6&xO`Wev6+~Ps!4@Mhv9)p{a{%i)#cGZ%>hN;Dw(>=OWXa
z+lf-u(PX!4j@F9-_)!r56oe_V-#rP8dDlaeEbM`~_R2@qEx+X)%Gq(*Z>-BCiqtAa
z%;J({#Ffi)PRlu(^6n$mt0JAq&%%Qx3yr_H_^q{1r5JExN3FEmBkYdHAgz;_T+Gbi
zNyk0FI69FD;0(BA?D6?^Dv>I!3<`Yhn@hc|cuh`19jzK2H9+{fV<%CMW~ZM`w>ZT4#P(h{yLaz9{eGy@JG|DMJ45^h$R%>QVDP?gh2tM*Ei{+06EqNAVJnBPA2wX6@;D4;DGDNHrR
zGbbsLS^rR%ro)gG7p^MOeNJn4k+@p{7UxFja$Vc#Z)+5AEQa#U-OS?MmgAi#-4~b(
z#BM+g21`A$H$P#Y1FsZD=L_B5?6-qy-!B<2u-ryG7tNgEpm!F(?Z5%r^
zbJ(qaShwbpeJ;Hrx7g-#2LfQ(;wy&z1XWj@vHQ?S0R1^=3iDA>TnBImO%
zGY)RVi1E@$q5pM@+0I*xCPyWJY9Kfh!&u*4Bu|~v3S=y2%?6{Pq^SeV7tr4p=|k)X
z1`!sb{cT(tFP26^Qb1Z*h|O9LWn09NVH>?^&t->1LW)1H7V(tXsnaR(o(*ms&=Fgn
z>iMVH{t!sg-aPkoINrL9dV(AZBUQup;f^zFW=;k6o5X4-H6{S1WA{WL7^nWpS4$+xK%^Hr=amTi8t79h}JL%ZA
zZQHhO+jhR#wt2Je=>F@U+|e2A!8vQ!-s`NYdh2-!YeJXy-p>1gy*{Qw(#6DPql=>v
z5z_F&|7m2wDE}tH&HG&6Q{*Hi@8d5K!f!^DZ{Z%ZA4$;LbFh%8CP
z)z2~GIzML9XbLwNkGQ~YooX(P)1U;=vu!%E5ngMSxB?)WNIqZnHZd6)IAGK)XYf>7Il;=+%~2RM3yWQHu0o!
z!CI-JW_}sKY=wfxI6;S;Yth`Jka-*x6Y9z}%>+JV&h&4aXRuQ^5v87%pqYQ{@9SUp
z(+lEf)fqnQ1R^c&!1_{qt3vQYRpxX;nVrB#P&Rh?Y;_;!|*yrjEu4TEM
znjHdkvi#YjpWF-O3|vO&$BNx5+Ij9h36v|(-kuhnj`h2sw8_AS6N(*D49$|75&|D4
z%bsImx5FM`yhpr+YiLkGzMKMr`YMG}M^oAB+G24RR8Dd)Pylpv^m$(T2zh_k=$qvN
zL{x-8yXgJMeuZtp8ZgUcw`fc1mbH|mJnLOje3}l_hUP_=t((1>4PzEdK#^{}3Z^;7
zbx*b~ve4^AKTCX4?Nen;Wc-`85Mh6NnWHN5hf)&ggt|=)sWx@oKcpC1xtLuQCW)5|
zALFR^r?4fbd-lNY&~g@VWK|J+&qOHqiWQE}G_%eLdZFPp%Cge9M$_pVxWPTf{9*#P
z3+R#VW8ELwnXUWl?a8v0ewu%3rbo{&@a6pX#7gp|&>$kTu|eF8)s|lF=Fvl$f9Dd@
zpyhG&dWGr6wk2-BI>-e(HEB9gl$*=Bs2C-`3^a(+Ph0!dlWC$
z=R-kTLh4C84-&M7ibPQHg;_%IDK1thq^SJULXeBzN4erja;}B!waU$UbLAo3;W|?T
zw62ooH|?3rRp%piiocAhavM4(@ct)OPMt4mscd;_mJP1+23LAra{1GRz>?v<)ShyI
z0_5T(KHdn$A|js#$y%7Ac)HUbFguBRwvKTw1p|}xp
zpoZWqFEr-gHO2TWGG=N%E|R~Wa~^)0`_WrH4#RDq`pQ#R^dPJ=^u%x1LDj6NAJJV6L%d6E|#GAys1YLw(v`dm4U%AF-F^3zHH59@S
zPf!bdDC8Oa735_~U%CnfY>Z07VT-8vJgfpPJW(l?tN!Yx|L)0t!!|_7HRRmf|IBE(!v`9ruL*XI68HJR2XZ=WCw2$YNNII5njpA$1)Use0#!nCBc|;O169pw^{;JP9>{AF
zJed!E!FZ_`y^CdnW-HEJ_op=H1xl4iw!Uia4I4;@pjfR?5rj|p=hzh
zjBbog`#MwCZmz*$-ZLR4*9z_5gU5#)FG!JWvXK#
zy$*zE^D;*Dcro9M_VsdyjgN-*N+^YGn5cZE5@=OAL3}*|_PYA1PsmVd}=lQ-!SmNaef1Ie2Ni5LDQ0kS`&uF}0gdC)bl@017(Oj5e5MYz~COl%#29$7%
zslGrCts)t8q;~pHm_($!Xe8GD0A_a@3~SNK(KNR9LCdo%#h2s&E3*qsnXpC$N(D{C
za6+^Rt1PJ7IK^tkYR3&sMrp>R6-r&^oYS)9e0n+Me9V~*o+H)e)mYt4EPLNRf$2B+
zpQw#w2}`E4=V4=o?3$G-v|HFq7T{UR1*Fi(P35dA6=3F#kmi18kMXjhv!`H6jsm%h
zkc;Mkm>$e06mU6nLrKik#Zs$$
zD~qiO%-ZvFQDxX3_abIl?an;pH&hY!Qw69#iVw#5jRvkI1OFb)~BSds24<5EWqPz+qY
zNRZlSqqAYta~$tlZp#uhSf^l5x{1^AvWu-G_SV)l(mi==d8RiP(bNPiPR{*;6728a
zW8IpvEWYyBho|nffxj9)YB?CRxP|4g6t*k@N+7O+*
zFQahWTq=CtOosMrMl!86rIWp%)c+OM1IBy!G{@d$VIxUA4!bj64OYL+FQtG0<<-L1
zUKtPB%_T$YG@3IJog>bcoYB&~&}&f94MKvj@`h_F`kKuo07kH1Mo^lBMLEGmi9nQ6
zQVm>&z>>v(jmMdwj80?WNk0SG5{nIwO49rm@A#j)$pM6oS+72OCtU^aSMJRo(_P1W
zw@F+A#%Yp)Rv
zOJ~VCUybS@NC%SGI}V?e
zGwx}xE;aShw9=@<1%}cIN+HhUfMoD2@ZvU!O*`ZX${%u^@!9bixvhIr@vMDyesA*#
zBlCngjk!ggrRolNuqv*gwVJl*I{%>)(n-}ctauE5=iRoRw;uZdzE9o?juSeOzZiSO
zb}eXFqcytMkdA5^X^yyG_T7Ary5pnlqtlQv63;;?6YM>W13F$(fhU3)I6gAqG6~_T
znafQ;VI9B%raE!2hfNrcN2Xd)8&(teYvci~a`8*^4TD%$CB+yYB>)nX`x}f$bYBeh
zz7O#2zoH^RMKo$X$k=m=*_aYn>Ma%gnXk_&y6e4Qs-7k{ulVq=d^7lXpZA|t&pmPD
zo^h``j2~U9DjWAt86!GA*d(^MtCUYHwq595xY^`bb=&L{d=pKR-IGnr-OG;@_Z07;
zW+9ye7K<4?^X_ER)yI&LUd2kn>69#xfWC2DnJ<%*F+0c&!>BHfYylW+T_0@JJDEZ0sghE1F+|unakExCM|j#!;v543c^;|#f}x#KanT(}dt2t8L^d*3wJ
z0GmvK(|A>0CVTzwA{HK@7{{pS7hdKb%<
zyUgM0w)MJtO|*|)xUP9XYSug<90KUdr7kkl|cWAUQ;*m_0f
zvq-jsw_qDZ+o1`V4)<+R3+TWr@LfWRJv30E(HvNATAoswPH#r-$>%PAS7R^U|8=F|
zR5_i1uT{yYGGX@br?=0&PttTdX5!xz`WKz$cOFEU3gXXXH#sDabWE4?{X6LSc8AmD
zv-wjL)eHjc*UcI>8=u_>!WHlL;(ltE$C&i+$CB3Y(=zt=&D(8@4rnGVnAs%c^f++l
zC)vi8ST~L_IAFLXj2f2F88qj&0Yq1%Aq{g;Gs0^P&T(gZSu@#;(zdi_giEnam{pWX
z8NL4bt*9q!)MYWJ=T&x
zwsGXs%1Ld@ZQ(q5n$JDa>tO2(uCGmX4KLxSTHEDXm&-SVZiYth8;D1E@85Lb3Knc<
z!T4`mEwj}9--A4II!Sqh^2#Vo(hgdBspo2C9qY43FOqK#(QWOezTc#;xwpMf-%1R6w!Rklc|IPZGO|C%
z9*MITHSbs2T-cHALbX4r6fE`KVW@j{PV2=+0yApy({hmQ5fh=I&w)d$
z+G^Og+(#yyI#|10IY)DHlJEaA#ny^_g$?Uh1!^Gw9fEc^sd=$1*mw%X
ziPRHYpP|lh4r1Jw8X@EUk3~50N4Z1D)c#o%yrvORi9#(H?KCkeBL?9@hl9CV`vPG8
zhxPFcx$f-g^T2f!M$1mqaXmbT?l*D;hL|DnJm`2bY^zz(s=PsWg%%TvA7LVli}b0V
z)2i;NCB;P;9Tu+}*d{r`SV>XKu19hMLJrjq6mue1ad8xFOU|+T%O%+0cZu!D4)zG`
zyop$9VF=OKC>ef;rPR~HKVpobEMj8fjbpKx?3rE3X?)0Cd=I7uF!njV_1S@4wA!{;
zsaE$d_-vO`H=PkX*bx9C8zzQrEeEyDY@9bHSk{dB8nivaSJIp*wD`OV=o5B2TouIf
zP93+m0t5X-tYeOIob*Sr_u_`2B|nvz*u*jjFFvv+2e!EYLWd;j2<#=EO2?L=A@j4<{Y&l2dr=z<-?cC8H&R!i
zyM;GtSNwtWkDP20d+NysU;^af?k_}VkHA?wKj}g8K1k=mj++F?xPv?F18Gl5DLY2&
zV4Q0BeUN)=Py~v3k84Z+qQNf$G9YJs5dK2cmnj35&*pzRBVb#n{-!?8B1#VGdhX6Y
zbGs-|Nx>bOyglG3vorrrmEp4CrSz3`#21ebr+$Z`r=$A=JUmd{0gAan;tSd}-i4fX
zATPz>jtizW2^-!Gs_;-}QQpk+06Zb*71)X`o7E@8kVkm(#v|LwTyzR2>zWLdul;N-
zR&Hxektk)N=(BNVIH<|{#p1X`mWcK@z?>nTeEKD3LlaCdXQL|i4KR7H(mu-(<
zw+xi5va0Oq0dQws&gJf^GvxSCkbkJvUHQK`z8Pnl`VV1e-d_2dh3}0dd=*J)^PXxu?6uvQp(icX*}qu=D724&
zT6}+0z<(*I)wL6dsV7B4x(4+ckMuJr$t&ni>(G%HAMhhjWU
z9)zs$UUC0Tgl$0s+*E3rXT~89Q
zWGFNoGVx=UEa=!LmHNFm+qI)IP!h(d&zS5B$6HrhmwbCi8#IbJ`-@~~01@N}YVNYk
ztUUL2hN{wvU&2FCGt{zuRFduzG4(#_dc2a0@=k?(SSezmMQ#*c8w
zrPka?%w9ytJ;LL6c!
zt_!Re_TFudQFR$nw~S=~iZ;*8X=s{bisD@*I0xx_ynUc3g*~Pn2Dh
zh0K-vCxn64m7UcnZ+_?b%jjp^^X5lFbiylnBIA(ypz?SUC!AISPfCZN4y}**3lBXp
zcBSzRPG|!iN1wWwvD_^q)P#bX2DMc(2W8adqjVRo?<{-!xwq}gE&J>FqFzfdS;zTP
zWh|!y4$D^C;<&8fQZlX#g5#!}}B0C;Rpb>W+ms*>pCL=h$e7He?1@2sJ
zevqAI$LRf+zu8>&jb*C7tM{<7e6@M+a1uSt#pccJJD~u4#zvaA%ffc2OgeK;xBi!^
z!RFIsmE89863r92P0!~UfA@1AKa$0sGee3FSpS
zFv7B8Rc&x)7-`3-5KbF}AA_x`*Z}Y81H+k-p(UKece^{IVm=}AxihTl&nBnPCzFkX
zAm&)6DA4LC>5a&dPO7p-1~5{gV4-Xir&&1E_lDNG!@X(h)k=(jj*u4h;!N!`D#|BO
zvTqxuKaP_qu-~gfUkA`xw*|RUehyCNa6~I>C}>Q@?mwbmnszj*>io`{>s+y4fy-cH
z1M7Z&8>nI~`N@&jqUdXyuh@$BGM2fsSle2<>aC1E9S?~iMh++R<(TF?>Z-Z#FFnM101Ahs25qQ?M_`}0p(zN2^y<&R
zFmdu1vPi}DWLnk^VAPkbYBnxdb(>~Del7jBf4i7>oaK=Rf?`^0E#gwbc^MfyO**A>
zTsmA@TqY;n`g(9xe;trC5#fQ##Ui7-4R0mY?X7#p`x~wV#}-(X_-Me0OsuFghDdZs5d9y
zV|6FQ@2nVm-d`3;J@6Ham<{c0V7K-ynJBY^AJ!Up02Bocl`Ikl&gX>`4))Pq6d(%d
zyjma!H16~5Pp7at%4-pEuzgx3Jgrg9y?oW;c4lU!UFBrMzHBD>{8;fGN{ZT?OVi*R
zUXg5ZlktxX;{%1Srt<2o?e(H+wGC?Nbq%}cBsC#@p?v;uQBz9XTXu6^A_6l1pms#g6`AiR?MoV2b2n|6!r}AWMq*CI?=ub1*<2!EmK>r
zs3otnovc4*HS6uVYisz3b6C3dJ(=uwsu|ZN=P@akwM{CRSJ+95MKu$C;=YBfS`C}r
zG)y)G|J04_JGb;4^IWvMe^fs!>07&4t#p-B}Ly8N+23d@y8WK=WRe?Y64
zvy87q8d;YXB@XK&H*8WD=gB(YAkScc3f!{Pmw~)x){`(xvK4Gs|KaaVhS#o|4?&$z
zsUQJ05Lw2$btbN`(bB&YC&e9@z9z8);DsWcKP*u*`C?O2u*SHidMb_e}lLc
zK)?P@^+daRqVI4YEW|)PDyl>NH88^CYzelcfdc5OL*5sISEb#+)q4Ym>SiAJcFhah>W->qE6>>EBzP2jwpwfJ}pyHb|yM
zL*~ZAAdn0OV-R<+GIvfy3^^=>ti!IS_|M+apJ3qxtZXUR3Pze86NXeu__qzTRIWID
zV9-#%vAdzA%4%gTJ7Z84!z}o+OpRHMc5XYkQp87@=hK6T->;=4D=g3^GqENK91u7@
zSDCKC57ds^|5qrtL#larfn$
z_>cU@+I@uiYg`|;sBvAcVb9lGkVvKz
z9F-UrQB|VOb#{@%d0O2y!yo}h>0yY<80GKy
zZ}={}VGnxTT)uS-(%-7vb2NdaoKp|Qeucd0>f!Egf!fQ)0>hESqyI_tr<`U9Ln=rq
z1Ty~T|Br!lXiQk#HIzU^dQ{0QIW=IF2_ylGKK3Pqs*sw+irv5vK4T@FJ!2--%>he-
zU9S^nS0(X}fg$A-Q3*_Fs4Pt<((wby=H@(nhd1={X^EqOQUj3Ty+zVPEAHCFe82Bg
z%03eBt%*|O2~_+JUj-50EI_C*0Jf96j~SE5>j)
z*~J@wpa6r{l@wM~YwGfLxb65KWv)iLs%-X;B(HKh*y2y1ODTMFv~vq*MCn|{??#bT
zw@k%$8oX+^I}@V^htP(v0T>M=P&{}LG#h`Vg!NqQ&OS~_sY~8FjIk|Mn;u0uKPS&W
z7FLvlQamt7=ldI(t6cU);3l}~l8Q#qys7V(GT}}sm*~IGmU=JDV#cbc@pq~_&=;hH7*=k
z-GNKu$~61_T~}&Gc97|pZN#vFfQVG(eY0{ebQuOilhp|Xw?Kp8G!7<&%nKs2H4QBg
z+bR-zT9)eTBbM0NSj&_pdu1gFy<4wPT)Ar>6aTlKqmQ)p|Yyrnn6{D9q(LiQydi8qS!B
z9%s}LVq2>5QtP>|+3LCmFy@`)Ne^X~${FjQ6Ca2$=k^VM!HPxf5ea%}5Lpv;LC9q9JIAt?A=>
zNdH^Z=dP3wDC{F@2Q&%=kY1TI8i8QXayc|XV9STNRX}6}BJM^)WXxgRbG}8K9&Aq~
z7h%Cp2On;;qzZ5Wq~VO^qpD;B+9cdu4I61^{{jEJMJ;4`t(`?J=E7M$K!jyPy|NDp
zxvsOI4}xV0bLAf~M`es@ZJ4%|hfu&&>AJ+0e=sN7o3Ued2?Ijy1=C+Og41d(=yOwd
z%AiLS>%!X7Iq@K6tg>23VpJ8g!v2jy9;zrMwW2ua3Kj*uBvGo$`Ttn|-$g{fANnNg
zY~5r*t(`)9TJ$Fs^n@WzwJbCMxVa62;vzMXKV-3>Z(8CMPs14|$iYn0Jj*TN?uEQ>
zh~+x~r8@UU5hYu2mlteV?ySdKL33E*bofFoxj=6*M86bN?;O^p$Y*JRi5&@cER2~1|3+|IRX|3#
z;EifTvFTr2nJg&M_T6rjaYsS?`Nll|5V4^JsHQP?Uv06=bdWTt^9o
zNauyS!jeF4_eO;^wEV`8++}kuY)ZFceFqR2+}MQ0;$qny)@nWWyeJacdxTjVWF;B-
z=*!}+cjKQ_N7mev=0F{x$|gUWq_IKZ+)8-BC-JT^|JDo5$&G6xsW%mx(K
zZutUfgiE+xnIvmUnfQ*8_c@^+8C#n-i#M{zrdyFFw&gi22#R6={mvA|S;zy8G2}_j
z<_`x~z)drcr#5bnP2`nMrfiN=)@j~Fc`J-YIZ8@Va=#wRtB~(94<#)3+{~?MrErVF
zi>uGf{QMF{(@RM*ctkADDkBj;e})(ZS)vM#NQeq;q0zH>WNrPO0^#v5Zu+I>qlkOc
zDM&e@*NVhz&fgZOs>St*GmU)ek^yC4{;HsP0*OO`q=GvMrzRj>IZuGZD^J;&7ebZS
z07ulfu2Z*UimVLsFv}-fUdf_{YOiN*?oeZ=zoe-Lt*up1GdIPtF{7v}vq;@$(UM9O
znn3phvxH^O?5$HWogu_lvP%#~(g>rt*VWNfNcJIQt
zYx}xElD!v}6}jQA{zs{U4v1}R;l`YZgULw3(ucaYhFA`~(3jsWTfsh;($FRlEqC6#
ze!AmtIM%!xWnRP;M+eCT+^ojr)z&N0R_&rD(Zl*ES`u>UYQV11~F4+R<8kKu?`O?Jb
zGePOH^l$y@*4D#Hc+zzyLE7|Rjq6o<+(oD5J
zr7`-+lbUu^HaifmI*alah3)gZl~b0^5VMaEpv2*C*x4@qFPigw;uxq=R=6_
zS%oNbGXlc(6hWENbUV6%wm>7`#8R$}S)_FNi3Jz0JMb(#W#|c2{t962;u5#2C1hC(
ze}7+`SuPnjlx|?9%ZzJy)Rq=Q)kIFVOI&uV!!e;md&R2e{?hEq>b)DH)iPDpS}jO5
zrY+G6o?QN8oR${L1s>OC#|51)nu>thDjQ^@*t`9!zXT02#{pcU^sut1hX+59AJtm;
z@rJOdI1l1RW;A4ROT;Rk&$I^OEBHj0H48wW_(HMd>0^~mFl$MbCA9CEH|=jH1BmCN
zEyB{s5<)j~RuOF23G*yxHjC#AQ-r1oI3Mg3XbpE&8I*mwe(VT7y7p@}-$^gV@}Dga
zp5|SVr<1=7JG;&NZlHyg$Nn>g{f=)&`d)ct@xh_UqT$4_$Vobp8F3dU9n&L4^%IQ6yZ*awQ08i4lf=Y~8RJMTKZ+VQFpw?G
zRO|@%And+h)`)Z&PC`7#f+GHd@=A=q3V-;q%T<$bq_8tBqQ|s|5iuxJB1%&r>bjH!
zJHrokrnIXncdd!XJBgwcx^+ZHi4ic*&q^e-bpc!Vy+b^aW~1drIhPqRh~{Ah!ei&$_Bb_BY6zn9Q2RR&F;o#V
zA4UNtMG~3IY&mPY}jsj!GkCV^C#Rbb!4V0#EEh9-3AhRoSX
zn2X{=F>AMk*`-L{V0$W4Y@>VE)K^k87%ZlFpg_^W?UpRlgCXomwyZKD
zQxT9+V$6<(Tp_c)S0{RTbWwpa)qJk22hY+9E+&~w?0i>Uo|0}MLOX=|dS1k-Yq*kh
z{WB>tFO-I+Q?hEtVjgXj6TG8XybN_7&hC2q*{PGHx)e{k^lz-B1ufc%(@S+DHzd<-
zWC?2m;xEli1cOsBvPj?_`E376>SW3YVzVM)^{xCyXGf)l=%IGSx;(m3m9A2!SN}E`
z<0-A~T&K8sF4Z>szDmxB>cA(YN+sGw#4wsO)7p>VYs|1tv)j6nFMnfNUFrD+osT9qWxSPAg`;7*|3a#_pE6
z5h}`gIM6xCGU8l7{3#5VP%W#l8Uw^&G~yYF!5qttDCCZJzHZ<}*fhqJ;F^9XPAoEq
z_vRn0@?dMSKhWfGr7TI8C=A~mRKqaH@X8Rxo1k${y-9H>wo3=|OXJ}>8Zl>+=G5v{u>};)*{3qkBw-y}Kx^QK4TQ?WJ4|0l@c%MFXcTw}(niN1wu>a$
zvG58QFx7@GpGVtNCp)`!`W90+hUaF9w}%<3)mauZiBM+XS<SQJsYk2jyp=Fo;Y^aaC2jA5a3
z^V59&S9-d}xJu#8$c?K(a!c!%nVFmd5Xl;o0e3Vw;WVG8-k3O`a4
z{8$u92L&pHqrwe&s~Fa{8lob%AimeqP>KuIr7QaVt`c4yLMq7;igPFlywlqJ*zi#J
zq(UJDiXlw4Otxn`PfI7YJ;9BN!`vEgZ>u3dv$VVwN_qy_O_*_
zD_ta#a?Ae?2-E*BAdCczZ0!HXK9P-yf&D)Mbg2R5sI}zwwcW)WPZJesnMptsP|f3-
zLb@Nc+V5*gE3RWCMV9@{u+EJRef-g}Ls
zD^+3&-%Kwa`D65mZK*ZnbmM2n!=ck3AaSBuan)D4n=e(6Va0eP_T=sU++01h-OgGW5lm(&!GF)2cP8FTQnFOwv|0MrxFV{9AT;Pv3Y;1ztwu2PY*i#;0AiAJ=gP0
z_;?5?Tloat9#F?a3=x$Vj6;_@kPpa?hz?-!mu&)?)7I6%mG7}lA$(HN?n`>VS!Cw2
z3(Ws;&XiElmz1LAlQl=tEr2WzMwqr&V4Mde&d_QM;FqASD46f2D58F2UYRVoeL!vG
zH$
zY?buf`PbMg9<=%6yb=1vEuLJ{8>AF87Brm9NfKA`iIPihUdv|HBARa~=bc9%27Xh-
z(en=ocRJG5TjIZ!Vr!SCLThrYc`FWYv&llp=fe%O~GvNX|;$FZgqeFH8&Ets-3Uuk5Cr)N$jTHj|fH=kwc1_TuN7WL^4VkjntMWyb
z=SgGr4I2~~Qv$wFLCDUeXXGSx*O;NES`SKUqg9pJ1q-PxN%6D*d9=qtk
z?NGsP7jJ>UGCiDFbX0I2x9Gt8@y~N;EjKGGvvP@Sq7iQFb)9#t=H86F^~v;3iiX@R
z&>6wW;&Cjk_CDL$tsc(F%#e0~TK)CrbUpjvbRB4KjW;oOUx=H@eOf~H{iomxmoqb6
z_eg+%$!qy|BdWsPDe*3HN08%|A0M_GbWt(zY$itnvsxtN(1bhyz78|eu?83rx|qGu
zT+ci3I^|U4K3v=gqzK#i+x+zX(xFTYrf?VJ`b4ID!!}etem!x@WlhEPUikcwzCZ{sN<_%@-1Y`*d{NXJ?G8UsZ$5eHV|Ih+3IHxS6!ZP
zhx^f*+x6DI?vk0nhpn8g`H8_r)oHH>Je&3
zN?$;~-z^IA&R8|RbPBt2Z3?pm1+eG-J@R<4q+u2ScB0ywGW&SJ@RsLGmDmMoMy{_x
z`#6!RM!1RKMfQ)-vMPWcD?{|#_c-c8Z+$1a_lsa^WSw^ORna>CJC-F|5;v}08$lk0
zs{}MKA;aS3y$l5)*_Eq}(CXh+=C~ctoU;e7A_4fRi6`2aeGq2M=a{8$Y_l+;+N6%VE|hoyreu_PrD3q4U-8`Rrt;bHvC+*lyY~1#
z|1V0InSMKm<|InP5Rp8oxro*VJsw>Kr^DGKv&kn*(Dq$zT}rnEeSg1k6)swJa1cHh
z8k@|W2TKfqd5w9AdAniK!LMYiVYp#v;mqvW(Y-kWM2$lmB?42B0*0Eb@y}|}ug&0?
zW~9HrNHX2&lwB`!V!S+VHu^*Yk^)czU?6X$3~Z-&F8b#Ca)2`2AGSlinIG9G>D)Lmf+zyjflX2QFcd|}Goi8m>h4C+%%`TXntXgW)@q0E&
z(NaqzKVWqxXAHAEx#)M9U(`P=b;iNz_nF&Lat2Ip*F52KCXR1kJ%QvAwJfS(B7Av{8PC!gwV>OEG8FKkx%^3Td;WU$#Qbz7We
z3*+N@OUxs%i;*6m+bk0Yhn`
zhfZ5>&U#bUEm{p-aBwh(4XTg=GenJ=Acb#pJjtf2mijCvtQ_1Uwkm+kB}~~W@yfy^
zmMAH*aCM`-+$-`9x=1}@rhMU3WubVveLcRu
zo=v0xt7u}ptj<1(PERuDl&r4iqqleJJ)*S0%WvfR@jRtjLn~w!s0JJ3C7;#Ek99`V
zV(R?oXCuf~X>Vhd)zk}PWWjC%Yntq#5~m7_L&$OMb$(+Se)ZSh7!{W=Z2UK~S-(3L
z`3nRBqON+{!(s8~(xM+^g^yyzCrxR=fqZ$j)~s=zbDKRu*IgN{eGz|GJfh8W@|eO>
z6Zf|}$dHOph?-jTMTJkp4Z?Zu5YtgfwV%*tR6i=%B6PPMDf{*Q#EFAkO;Jjt+o~^iL
zA|B6;IxC!gnABW)aD06;)B8OzT@nXhn~A!cdVBbv(HBDiXPE_Ci=%0FDYJP44o=bH
zCC7fv@2pq_Gh?!;{CeH^
zdR+#fq1=>3XNLJ`SVKQk$*J~64RP0LkpW2(v!YJKIBSZSUV~-O?0Ke;p%LR{{x5fn
z`V4X%^5L`W3%-Wp4*o*3!T04wo
zbfA%oZH%?*fhC24))6)3sC`_r1~yaP{ZZMd$T+vs@mkjET6ulTH+er>`^PPH$?1WoOe`*=DPoUiYH^s?q43uehrDC9*1y{c-hRddd+a&<9rs?XFhaHv`DT*_SVR5%q=>1eJ2od-Jk@IiL;e|;rt
z*86JwOz5oT&|*CI%~V*6&BXLNV2-C@sl_x%DQnR)?Z9+x-Tp3b(V8`F^jG651D;`>
z**oRH1=6}+I+i-|LDQcXyRE0@o%H7=WygWvkm`&zzN+Zw!x)KsUWjl?gjl}rk05@G
zVi&z);++vauN7
zp2^JEr_SKLKCO7*vu0@WS?R2#Gz6*+ynyd^nZ8bi*haMQ-oU31NoV|t{}M}2
zY}2bNAW)W}eZKvbg^V9pGcxQaX!hnv3&;CXp$Vgh!$~CXS8)KEiA0G(SlGvMom2PA
z{_OeYND_Z8*6{$%x5ZIjM76i|R#Ky)G2blGY)BU$SJh@;N_Zcut2R5o%_6kiWSN{#4|*juOc-A=V)n3buOLz)UrN;m{WZ9;OqVTS
zu#KaUW(mGN|>{w;HMHOQ6FR^A`{F%ecFo3
zFs4LMVvbIytjmvO{XFg|LqQg={a)wy@83F}!kn4jQMlc%J_nI3{4xV?pV-M9WMfqF
zFso7m7gphlrb1k6(VbnEm*@1h^cpKm-1gw=>rJX1sw>vM8`0|+cea0Z4%mpmOU&EC
zk^pOdEc?X`>f|C#v=y+B-V>H?(^A%hz%7rany3@0-baKx7YH49fCycq`LJqniO5*sI7G41kOz=%M!(%8|(Mf(NFHeK&O
z%z!utj3X@0!v~`}x4=V1)^b#6*slB}T*Yd0?+#LDI4rP-3=&njZ_*X
zoqKCyg5B6`y|cLF2{c$J6Qf2WRk6h;jrr7WBZ_nfckK<{n=(1=Y$im}2!+x`AVyHy
z{U|luqQoFLAzGnq6iynRO|!}+5|#^HloB3HMWm*y8T~2|T0}`-Z0i@4uwE}AfO%_K
zb;S93Gtmz`9=R{If-1e;I>GeF^=^QbH=+CCAtC|tBH{ZNTCEKhn&K()%<+w
zG_jaTUvZO?@_MGvxFrKfBgnzg>iB#(;MC=WsQVqp2rx{3+x%d8xO5{TakQ$d*^JD6
z(9%(qxivW6rCH&L6p_xNU!0zqJcFr_A6Cflp~zhD&zoR$TXc6l@p#=2*;Fwy89OXl
z4CI5t5&87qr+rBlOfYnqL%GFSvS5{*SG&*XQ%TiN?h6z|M>37PI8r^Bfn_!4&)!*-
z6mPekS>Kim((2SG6Mt(`;jy*G+C(LwP!gpDW|*VYa)feK^ooUiaYf65mHeF*ARl{N
zOiQMA^iFG3<=rnwYZ2#hf9uZgJCTIap>w&P-b)?#Q7}=%K0Wo5|HwiY^`b~Q{9&1R
zehY!vdBM`}BUpC_N{TQ1a2gFM#E52qTE`%>@E(2$2CopQy1I~}8_HAbl~)vv@e#D`
zyuBG#MV^f>#TN2D=UZPp;R2fo{1r*KOa&`=k+jZSq8rmlNI>~}$7Lr5hd~%Yk!4i_
zo`(o_6AGU$1=TCHt;^sB{M%FvQSfv^3|DY7FB3YXl^a_#$a5i-s^~UE&Dx4AHS8M@
z7uDYo>?2YjRRWDho}4RLj>qReD_Sxr`IKD?{4&$jH9Bq(y{mdcu)VCPmaF@QF$c$$
zs17XEo_z(*LDP$=5JI3AHgruj=pPq8Zl9^IsH&E)z_V2|^BSzJi=iwR04u|2I!Ndz
zq@m!(s{2`1(o4o2c(NcK#JbXW9;jIAEZ|ng?$+tcq(1zvbb;mK{}y*x&ZJY0@U8F9
zhys7b^!8HB2|0@
zdynd|Ung!tb=31kEmIHhAo{MLyUOSk9H}8(SN~g^DOdHO9~E57uZiwyxsx2ZV3wM$
zxX;mN&#AR;ysf16nP&__+4kYt48o0E0!hqyygskynsMcc+nr6c+K
zCO-{pJCctSt{aAq&*
zSoLaf0qb_}b9bgUb7bu=`!bL#a|S21=i)%pO7fyIT+Ccd!ss4uvQt{GRKmwdi}=(w
zr=x*_yIJeTlJ?Wsm9nKCTq~Gse2qV4b!3018gqU$p;j#`c@A>q4^*E#4z$;9Cp6X9
zhpc#2^l~2LsxQ7r>If@3IUD<`nKIlhUhFlpkjpglL43sp*PWhkdsh0!?fOmrw6Sc%
z)qb0tx5h4DkJ-t_VgJ)DV-9(@R55dT{MhT@{Odd6g2|_$lNI$GGS9=|mhU->{O`Aw
z^L&r7dD8^9igW!&s_N4keNJu}DLyLdKCeAwM3*!EAU!5qsD42j1y;oc0EgBINuW^>TU0%%51Aqbo&vyqXQ@6qzG
zNSH46r=;{#Qpo!BP2ja@%$9cdaKn<^w2#)<`DoI$@Wz3b
z@J^JryP_^-<#d+b;Tg6nIZ`rtxija8
zr;?FZ>{rj2$t=mIoGI;7>r(NNpOQY;=9RYR;P`0;0f)%B_wdth)*CI~BV8WUalFMqJN=6<|Y(C10?xYa?^rHcp-^Ex_W%4UmJ
ze1rVyB=pUFZAiKVmh3n0&fGkX_8QcYkH(Kd;cPM^f~)FVW?_&TceYq<*zEw`9zUBr
zBGaPRwri3CJKg4FXg;#{an6z0IllQD3He89TSe19rS*L~M4x5&c#73iR_a2p1W^J>F>K8{p>4b^Q3RKFoa&NcyJmvjh}&Rf
zb*wfJQVP`9Y?zV|hp-#Cdm;4kP!3xqL5%4_MD!$mL?UDJpOO47!6rHfj-pdc0^b5h
zp}Rzg`{pt&KRmbOh_ypJbyugELwXVgUkpp-d>$`^6so?iYion_OIw_;P7g1P#^zlg
z79cR{gE^WRYRWzsB02hyhO4sa2zL=|QhSj(#RTma#yTMPSTO2lj%OYvyoQ)xQ#3=8
z7wLta_V{k(*HGNS7t&UGGH_PiXjvqB97NCep36V`K?wC-a8En-FFfMee7^dgQgZhy
zjg>)#p1J(@r@~yUUp$R?^l@EhE%~xVt=}jAAeb2YHr}6^n`dl->RhS5P^E4)&h7)~b9KXJh(}L2pCb7D>1baZL^;zq+n*^i<%L@laW7
zgiuvLOx70@4C(!y+OinecUl$5{KGK`Jy4BqZx$&eJ->0B`B7ocJH__CMP>3$Xp)U
z5@$A3A0CSVQbzrjd9)l?eYC?xT)@@(ZT1(Sgh?ETPMkd1H
z$SavOXhcyy+XrciMv*A2`ue?=&L7Po)e)N?#LC|f+8(r91TPJXSri{;lRuHm4Ihp=
zHis1LRCi9qQHxhCQW?tP=uPk%d}Ev$vB5WAtP+>J9l0Rs?v~X$I}e)$aNiPgS14sGNIU8=iVRNU6fQ*Ai(NR
z-t688>?A+^n4B)rPy1FEvGFTByiY(uNNlcG4@-TkQPQl9v#I+_^&ZN3A#0s5`s=sN
zDr-mbPaBk2&mD_h0l#T$;jO!p1{obRgXzsKKL|xLs6NwBO{``~5`jX^gA$|~UqgJl
zY%79JD^V<}xGd*4sG@UUN)!^PM8
zN8E6_?Q+nm@sxN$jPoU@?WUIEiJ`~(!Xp>hi2@Btu|xdvYmrDXz!9&VM@pM#2?Rmc
zY(sqbAAaq>s|2K6oIS0aJv}J?`Z`Stb`>j-ow=mT2MR;roP&aolZ%31kc+~Yf?eI*
z9k`H!=idu8tvp=3+%2sA##fg=9-S(b-_
zoB#j*S>4^mQp*Y`*${ZPv@8X?w$%qu3U&o2a~mtke~(iC9u@vpKo4-fR+j&Hl%62P
zUuylI0{)hv<>~Ha>G_{3dQflz5BtYms{_ya8^8b)xLDXD{dYEA($3RE-O63c#mUvh
z`R|JfP_Rq6IJ&rNxtd!7{#)A0+s@KTQx2%g|EqtOx3aUb1d&@
z)bg_M{HO4L-sHb(@?Vu_*R=xyfrF0&2oC=S5pF?FZjS#Y%${|9^u*B(ykM^ASgZ-G
z+CuL#Wu>8!ps4#mNJwCAMnJJ06*XYVBaN0hYN;~V+Y1rGAQGxVp#fDNK`8=7Hi_A{
z%n@M;m(yqC`Be~AKX?1%)wR3(OXQC$;|ZS{!jF#e@Mo6ou0Yvu2^^Cli0CT6Xi)Qk)(o{VKslcuSab711SSICe>y&hx;*j^>=k9g1o
zU6G&>nTvDxfBgXod{DW93i>o=jIYOn7mJIL%!vse8wKu(rlzx`B1ldI?#Gf-9+u{y
zqmCW2pp8ohN1G|j;>8Xrl5i4Y_oah@+foHAxadMT3H4dW1(4zQ5dYYV1?}?Jr^Z5;aIY5Goirg`^
zwA1&#DPHVprm?&rLygXny6Ujy-5wCO=Wic>d4I3IT+nu(2
zKFt-Oc;bOZN%VL0+F@nzzutFU?pA5TCHn3sp9^hJw^-03MZoT(7bRQ$Q)4bBpvJ2g
z7eWewg^Rxip)vz5p^yRlp`am|VX_1lOmy8NB<4eVkC^Ohramyid6w3Zp$tKhB0MG-
zM#ig5)lmKH8;#l++ge{}kx?A}oVA7+<+>|j^CIIXl)ynl5#JCXN54(1DocVeO8pj*
zS1my8k4qtp1lK<|W>IiobD!}~(}H7^WrLx`NFgA5(7Ut5&sMAvSSInRmOt@^T1XT6
z!vxn+fb*f{P@<0mEJ_WiThJoG`D6#^Ns40#!y%q?Qq*0y_4K5fo$@#k&k@O-*JCj?
z5|6?Ewlqi^#MNJA(A<*YFIz~q`hxu9xeg-kOlTSRmuw=u&+l>8LMa3m2+S8;*+>c)
zOVcyhxx`jj8`OO)!DPdq=zc|-T)XTga!`zfKbl3PV5PPSx;4}W|;f(pCAn7c0Ge`y02^~oIe3Ku?WY={Ps1#V8
zm+y6`3Hx?k?;k5O4IGCQi%HN}PKeXK>NS#~QG3HAK`16di?%ec<}ma=R<(}R8r6Mh
zPXW8)CX$`@F)|V!*N`uT=|h;dLCbGLOPC8#7Z1)RB9gqvQRo~jPA=h(Zo}?ngJ5tP
zYFCOK|EJ3@d`}3_+TrOFEjGznh{Q&2iogoSnM2FSr*BE(h7D5j6dIBo9Z)tOg#=-e
zN*_lRVl(26Dh>GwfT`5nq*=^-NI3Tw$+)Eb{4N+($a5i(Q|MJcA+g7hK7xk6aKHT9
zr9xxnp6LI-$0_FjrUTHls>}d45Z4_|k0p1o)d9|gnIe@*t3Z8~CN`CH(px$cml5;lC&_=ohCM816DymuQ)Z?|pLo4?b_5`Mxb<
ztFg@=qdpFmJcftMLyH-lt2Qpu`K$o0F?J{>6UDl5N`169iQF#v5IiR`0Xwbb%FHE_|G{@`UW**-JP=_3ngHAd#v0}|Mn_$z^g-}zmv)u2tU}>G&KoC%>TWAT_zOo
zdEb5a#t}l`%>S#i@zm@`Ha(H2pH|?zXvSrw5!-P-f+>BWu66pH-EnOj$hZdnJ4b3>
zv)Nzn6UG60)ozu_sF_oaRT~GczL)ibx;w7lHUxbE2L(7YlDAGzmZkgcfg*ZSgSUlM
z$EvAcmwI6c!}VTpMFz>V_xy>OP=r?-vR87T*!UfykTnLMJM;SCFI?Q*cd|vBX7b3L
z%1ws{_&__1D`>5?oFBLQ(xc(OtuVLd2L2uix5C~|TQxReN|mh&FR=lQK5Z1I1k2D_
zauJQDPj&s!t8QaZD&tR|r1RiQyYuU!XE&{yU*u=mxOu71^4Ur*fo)s5|NT2au2hWA
zFgf+$@&gXj`}}N!E`y?}QT6fHaN4ePu!21h5Sf>tJh#aV9KBf#N!cRjtr~^@e0a_K
z+m2gd=vFQ;qt07@wgHDyQk4ba96L~<4VPPcfqSsq&*^@s
z%+~bWsR|x*PP1&4mQ^J#QXr{d`M;KMSkSE0?*GV@;pB>o9HVFuSa)}pYS>`9%Gtob
z^?Eco^ka$7rDb))HIkc~SHP!hwpiQB+O=$n!^9n_O>(7f5Yc_DJ$f98v7U!z(yKdV~|^jHwsN1E$j1Hv%kAX)uM
zhh?#CHuyqsvo|j;;C98k!oCU9^tbX4?O$|7nm0ia%&(|ajZIH(#FN=$>BWDOoPJmm
zruSQ&5|iU|R(4(oi^;8Lk6A$G^7-tCV}stzlrZc0+BK3=2;3DCmdSowO#J}R;<2QJ
z1E)cLetx4wfmSn$3|0&{8L#XZjkkWERmE-k3SWc%G|q~$1Kp8xxx97?i21{t$z|p%
zm6#v>^%WOY%tAF$M
zbQF!xW;8U#mx=!purs9l=lb$$MU4k!*Nm=D2nV#Lsw8xcy6to1zC%Mp!+Bg8pCgeg
z)F9@{{LC8xU)sh98l#%(X4KqXlqI7F==9^|PJQDSqV7h6vL97X#_r7q*nuy)4*t4F*(8h_%oMSitP35z%cbaFJWIVQ6`I1Q%s))D&r+bQ4j^0eZFP|6BF}R
z=K=jd9)z1bHpr;n`W3u#H-BAc+J=#~68P4cdA(KFbsvbA@OV
zZ7MuM+>DH2M`S!tp;z(T4v|7Gqo`j5I+~7=iLh{aoam`Y7m#6$o+cu=PsB)C5*T-k^vx~hPBwZNhf|SHWN;1WAZKkfiw%ho(hvl9Z2bN4fb7@%;C>WT6
zZ4bt8Xf)5i|@zz~kNUog}1B@s>
zZ!RVtqU1#+f$4NZ@!@`wm&kdTk%f=t{YeVw%$nAvtI4Jh8%uEy%E3n$^wj=T5SVYe
zKNFoHDfFJ>R0&uI45gu5KkF%=8zY7;>u#f~Ql-<4|g@
z#1@E9p9iK9U9k7cG#+w;p(*9_wNLzvNkUz=rcVr>JFS*CE%Y=NasT`pN(i}#rkFkv`H=`t7b
zyQZaqW}r^4cUmJea~nSM^V1flAcS-P0X+uddPdu!yxi4JL*88}5qGJmcbmmuFNJj)
zgcC9U=WS^tT*gXMnogr^iE@nLHKcI!6_MBe%V;eu<~mCqA*mFa
zz#tHo69hT&5fg}gIT?~VJ;GrmGyiO;m5K4m)htDsid+qR{~QN?sC1j4DOA{=hUq~I
z{C+tOt_=V7P8^j9ZV|&@_kpy2%-%=yeF4f41x2+qtkj!)n98U=336m_eM0&*SYlY~
zi^)h_ITOz){vFK60tipp6z-+()I^}z@V%cVY2E7bAYy95Mf>JOdxyuqB?dgi#a>%3
zxqA18;plChWzuP0;ykfGiiK*y1Zq-6a@5S>1@E#n4dLmVD%x?sT0F36y6`}vZl2Di
zel`eu_ltHH|zxuofqG0AsSscx*Qvg%yS3hOfsX!6l?Z|g{5q0Ql`sh8OGI!RhJfw`jI
z@Vk170Hly?jc|3o6#exubB6`FiQ&;cx}7K*Kj5@sEs7`FfBncnVJ)*jh;B;Jx60gyKzGsT$QyoaY#
zkfn&W%)E0H?{<(aEK#E~9S$`S?lElRE#DNHL~FX-_${AaDgMb#vq3j~XH9R^mdj9?
zZhrTm%2I`f)kQzP47(i&l_a^c9wwPtchG)6xen5
z$c`x;>RX7I9`s2cDYWfqG33W)p&UK6X^JiiwBrvb9DU6=j3w~6wec0nxNId)5he2L
z3+qS;G89FlA+~7P)JeA3?nQDPqu2*(mt`0`e0W^DkrV8{XHUU91RIek+*hr6_A$=5
zRwWT&I_{HUarnsk#`wB%b|g0o1o;6yKogo(fsv*kccuCMYqQ3H@d2
z4A(9Ou#_h%t9)449D+UxROfH;lg}3OZB#`A2X5_M1K@i9qved4lgu3-Lm|&IZO!iu5LgOJU#K>iX1U>*z*n88uQ9)Ne*mVLSiz!?jiUFQ_uR^I-3xyUZ^~LBc
z&WhkObibTpa1YO_?MuQesO1QC*gP(Em|e0}VxVS`|IOy_J229qa^)4MYpdnd|GX4`
zJ=sdOgBrwjSn?q|#>=_gwO%+Xz?DIcu5#}+sn8_$x+=u_&YSytBItQ*#_@Tgzpz2B
z(!j_>rQcX(Eb*<}df|GH%&~Wz;s-~b?Mk}ewrzu_-#`=^|4Zx@ko6TB%IJ?#spu?)
z(oV$Zk?Mo{U^v%@2Vi@1xPP@)wCw#NYCUc9H31L^ljs(wwJ=^+jPjoa92lr$bWg}j`nL%b#qc2C!tXM=-aMK;&y~qT>UoIE1*`jq0;3_f2#-kmt!J
z6K*4rNy>6mS>8hmSU?a4H&{&holN~gAHKr-7$P7s)AUWSjgnpxGybK|+(A!)
z-R-DURI4TtP9@dEh!O;3H%-VN;D1w+pKM?Taz>zzR6I6`Gw8%Su6|SwCX~5$~WuG~49=>MlC19yruXQJCIliX1$(`BA3cv->!`}%e
zBeupfCRb)_3ag9Noy`^MYq@IcU?#Lqt<`ckC#-$a)UrwDOhpq_6lFg!=yW7gx$RH~
zOQO?vsNO@Q%n{4@$pEACOQ(!KbV}~9QCQg}VIm3}x~K{)VIS8xhVx-CXz-xj%|o~?
z7&adgc@(vSBAI7wUfUfpQE)sODiWaIPl?kk*w2(yOU|}#Tv6^(vQ}HR7aY-MiZcyr
ze;Dc+WtwUpPEX8u3+$@rQQxbSkQMBl&Z@@P-W2f>nTo&M)*p$*VB@IC%Q`hoc8j)j
zR{*eqY_i~q)?;>;zmiZqnFGmqPO`4{33Hyn7{9rNmzUSZ9&%^-IV)X+xqj3*}c?|RUwU>!349y&B}tXoP^
zzTkswm`Jt=&HPTye~@jr3pxNt%SDcY8kSc^Cxw+XGj#D2KHLPyLVe$=uOrnLc*H?W
zz_?or>oG(!as{JGP46>pMt24oV-s|Mc$wWTub^fe1->=z8$1ZJ5;}6VpC#BUwfFLK
zF-iR3fysJvh$jQ&CP@apy9>u&sf)?6^x6li*+O^q?IqpO*0HIF5%S+41TL?M(rP4`
zVaB5vOrQ(}dN?on_#)
z>FMz
zvmb4{30No$nR3M+IN)s*>3Kn3MAU?+rM=v0+CvPg%uDqiL79??W3xMY@kn|swZ4_-i-?yV=ep%t{&ex;b+J2f1Tv`Hlxjrkw
zt{i8soP%P=5Y1ipkDQ?P$suJUM3u$f*bAY)CCgYUa-xrvmrlZbZD0_I6BiJJ$}h-Z
zd3iS@4-<||Pc6WZ(+)zI&{h^m!;#yWlkdC=TG9fb2)d`Gz&m`Z$#hjWpC
zVp2boyoJQEy=3exMgWO;o`HKwx-js^qz`v7F8hxJJJX4o2Z5Ptrb8B(EG=}QmwzXC
z9F7UnglZ+&rCugP&DbyJo4J;nBQV=eiO34BIzk`TEsK|6O4JZZE*PXn665gZD&+eH
zNK4MnQdNjY^wvUT-djV&MvI>lF~8gYeYm^I!dlBxlP+L>yTh3WDK^*)A_+Ujlp`
zY536`7`9?2>O$uMe2bDW{2+bwJyZoJewy5W@E!Y|f9j&1f@oB^qaM4zY}+R@Qy8wf
zBjcciiNpX|qW6EvC7cT0&>4i$+%9+KX%NcWQ^WpmMQ65&6;$}--ul-h9M
z=~Ls?>4`cCJ?N?P^=ato+$?9m^iu&q>I6K#<$`eZP=C2qQJIbsK>>h
zPFWT5n-Ks;?)>+UvVJ}amhKv=EWJ*H`uTyHrndXQ%GWtCr_>3+RHcUY4QmjdhFTS)
zv#1x)+~cOg$c_Bsv7%|jY_|mgIcA&r7
zTQ`m}Oy%GRg{MTP%E*+5Fat$iHl3-Zr{i2N=<1c9qp%MFwE*=gfwR*x2%uXm81A%W
z!X*0aW&H$5}LiKqHd=1ETVSr9UaAeYSvT%UM4azv~-(?_JR5mlnQq=J;X%ZZoGYk`Ov5^
zO_%p9-;H6ei(BipgLizE0St&TRZ+%MtBM!7r^`ZMew1ozycAX6z}SEzt|^qfur#h)
zRq0vW&TcEU730@>XLhLM%aHrsz8yd;9N)rGnEmMc`f$GtIXxJzrUlpPv<7{$!=~FL
z55N+0I_saci0s7LHWML!v=R8llR#i5LOk#4Eyqg6K0kLb%fnOlpMKJf`slc$qi%}q
zk5e$Z13MTLj1s<^A$IE4)pt~teRK=sH@i%t8o6c}r>TUihAQ|&R{8U{9C=i6Hj#Jh
zT00}OYE0``by_^4`l+i|9_-0gvGatku7d=THnNFe8ctm(pa*MTceRl3!~sf<1^>FY
zW1Cjo)YK}jMy}8>JZN^j_6Zdx1c?_RsjK(0!@7M3L261KcP<8`Jiah;)cif8=ZGH?
z7w4^YR5N4jO^vy8;Ce=Wo|I0kkfY$nF_ZoxLp7ErEN(oU<)(EUdLQ4QVH22$$VTfo
zNYix*DRz9rb|xdEDwDbi
z8e+GqUvW|`W1}>sK-0QT!ra7`k-(D8CBmcnz=lZR72_{aQDEI)&VLr%A^&=+p|>z5
z4@C~0Yo@NqTMYS((;}=;BZzl;cEE2Y9TF4sjgO_t?--)702|Ru18Hd
z5O!k#99cY}9-sn4!+aIc?s)gJ9o964SAVs3@x&z!Hzs&2O5Uu#sk1P@d{9fhxMhXz
zhgh1fx<$Rb(RI
zGGBn(odd1ilNM}$t>;LIA&VnaIm`|%r1A?n@?0}^He``XAE%G#R0AI;(2?P|NRXA~LfH#M|2Fol))Zmm=?*%Sx!DzvjJ?JuVXhY==q@wS7V#tq
zOl2=WFFgaK{2aX*^HOzL2Km*OVJ&o^%4HP{<_ja8-)Zn~@Yt44Zo^Bk*ZVM|bUFqO
zJxg4%0l~sw<|U%QWfYbOiNe}i%oBYotv0RtWuU;arzhZ#4<^&vR7cOL$n`U9?4SHa
zQU9gN#XD*&lVYw*Z5>%fsJP8es51KRwqJC$ADQSgoPMun#1|>Vk5Ax(B-!6yk}^W=
z%o@axWS+u`>dQwqhNsTWOaYX#Li`9FO$s{zGvormsUu*VhILLp#rLFWlJE>3={0qF
zU3e7IL;HWbETAfC1fYK*Z}Rf584=#4b;U+mq_$2Ctb80Ad6VoIk&J-RiINj}+q!*2
z7z!4tXOxg*de++>8?o`leQ!1r7r%AG_lO4mN*+ed`{+(4=CB9~$eGO(%>M4FZz{KX
zhW4O8osWNf=T}v_0B2dF9EMLP6f$9T32MC%#=3KSx6JUnRq*4%5`b#K4wYEPHaB%D
zR?N0}TsyMGbFP)nf$db6_p$y%!XRmte@rj^(1z0LY_Ne(+9BSx{_Li0za4%`-sF&)-AQQ$Bd&{$9D-?W6nprjdXrm+B+uZv}5j6bj;w
zWCZaqrO0C~0wTV!0I_PH{LS%=Zhl4e~R5Rgoe<#Cb&6W
z4`t8F6c5@N!W3sGjRj)3(~n~{o$AN+K%7$!yLe@IVcaiRfGgH
z_?ifn{ElsL%DYO_02|RPbh2S`1Ww_gIer{bBlq>
z%F5Lr=VT4#VH#dK%NTnE^!m!f*rm7`HY0Qq_L7}V91_RA-GjOMb7KD1c#DL70I4@V
z{rC}#89;6Lho&sg(f+x5@cvTY!B4i3f{T>^e(jgLaA++xvJ(A1ZYqJwA%Ntf`Q)iJ
zJ)>3>KzWiK%c^`o@$`iqx`mo`tj}nu@e9*PbK&y+57%3o5D{o%uyq+}3@_g0_A~Kk
zi8SGI%Idd-6UxxS^|l+;C}Bbi0C=M>toWpc5?Moeu~BE;`=RI4LIUqxe&54icSjld
z;ox(8>#tYO;WNNGC4u#4Q7}4CVfdjQ^aqD{AxQ=~ft&q94a$$4={vDQtSVEMjb46Q
z&gRjZI7d22DOruIFHmj^e>pp#N1IAP%pSkl1Ger^h0pzhrUxIzRkg%dbnLv=B3BLH
zZi^NbVO7O&(hQIm(`ZvECtqNiozIsMfz4kV1BcI^@J;-2AA9#O4EdX#`k=f=bRMXe
z@yQb*+Ghv1K)q~O6ohf_HH`Pr<5mYU0o4R@%mf3NK8?{CykbG>sa>5i+jXkZdc7-
zL92(?`+81jE`fmt6rU=v$qLh{B5`pmxE7U%K}`^ZtU7=K!{H%DTB`Z!G~=dw+3RxN
zA%ObGREU6ZCx2FW1dwpmBTf?$&A4(!Trf>VUj^UGZRrjKF3Nw=|v6oT-SeT~2}cvL;sm0plWuicj>#go;yM&bQk!0^?}Ne-w#4$+H0IVP|1lhX`{f
zwBZ40&JvK{HYRHm(JR@?>r
zJ!NvKDzJUqW~ZbVM(8D;ITKk3AiGRbPBgGfwGT$b7_qf=4B<|1S{!OP;zP_1QE@e_
zlbA;4gUks)7sKfY4%>w#{2ZI&99|Y(bL*IJjktSUl;BOa(l3Jxj}GcQMnM%CER0Hr28)#qF&`%%M<(AkQaUd3sx#iou-_-F0rFsY1*Ipd2;D?K|a
z!Tegf(<~tv362U71ks_+(h*M$WI`l5vG8r*CL&5IG#fbZ8J>Id4{@`zFg6Ms{mK^k
zaLV)QW|kCt@kYpD?;}uyrfcAeCLPvS%zue24Jbs7hzcL4*oh8cq8u&jAGF{Ddj
zW#tT@Lptm{LFN_nSy?5ELfIx=CV6-yA+{Wu4W3UA_+UW(HJmTc>f|SzLN5U#jty5M
z&k%^>$z)yOi<~-cStfxfRJ9qMoCF7P>M>Gxk@>vuo2KuZ_1{}!@Jj{6Jio|6BDPW9
z@2g@T|CY?j8Bk2eyp!A9pr?_jicScE*PybXwiM^K6dwe#N@vQppr+@VB7qjsw-pGSL-r5G^y@qXVDe
z6go|62jvK0Q}oY-&hXYat47xro_fZM6D&{Z>Knh;>Zov1dKjtsm{(Q)wQmHVi~^&#
z;A?BlSD089_*snWJ_uq_%mAt}HvJU@cZCgEU=PCO7#<4hIWxHw$Q4bU%QxHm_8WXi
zUqDg7ZRssq(&O57u~72k3Nr#~-c=q4(I%v5GP_m%ciXzflYD9n=9r=mCt)}z;Yc;5
zfn>)GGbd!>nRqKl9b)y>eIyNRwDg%)-G%!_7cjQ3u3i?`f{6lP-g-MO^LKHYhsmXB
z2=I5gL&HY%;OL=1%lJkBqz)Dr=)`<3gpeSc8mR{qWIzs#1MDw%IRPx5M}U=&GCJ;9
zACrA>M>wFOO#Ub@?@>y_OgIR_83vKmMA+Uk(FZWnNhnJ|5A~9^nYpeYu;FBUX8Gjs
z+@Xq9{PNBbG8Q^15lh{fQc!+i<9qkA-^=<$6I?ZnGWQ};F(o}_cnERS-KaN^;!!m7
zNV%~D?Ivp2^8|p*5yc&>Xd~0X{pn!pS&Z{6rkhh=WcYLlPDoUULb;rM?%(U3)T6L2
zXw2_upXAY186zsi35@-pyQ%YuF^JOw~7huhq9T(G$GwFyh#Cy4-1{_rOJCqmF~
zK=%QGNk%))?ekU$qAB74THpH+e|!+RaliucANxiYP#SPIo>$?1PW=0JVOlK!SNI*(
znOa2+3>))+%AQoJ=y<-o(p}`C=tLR}`?5pbf`{`3(nEsY6;9e>3vx{UoqBw(zXA#7
zSMu~I5(x>8M6zWnc$~0J?cJkM)U0i83ptRa3ysD?i_=FqYD+B-W^eL99t~f>$`$<5
zAC{iB=H}8$O0L$Usz1bzF@VPeQs7`MqFvgsDd_Scy)R*Q_$)$91VSUWF;zwh-Y|oX
zsl!F?;HBaKVKO33PLE%f*qmQmUdkaL>#|MexWvf_j)viSITx8@&8^U+lPDkv4}tZk
z`GVmabGNGpkWh+t6-j>wkYG5pm_yEr=_}oY{=NR5a^hIjsZX!TLEBUDm;EEKb9|(W
ztzq$eHTwMb-JM)=pJS;_C2qpVIck+O$*a3ted=6@#Lujuv$u)7^F8u27`*4=b91Y6
zb8jCFRazPX~r8lnz?emD+lSFjj#a26coAj7&VqW^_?iSy7Vp0vr29
zPIVGQqPWGQlUZH6>f=8S*|lO})8+E&Y&`MPzIl7kw4|is;On1iH=0xcHJCvV`|98e
z2dXIlS`d9wy0G-fRK<8W3^4tPt~+%~Q*NvBd3F
zfbtsswAqd#JN#F0Gyss)N;-VI-#Prkq#9=*!)op7zXIa(5n##?4u8k3*%d(S
zVI@ox6*8pEYZ`!g>*e<(l~kM%O|G65>e=1MVcuP;LWjyOh$#uNA5sh>Ajb
zi2ILW=rVQ|w~!h#CS)qQkzu>vx6J@uO;8;(MWRN5_S{E23~WzX1qt&Mq>+-Z!NBlN
z$5RU%VrcAR;RY5$)ral@d4B6LsHfYJWo)2LoE;sZW&T4!`uI?H!}M!}tF4uZc{664
z8;L6>@QmkJum--fNr+U?_Hw(NKKH{m;@4TI8hGY+vI0IhQv>1%ge5a}$9fieO`{|x
z^t2KYfTkkK`9@xOZ(=mTWR;vOGM0qw6L|@mRH6_-nV9mjAWOF>jKSBGpSR5t4aEU^
zL78q^;<-9uXMDLQCd#Yx!wre1?Dm^K2`hgVw4c)pX
z2*`iJ2YfRt$9CU#1{NSdIOpk-kNfwXMR#~+x<
z?KtWrz43_@C#GV={9l}X^(jnd5-5pAIcT0D-G}0gq0)l{`lt`C!
z`D}cDzW>1UlgR5{&bfQfnLV>+t##L4d$Mx^o_fAuASS^J6RcjBXd9(cih}wbJUUv-
zAZt)wAO`1P>+n*-I=rYFUnw8nUK?PO3BXt;n1y-+h8*=P8v;sZuC|O@#)N!7#K>|8
z1I#$zjX1ev48)r1JE|h6jW6NN#h?ZJT_ex=86BA`=
zUzL0fFEv_Pe&HOIrl~0q&0KYa6B#EUP~b+k1+HQvQ!
z#UxhFcB~BbP_Z(t(F5WuCnG;@PJTgD;w2I_X58RL+HJ1|MKhLh@;JhEu%!(N{XD=#
zIyR|Mjz5PV`gaI1NaJP0c73)TAVeQ2og&2`
z-zc6m{-hZHWL)7=am3``CKW*uun)yaIES2o+df)O83M6`=hs>P%YDW7$fBC3lD85C
z0?7^^Ljq>F4*xb>Pwp@o;>Ntvxl(9Yg#vuX+Ws*W81v{_H>?dpjnR{8$a+UyUP9lM
zL_q6oaTbw|S4pOS-lT^^b1VFI|NKm>fz%=AN%p>2PMsctMk3g@3$;|tNjCIjzf3MH
z(%Z6>kxH5A>ed3mbz{_b+ry$3ugZ5w_m$C2$|AbWvM(S|G3)OkZ;19?5>1IxIecQm
zWIw+8V*cCZ=G1b(iJB2z_^IC*Bk4!Fy{7;KGJA32jy4qc*HM(qXa(uQiT&%rz_p*A
zYCe)w)K1sK>d0MEIEXlMuShZTsa;uUtf{DDD3^xbO8Uew|3)`6Bs#>^hm@O0&F>$+
zlkWvcR-O!$uw6?RrFry6ooM>^;|Si$_f|<%mZq;xa@$Y*of>V(R6iW?tYyV#@9q3guSig9qhqURcpH1UB9vEMF~8+duzdI=H20dGp;D`8z&jl9&jtmdHM;f
z+hrm0=b=$G-W}9)Q4ls<^{|}ax+1DfXI=U=@Jv;mCd}>_BN0^tmVl
zoY0-Lx8t&0(!pY9V;c_lp{Z)7AI9k8Fe=?YbZaG0sgZ5)6u
z3FG24tQhq{68waBShbwSpDUR?ilonA8Rm_u+LWL3A4WAKY^PI24JtHRa|7<-Ls?@>
z3Yf#YnkMnmcN?P4Q%?idiQH#0d`^YRfbWI5k26*LdVkANkT6%V7GA(scse|ohcXx&
zl6D8~2Adn?aE2d=$V0A>HWM)ynDE8(^PxDd2&%kIU^Ysk^a0y_pIQwmk?%lyK*=4M
zM%Zpy!lEqZUrBPlh!J(UFH&C^9*5Kk$54x7W6)vNd#FxR`iFxeB0B}$Qg^tdRRNMvj|
z4_MJ{D*#LMxe+1DAIv!oxYUkJ{EdTC#+3e4m6ceFiJ%kqy>fAXXDgYTf;i(BBU}~&
zG>PRSiw?9cf@H=mlzKpJ7mYPUFpvop&qV>fm^l&E9N=D{(mAYsnFl9i9iW>Eb6`#Z
zgpwQLFDOPznIvfW5^{R(PMnS3aj=hvx5y}(z2QuMLz^dH?ljBxGtR|G`1KDzzfG(b
zw=}>s8rQIUk!XZR*wp8pG-%bO7mX^}kIn#O7RvpFcMh_n%MFfdWQcZx(`|yctsFH{?a+9==3$fbL{#$fxMh*TBUeXaEZrerzK!F3m
z1x=ZcBIeilqM}FTj}$+XGByu`{n=x}s9#YxE(7jv&IZ<&GvKC!vtxmCOy_4KPM~c<
z8Zn`iGdCzK!GZ>>cDN!hL7MWi#LA#t;8
zdwg5_+bYfXNOpYr>zY8FQ^GqV0Sdwqzw#go*gR~DQbJL);E|8oZ+?8emQBnKyT%|-
zAKG8_-6?q1c5uQ?cs`OcmePy}F_Z-j<0?_{`7Fz=+xG5kG0!iVQ~lE%mZh@~Jc9-0
zFH)}e*>bf0*wf8ZBU>e9$5Vk9w-@kGcS$#T$|4WCa;CE@XRk9xWJdl$!
z3oWd}ymb4LLGhT;6z=98mgtRT=7-l@uxzDK0$pg)n1SSPS<293LoY%Z0TxZJdkt{i
zFX+otI$i0&V+$bGloFYE$$hNTT01$%8+q+*lwp_dv)I?-@IH;mK#ioPn_MZT9^kM}
zJVYiq*E-ol-tPF|Aa{%}F~`0cM4zr#_q>g&{P*JLv&8kOx?RTyM3l=$orQbyvlnG%
zwLL=GEn}an1d(8=@szMF+TJCs?>#vWzSH09^
z{o}#wpYQGJ2=qDcIp8J9b{QM#l-^q#uP}wq{Pp)PfPlL83ZTI
zOaZ2{_UBQg)9vD2A$;rzQ6^E>=uCOr+~qPDpNJ-Ad$PP`A0J`AOhO)c^(*jU@cqgr
zF8RxT91!=i!1+{aRC{<)>M}2Z>z%oE_1UelU#5|BkB`LgyyYDqF~RyM7>pL1A#!!l
za5||}bNl`Hfn2CtnEi{yI4cNAqP`eO?z}hbzebD969IDm$Ei?8(O)9wvNCzX&i&2P
z(D92lOyn&8(lOg#4HS>QC1{AqTe91ys?QhVXJB+-^zK~Kwn~L2hAuHY6~95b-d~|3
zX-z(I#4KJOb6OcK-|@KcIWNG+D%oCvGntxqp{HW
z+`0DY;YUP8U_X~nBT#%bAdEdc;6M~>*!=l>`N6UM`MBt3SUSY$bA2k{5WwS?D*Xhh
z?Rle8v_o%jTVe|^wgDuOn30pM|J~bTuoVX}iHtmaR(5~0Av_6lzX?mS-25&2Cb|L-
zR+ff<5EF+4cUwNt>AKi9HJrh_@`z?089?mj-Ug
zF{R5=`Lg8V(mT=H5q|vI0arPe^TA|xJm{AuO*-3}lKs<9Wav6Wj>bM
zpAWsC3kLFq%V7)3Bc}Rcp+9aE4rIc7-6)*HnGM2WmqCPJsD+OZe`I3--|;9I>NkeW
zrC&>FF?{m8!EJ<-Pnyd@&l-x4C>bLWmWs(rBP(+%E$BrKF}K=wJfON)Mwy-pLFAS&
z2s3()5BJQNCMWO{vX6>rZ`0%bzemLERl^2NmoR_e`&&LtnvgP7@y`Ha!kp_TEkt4$
z0MZ2vkhn;vw^5)+s8TVPu@*eTzsQx2`t+;rFt9{(gBI@*c2d$O8yA+9jzqen2UOok
zbks6m`fM`S1L5#Pg&KXG_JrwRe%YXFRj?(b9e+eV0UJVZ;0^j#lPe!oMofVr>{
z*af8ih&yP^IpZ9DXH!5H0aRk-?EcBrlw^7ZcLS^$2z4J_#P&%OZd6%m8d1&*LR)Ee
zcGh9*(l=Z>v;}iOlR_+g6ty+bRCdQ9_O>Hn(*mZ@)k{O@2!VM^vedGl_Dg}1g{%bR
zv)^RB3`oB(+Sc<@6)^5u>RT3DkVE@94@YGV=DAQ?;K_6ut~CD*#2lg+K@Gj849{3p
zV%YC2Hmm^hGH-(YjKoPL?8hNTJ`XM9H%!S`ZQn2RSZ2%GM7054poR@8D#%<
z45;1^62!YzeiAd%yvYZx`SdGPZEX_d5u3FyA>7Nb%dqqk6BuXk4L3mfI6zj3W+U=+
zSCeW_?n{t>i+m&wvV4Z$#JgojHC0_r_oJg%=$|
zO()ZHV{%~*i&LVu+wbYdPgeUAJa+$12ptaWpvnVumuih_JZ7UG?%Rqh-vismUWB0q
zB)7hq-zQ(PIUW<98e{qjp8bRJvrlP{eT3~8?}78r<*}JQw}*3Q7F=jy7w4gWue2YQ
zQ_l4A&gJYmsSrs#?rsBV3+{-YOEM?$inj+QPG9sZC92;~poE+|W|CI?Ja~5Xe3$4&
z9JeA=0`k9EysT)kRt-8c1CCyvlf>ai*F8h`jt4)m$$I5)ij$3vW5|lkXtsm&r6Qp7
zp_Y*Gj1PVFeI#oWkBFcHDc{G0MkHF`(BY`W~O4I^}?``n*jhX`VYB54M
zDHB17CiLN+WrW-=UqF^Zl}`1b*#>en*=V(*N?8SJa#hA7scjBGA~r$g9RA6dl#D{x
z>$PwL&E<0@n#f+I!MMgxj}sFYSIX_9N7hmLi*8Pm$hKZb&Ui`gOZFL_<1~lTj@DS&}>o;j1D0!U69!sN~UufSAORZHNF>2aJqg
zTrReiu;^4RTlodNPBLrI
zz%5J#JCE5Nqz)~u5MCg)4{64ZNgpW!>6XASRF5z1Bxbp98IIX{vu<8aN@R|zr7?mteyxfQsQn>i#fG9Md<8E%?Nhjs$
zD+<6te7Ca#T8*?6RfW{x#GQF4(>$3ji7|xHB%{A`*fX0+s*(yLlz=G!wJSNSksTxf
z{_EO=??k4eSNsQf5vZaNG=}id|B2x_92!SBOHOL(s~=3l=W_)?OjH+Y&@z|y8X0&Pz@Qbha9P-;YK$HZLZts
z7nM*V9SFIo{6YfWU<#MZ8KchDzC)T%@;;kUbtYxYF=<1T)O&E+XeyzjGLTcQd>dHY
z+ysh+1VK?#s*JUzYxJdz2~ZKmC}-}jUjY1AG9JZXR^8&uDD^hHsB$L~H2c1kzX4Fa
z(J~<~ZEDT-Gy7?ns_bHuV#zgJ_CGrkTy>!0j-n(mG1IkNOfJY*u3Y9hD>Zp8oP}};
z(klK#m&;H)&>X}Ueu_uV;Me{x65)p_lYiQ^9B#1YkjOmc9_%YZBnWKR&Alj~Wu|b0
zW^I!8U@Z%s*GL4sY+|p{$13RR7VThBTTAPaq_I<1~wbD{6Mq}`nq-(i-olGTpE)_WPd2&Tjg^w6ikK`PStAoe|K*v
zd_ZGo!2kZc;9uSW>M0a{eCfydN5PfYWInje&VAoHy*wq%T0^}$BabaPj{gi+YHb+!
zE~w6&=q0Mc+WBe%Hi6`3(r9Q!ZF~dRgWT)B)eV=sn%3bu;}|jc-*tGrqVFQU+Qkpp
z*xCN1MQs}VjOTgS`Q92NT*mYWI3J7ZMTVi#F^ke@!I9Ai1fEFKF%xdV<#7-^P-7k76xrySwdK1v>9|rAprX+`&Gf%S47RsxX=92+X0D2KgWC6o;BMsUYXz
z6-NQwFB_VMLyN|`2{Jm+ST(|Z%nF6cn8S6IIG`&Dobxb^!b9L>v2s(cuWN`
zSoLv@{VPeIg#v4U2hw8feS{vQHO5tNz8eK(vv`CWXK_Lir@0J;GD)oT%ocS>_bjSAWA@5=_nc@N9m6;+@l{;@xeKg+HVRnqb3hze`G_7yL{D)gp)HC
z6$=#zDo<>(`WPX5Kip+xL}}Gbu+zzMH48=Y@k8!|9fXx)$0$+?SLj<@gL_?YkPQ6{
zZ|3bWV}fFslD$Tscnh&lxVE~=2O{_|>V0VXgI$7S>C0{4_hLtkZM(zPDscxlzcAjv
z0uDz!Bt$D%`pLK>uL^0ulKXMSnMbD%%B3xlm$J`<0Cg;bOCVw~tq_G_B8pcx
zGS(OH>jKp+8j(_*NPp!gJq&|vbP7vMut!P??<1(O9>${Lwm2{0p`W4iD*t#CnSO8*
zbfv=4U;eu{L?|ZhLm6T(=?6{CNlR@n>GpePI`Bf!{QzIr5zRH8rh9W@FrA62{2rc?
z`sAIfVd_HUgY{83FO17@LWf&2guP2&JolvlZEhTuJ|;Ym5O>X)QFM@njr5mK{R?4<
zz$e|n0+u9oHtHG%QH3C8Bj$53nX7zB^2393#Xh}kbUE~)I&^F>#;?(te#o|!JoWNf
zIeFEBWU~B{tz`AxK<=@WmIE}Mfh1@`Q{;9XGt@!`M(C4PI2!ZYCUI80eX%OHEz!a(
zz6ns{QsRd!XO7YPiv?Y}V8Na+aN@#ZNIRlOx3ksl6x$N5%fq~ULMXi7ECy;@HNzJn
zWlXV?6R~oWbF_3Cw`s{lvXJDc_<)&bh*RMq%E`;q9)p|_@_8d=L_mLj2a^QsG9F!p^L`4VQyU-!ihq7(jds2u&y%lo44qpQ&bNU=9QO-K->y=cH$R^rjXW~AU5yj?62(THz_6sNy$ecnYaXIN$
zC3R#x4g8+cxRU{XAxnE28oWi7wV$l+Yu_VhEY0|>IW!e>%5SHP3Oy7{VK7Gy$Q!T=Pfv^^*6|o1kBy*R48)n#T|*OUx5fa1e*`dBXhR
zQiM7X9%txsCvU6*)Z~(fkyeo1>h59AIRr8%jB?0dx84vYu;|IBp)w@%$AtBLeUBS1
zrs|4kW8Fx70@*14ZZg9l;uL1uxouI4-!i+VkfNco)I%I1!OfVwx~x;Ij5K|Si9jWaTlOtwV3pFmuVbxbD{x_nY{vQ)!afnqF3GR5j@BTeqs&I0
zj8z6ZsIs6^rh2?TgT1ImBDMNHaV*M=f};#f&wo_Q(}|S*DaNmrO*G%cxV(($eWf}!
zr|2*W(ymnh*3>BeJ+1N|g}nQBXJX`xTqn?}_D66AEEI?%pP>wo`XfCy;I4X0tb*0%
zvFU1I)EENzTDdyHl10B@9D9u=A^Ax{`J{*H8#HO&x~55tncMj&MO}3)NY{jvn4{uB
zNUHLJS1@lE266Hfr3G<{A@0*_I502h#?{A$e2Ty>O`wMzd4*kLuZnx@q-^>gc7y(g
zheIpgr}XdHC~rh53b_>D&KHy&j7_2n?H|t+MZ>K@vGVr3oFVzy3~+@8$P
z3LpnIA?xJw{XTEbw&mfnns(ID_ou%Jt?C)S$p%U-4PTW?bF5U;f)`Iu=Tkb+t)h^`JB+{)4P{AcZ`rSo%a4|=};D>Kbfj($WxPX
zJt1Q?dEA9NU+Ls?^?~pd>Z_z#@!fA6w*(M)o&EdIcJ8-~bm^_24?P_K9rDF7&Wy#R$>##aSqUp
zO@uKl!0~usnN8#c4C3lY1xZ$4BK&M`wIkWfqO&lv#%~vHuAAd06Tu~NbydI2gcxkdE*cH$s>G}#A{h)x}
zW;(ux*^4e!)e9Ogo;DSD=J6!JFl#AfXKNc+o%`66<(PjI1aip)Gul`kg6vfpaLFG(9+y%WD&4_rO98PO>krm?2vhjT5!Cu
z5QnquZM9lNNyo%EkVpZJ#YNMBdKZVImM*;~A1WT|#LY((PPw`z%H0PM6J|K3%)oP
zmT<%e=^g7{UQ3nnA?^CL*Yg+AWl0T#n-_cr+pBM1tQ5!GIzK68wwKlx7xqVfUPyCX0kyQ#
z0##&EaxtK~fC4=8ma(5S+w%kvv;e`xgsM^884*BxD)i~|wW3`TqcafUW7ub_ASUx&
zLVODAWFr(B;Vi91dbQ}-85Vxm8j&>Ay%4=7)ZzB8kJTQ>NtD<#(!vLN$_Nu9j?nn(~sJmu>hKWq7YS89v&8gpS6uG
zxsfISc+|>;WnOhhQpo7{tx@|anac0G^eKGaD6g4VSR3?MRjv}q@wGQMvwCRufUzxv
zQLX}EAuDR=0hA*M#+`>I+FQ3{9mF7D*3%prSPFDBH1rI32Tm?k`WRlOc;oc3i>B-_
zR3tusF5Ldag53I$F!N%AMj8uBdQsF{=m!7v#jltidg~-O=%oOolm*EP7D==jV6Nt`
zUdZzlRU&)>p>UQSc_+Y#Ne*)|x~Q28)0*4jr|9T#PQ^w%fIN#+T*cSxwc!H>bbb^dTSdgC-ma{R!kR^lyT=>-pN|qoiy%
zh;pL{a&*&U}
z_8u1PHTnPm;%hBZ224=m=x=r`2vOeOjo8d@ugP2gkN1uYK~C@+1ulv#Dee}56Oi;w
zv9HDX_f^?>O`pI}dRd#u{1X6RY;p3kR>Ab=i@GteVRaVw)N`Q)cHo%*U4RC+(Z;hQ
zuDsuYlT|r?Jojag2^_B8mV6l=6XnjP4pk7MPER=p10OI3l`DFS44AvpoE*u&zzlQ5
z^tuvEE`c2laNW3wiPp+wYA>YcgBfDDQJ#2N={;FzcHm5=!=6>=OXU-j#P-2chW>3;?jab%J0n|xp8k$=zo;jayfW;>yGTSY%J`r
z4N`#}{F(Ifu*_+f)5<$@6M}D#anD~f=tx4H)QUdGX$qoV`wXV%YPYUF-w>%5P*l#w
zhGS)u5~#A};sr{OOoHon96~B^M0vX43Ou%VyBf@!Oq(4RB_
zGRI?@{_+=Ze%FOCu6(tN|9roUI&c1i1Yf7ix)YRV+<@JrnUYhiY94_I;D#7iNO%1
zijT_<@59SnI#P-=6B=I}=>Rbc*giI|8kF_+Yec(*xnCeL3n>-6Y)nbIT-n6DUHq8k{75yo59q<821yN1%dO)ER|-lbK%(#>#tMg)9QbRr-i
z-h;;@O$)}LxyhwA*E|#@>GQW;P
zlgx(Z)$~+t6>drv0^%jR?bp%&ZVDkdQCC1152y&1OmtRSIJ|m1lH5UJnX_O$PuC|&
z01B|!Y1^4yN`ikc=d_TM9E(j6pgWH!c
z{-5a%YBe8i3M2I@77jc<=G(jjrfxFB!!PSvU;x)PkAYG%ou6rpz&GJ<^>RaI@kU7L
za#d<`%}Nd4zV0u6f@3`%z?r4bU&h)=Ecx~80ua1$v}FGd-CCSf@v5l_B$Uf9cBn*o
z&w}xXfaHp7x&=fiHVD#9Z0sy))BDZKM?(M3D;)-A;1`H;IW~U)oDZINmu)+eeDg)5
zCD<>JV&ZI{lCxf|MSlhU#9q5_czw@)(Z!hKB04FO%qS{|K@I#G=f=J
z13^+X@fEr+7t#{Nv~njCv2yRI<&9hufql6jEb(S_222!SYW<}Ce{V|1`p>C)1S=&^
z?tcc>W{BfTW&iWFZVI!gQ2U?Vwa&ly|L20uOu15(_MeCKwkjc^t{&-gg|&qcZ~HS~
zM_8s~+s4Y3rL;Msv$rCWJ7|0vm-0-D>KXL=?nq>a@9XySUb3X;GI6Oi@#ARq(4!lKU=V}f6JQybP>E^*ev$%>H`Y6-Q>2(l
zmq|=<>3-zKA#K!_;I={?DbE-CGGIYLx}S_572%h3492jftULL?6NR6S62Pb(MO
z)4T{0Eg-d||IIfx$SzkYh4oV|Tl)>
z&daO-X9#^rw%W+h7kYXMCA<%R6b$%X(2%MNAB{dB2zy9`fWtqgqH4>u>aq#KE6n&k
zsJTdPz$wg30bgnXtZr*vUmf_w^!bFIZ;0e3P+^A;OyAAr|6UY}q9cB
z0p^Dtz;u%>vP9-uV2v(;&y#;Yft%x3{=F^Y!o=SK_7JMCb
z=KvHhr7$lKvp($ed=Od^PRB5jEjmeHV=S5)RFqC)9X*ZE7t?&8nI17K3zGb{z+892
z7SOD`x1%@)?Gcqftng8XRw-p}YRQ-h}aSP28)dhdI0^{l-
zaV*0AM1^nmMIyE12Lvo0j~C65|3TF<-AdMxXnL
zeW7J7N@dM}l3^c6qEtrdi7S{nC?GynxLo)czxi7BjDTc*4
z@p5Ih@2%`x)?m)LufRmfu*s^2jtxaXkJ>qlZ2?F#*s7470D*Ma+5ahtCi;14q
z9vEH&DCicpDTq`G=lq39v~@~GzC7Od~=r-cE2XHw=$IExMkk}Kbu~32JB0!RP1HY6*aGCpH
z^cN9cA8(`=B{R7(_6}`!oauC@OaL!9fbN4%gxMkU>~e
z7*|3v7(1EF{L4Wl13$@wyU6GYYmH+s9@yj4`r
z?@A4*JUI)Bkjb
z8=GIwGlq&?b@Sw>B!bvuldr4OwcWWoF6;{W3d`#nvl})WzL5vvz1m4B2_=Nr)*44u
zHpmrCm`|<#^h(}~w?r$mBb1O*QWtAMRw%RH?r6wlf}5I>209o!-p#D0pICU`H8~w*&%GwtjeMI~e*27XW>t$}j
z{wOR+A4yOdhk@(vp>axmV9#-6A~sfV;>nppNjrAOW1VAcuu^(n{_Pw#tXMsr+wG-(
zQhtEdah4_E-zL<1T#)?vT-VLge!97CZyb>5-GsC&fB!MyP-$v3g*@Q76~({Ck@39V
zO<*+Nn9RLXxbq-+j^q8;b-521FIx{hvN)~O>-DZYzUS}aq&u){YbqXtHE$?bYuvwj
z;>;H`Qp$Vt@S!|@^Lq7p^ZN61*fGuIR*KOrM4xC){QETfVoq4Q*$Q|$Pb-;5Z>40u==DcjG$Ix>|LMnE
zRxyCPO4C9Kdo7rwYR^c95HYAqI*OZ~_BNhkKMe)j#YyfE!E{^sP3vb1&Pi8!_vgy)
zbWTbeX>r-=J9~6zUdw^zig}f*mZAPwS-92{Zqt6SQpXO`bz-?KtckoOQ!;}HbhOKz
za*x4@IRfVOi*`e5mPp0*!fzTb#
zL{Ck2CoTngFix#0@H}SGH5-$|bydNEC3!~Ehqyw-iBQ0ybfma)^b4cmUMbFcT^gbo
z7nZnM@VG^@4pk^JQ;ffhCC_||M}`B~tGA)&$*rEwr2PKJlcyZPndS$yW2CB&qz1aM
z)225&yMfjPQ!-Au;yOBs`u90&Va<;!TU#J%Ac3gw?FM`-IeMTL!g1>h;6L1bu(m#&#SUTHB83_RF8Uh!<8nEu+%
z7P2IG6WHYyy1uC}xBpR0UMVv{E=B22EpN`k@ti$5)WGAXM~t1U;!p*I_g~lvk`{C#
zv@|Pze(y?x1+*Rrffh43wo0}ZNG9E#S}uvxdT
z8r#VB)m(HK#yt7h^t7f-vI`$c%tB|tVf|N49m77)-{8q7wwlfn@b}W0hUDPzo|^}*
zfSpxL!c}G;(&O2}7R3tB@bD^0R6}jY37fhmk9B)}>OO^mude%>)rI(gmXA=QNZN$g
z7T2m~E$w%b-O8rxa81&BWk$4mi!F}umd+|YH;R1yS=8l*Eiex$M^sa0HULX2XNP(u
zbamJu3$ZotlLx6@+N({TPhr^Td0q6#pFDS3Dpe*4e_1Z4lNvhJONz*{OW>&PQ*)qsxAg1Tq)w;Q8|iQqFnH_H6#GSI4hvj|c;F
zcfHfKQH9cYonBDrjhkdz4a-|(E9Q9jS0iT%<7$y>$+7(^8r&+)!u$r8N*3_dF4luM
z!5+@>kwF}R5}m>kTI1I)0PuNcon`&LNLv`GIZA(x5W-~RITlY~-tTxwFA!ESLOSK~TD4uO&;hn>LUOssnyQKYx+
zblpPJD4r~XV*zT$arL%(@gymx+mOrP
zl!+2VHMG^om3_D0l5kRLRzhtQOg((Y|5IGw=?2$W&N%1*uwY%%NS
z-Xc%B&b}EEy$mdusaTWR4Rr-;?Bz~_`Z7v4Uh6fl`|{pc_bS%;9C-LH=b(bePBN*Q
z0dGgoYqX1{FRITCO~L16b-L8X_2&Tg{5naSu7(<3Ux~icPyNM?8HcvkKzxB@@IkRe
zQ!F(KMpqno6KDG#@{C8!%RqilpHZc31%*LkZQVllo&1L36e5ho*|#m+nq>5Z9Cj&z
zOhJzWstlQw4wn`6FH{=z-X5F3=3|1-vpD95zF+&JRjB9F5TVqXOMo>W&r+Zzw1o;_
zCg;07d&u=F?o+;PQIl*H$1w;h9=HiWW!%4MSpP#_8xwD2tD)4nC)tlwlT-XC-Ed
zD6tN${GXSUkBY4Mr{lilmJjaj#;?jNj0MFZbPol$fTlw$BUpfk%g!+N27`{(kJn|7
zz0=>WO-FZ{R%v6nYyVI6XXTOMp@rW&zaoX+7$*pbay`i+P@WeGH%fH!|Bj~XKGJ0k
z;9@oI@%kN1y{h-xX&n_8Z*tNa2-2+9+S)rK;WJ$>Z^zN)7u82jrl!lVx6S&`qBTqC
zP)VTHL#ShaWd*rv%M&vZW2cb>IP82Uo
z??x4%0jH()s?3^yCnb;EjO>j+hlqv^r|u#;&%mTl;1x535(!s;8ZgkI-56F;eG77O
z&v6gr@2&lUx+myGR{8YIb~`pFNu>E1oNvR4TBva0yQ#```;JcYmTmLw?)e(R
zo&xK&R@mqt@mW7``!DUCW2gKYCVNESpHB?Kz`{O0FBq|MUVYU28R75H>F<1Z&`PVW
zUneWTL3Ti7l~7;6T^@k7gFqJ%BbTx_JQLvI`zi3MWbmFJycxQRGuxiG=|*?P9CvkW4%whn2ys(kGuk`cvPi1}291{0*IoLdhLt
z)(3xsPdiVo2Nm_`y1R{BLTs7)mszMjK(0R9a1|%K%1ohL$=Bg>IkH+NF_eU<#y$dt
zxao8w`s8AG=rmA6L+$#gOPi+nf%-tAziwF;{cgLa`{(#1o7?*y{^aXKFQx~0im)Vt
z7R!(^hJcHk*`dTlmUn8^`PI&x%5lL5!-yrLio?VGMME3IS>^%~;q!M!9=Vq
z->-S)pK^C^W?I*J@wMRdrM05|xLwS9$~M~mL35;FXZ{2#dr5SwwEn?{W8i)VBm4v6
zT37YHj~q;Q=4_^>EiF7GKQD4M*;}R*c?86b`BrdL<=l=%&25iWxn@PHsYl&Qu>A+A;jm$T(1edMKrYl#0
zrpfNAtdAa=B}+9d!`_`&IWtzd9M-|i5K>#tm5n+lOM~gI*&M5hWxt?@6TKoOd=w6>
zmvKfmJ!su>vXk>&e1&}e>L5@es_Kh)?ww_0mW+xeH0nD(mA(;rb>#Qj^Eu^Lq>^Ep
z?I%yri{(wzLzjnrA61xxLpZ$SH%Eue8A@br7pxGu$J8q&{741*h33q+i9!)Hu3tM4
zlgtb9=;v1A#vbq*bV#*o>crpId7pe!b^0)1<@smd1mZIeSkMdIuEz=X-;efY!xo6io2
z*~v*qP)e7uRH3^4y1--)_2cd<%Z-#cRD!;0tCq=M6XO9Q^KmO!pp#n>Z@~^N3JEwJ
z-op?P__&t%zZ=|D*HJr}ot%A7kw-u}7jStGt@~{+&T1+HG??QSNOE#LcKLi>gI`rl
z&mUh+k}J?jkAiw7E{9L^;P;M&=4hvI&PyAgDrjcwK_c?kwjra>_3)(A{NTD2`_^I}KB
z)+AQt6gsl#_OXJ6RDf2x1OtaMvuf64!+;i@Iu3W~9XZ)0C8Mrh(1Gs1zTN-5wEdc5
zVJy>Bd3-GC{bpoS_Z`7f%JcnM#Xw
zCTMZAIqB%>J&WS={Wh73xxCo+;eVGbu?+3-jom@ni5nVW_dvc7hjp9%)}#0(aCR}K
z(8v2|ang~PV0$nr_@11(#Lvz;jv8wYtARg6LB`|!*$oP>hEIF{7q
zs>Q`Ni(bSzgO3`gB3=&jLtII&W2RFUBG)F;`(%)#>>4_sK=ic|8{y_{T7O};CqrZS
z5f0UM>?kemwJTTSq2J=6NlgoKed)w&LeX$v
z`0YnK9Y?CLPYu7Wg{NdrNXh|-!PqHH{j3#F^6UDYD!X(dSIWF-5nZ_HuP7o})H3_0
z9k@@DbNAOx=*7tN`_S;PI5E?T4X2~>k5UZ3-F#KLm!^6
zwYB<+M4whnKcT`tk=Jwy6ZTwEP|HEY)y^O@J4@)xOKr4OELCrsgy^**Xxyj!HvyW6=v%
z6AR>fDBter8OuZ^{vU7u6j)dHy@A4MW1EegG`5q*X>8lJ*~Vzt*tTukw(Z7F&dU4$
zeV5P+LH^fqB
zRTKW|y-E_d1EV7|W8OvSb=QAjk^Bfv31)_0>Fr&%po%H{!E{l+dl`=BN+d#$
zb$g|8+kmH(clqth5yyO
zV&dnj7uLHw;zcZdxDg3NsmdlTVMzuPH+CrymrHZ+TUKmp2Y49uslcye@RcQ6l9X?3+=r#%co
zW^9Rv5yyKxVA87Igd0QF|Rfir=q6P4-SZ(2#>GP|C<*3K9qluhZ
zq@BJZ&q8&$|DFBQ6*bdALf?tywIx`iuwk9H3D3i%+`Mv`@19P)T`{&+-6vo~0
zgEP;JB|Mj4f0MDWPeY}1hLGqKgaHMSPWP}TB3m#K1;nHl^JoCl5_i8v1;b(AH>4rt
z`A~?xfkEPCa-gLn?V+beDx}`yB2_!
z41?u3LM9}8gZR{y_|z?Hy|)p*Qt}2R{k?Dt}X&UhRu*MnTD-jUQX-a_Hlbq-tZK$^Fu!KkIU}DI#{c%
z_wJNuH?W;4u?$`MXBtU=@`M%4JZ+MYc^z;vx5YWK>c+x!S`-Lr27BBNiFIx7V+Ae&7be
zfC-xa+GWU+oJX6nZlimD>nM)WN@H%j*79=H22Q7{d2Lot?Q!Xb0ZI#T4fFdckS&!~
zNsYzM&KTv5QpTy7cSjhA5gM?7fQA41cB+IDci9{_P
zBJKh2J4#P8%K>_phnX}^E}7S97Cg&oxe>KfdFuvvvMtbxS~ji-rTe6yS0oiuQf!*)
zPlS*5K&0J%&WxM<{;cY|egEh<583TROcZd7@i)yTlfVH8q-}A%DiM!|2k>kKiaAfZ
zI^;>1qWQkkK7Z{N@c9L7q(lHKIpWgh&VbL~6Ds|!aa*g7bK2^n5-$@F+ex&hHJeDW
zx0wTRFRV_})qEFG@0m+tgLEw6lz~ps)ahie(+_)heS336A2bSTmtdvxAMY}s0=NB9
zV&*E0!rR(gZGM;<$}=;AZ^c^SDNdgVB5S}_#|cp?WLB!JvIz^J7$_j{arDYcIK~t
zR7-65Af{al@m>0snAS!JDv3KWA`>7u=<^^=pC9tn5bzO^0Ah9R=4~y)9ygfxm7gn(
zE_fA`Xyy}*Wta0wk&1Z^Qz^9`9+_Wf-IldkYjSp7DQm{C%!71jI41m@a&`G(*)I{XKgy@Q&7
zVXEj}cyJpZz|8U>{k!$*MJ_Zo${YSvo=>j_;q8Q9`l#K!iZ<6;FljDNyMPf)%|p@DPPC!U?qU4wcZYX*dOinm$xEBl;PPj)x#s2
zHmEriiC
z_`?7Q;}9!w5olNDkdL5_P3z$O&FbHF&U0J7(KcW894zLq_$f^g1?&|eVtnl128Vw5
zYPUaG|1futTv<}Tyc8CnnYv&>C@$%^KHT8797^E$P;aU#5`G!*z_So!`qC`C27p6G
zZ@vK1uI({c!|X`<_O+IC#xr8tOrvq%dHpSZ=5eQ$Aj87z2lbwj1OFbMZ`bafdz((<
zVDAyGd-to{5BYw-Y>&*Mvx1@~p5_l-M@-)2MBOd3N(>qChM5d%Al1YF+Dp4W9-qN+
zw@sjJqZ>>TE#DOZwGRGw_zP{092)XUBP6lCwSMY40+hRi)qy?MwtOn;Rn5XMa
zwdrCz%495HiHdQQH5C};E-%HzFU>3y^%;Pr^_1UI|5305&_1`zD`|6K3MV|i`9RVS
zX7ZtA*`4$G6CknpOOH~98Q*$O;ejM?amniE&DG|qPLr|B>#k;So?s*^
zvx0aEqsdeR^mQTk2gQU?`|h|ufR_xK4-7@yfkGxSnUbmtX$rD5_*~z0-Ufe;5r
zLGynNbhy_z`@&imvKav+OU=ZU@1{yD+B*q|)`@vpu^k*UH9J{Fz~`=RGAB|xIh?`u
zfZ!SENMZ9_|Nd><_skd=&X&64ENnxF@g%}jolSy|>B!B9M_W8ie)FXw#?RV4dJYk_
zUVC*FR0XzC?AHRQidWx*Gkw!%&Oec2t++@dXAVmi!?MpVJ9T9}M!Afhd)i0R%duZ=
z|C*mC162bJHp10tjUXs@R#WMyWv-e9b0K)8~c9XJBFgIDLwDJUtp{|t%u6SN`!
zp@h@wRO7rf=)YumL&^rsZ!nWSSS7I{q5}Ac9yZ`e(;&g=lK_}+QRbT8aegU|ar|dG
zI#BWXyb2W91t}pnWr7Xw{~iKZ=V9JMbcC-&<6!uom+PC)@#gePTHfd8WpYMNz}%z^
zi{5rc9adUIlH92ad23Gk%TW`8vRT2=##p4J6`7PN2ZlPLapx
zip3jqJDL-5x3n`xuaLP?-`Qd5yVLw&wc5iv7btvInu%caH<7~iA9mVWx{P%erCWO(
zIlVEg?S2((@5CnZ^J^{Je{Co(cXQYo6x@WQKX2pRL?O
z!H}?Z0el#kT+pRLOMNUPIwP8Dr@&LOO#P|(>h(aMU%RYS82x{86>0CPT|GDbIG+s7
zj0Dn|B_(2*35l(3Qdkluj84MiroK;`=5$#s%scc(!2!r75k6|043--JDn-&xwJ_&%
zwT`GJr9<2;JP@2$V=yIG#tI}a>Bd8h1q1u#Eb6csXASB@!)I{`-i}@L?ME~jAFM=;
zmd7vCPk9J1Nt@6l{=wci6xh3RqfXL`(SFR#Ar
z*Bza^p&K6Ekc|!x(fj>lDV%Fb0#00xsSijmZ&$954|2`v_xg^n4)Xc+=S8WU=Qk`R
ze=n~|usVEvk~QXg*EGf+>(*S6gnm>Vl+jgvvua;!m6;}tl4&f2WG0TfxMp*>?H9@v
zYGn##u6x;Thbh}}@Z7P2nlhe>kmxRAJFDTwh4ft=ydaLDm9cW0JHLm=QiA3@c?*Ox
zCqbirUtEAJg65}z26rm@lKRf^V*9b}lz%c1GjDR*9ifn|su8;&Ya=iNKfP52_B(q7
znKhc6PF{3j;ZJcY;Ys&3Fy4bgqu2D&jjq&05C|*StH6y;)!z5=hl;*yrywsfa~!C_
zm6ALlmdZv~!z}dcdbKEpr*K#o+-JEP@4~8qw>klXOUmq*Y#;v4Isk_hVl#gk#w}M_
z-F&+>ZEs)cvnB7$61V3F8>L=bNh%CDl78D0yG0j%-d#!D%-x#|_(Jya+UbMEQ%5ag
zijgnvn)%%vXGTa3dT<*6?DEk;d(SyZBBN9Y)J`4@+_B�Z$TLDX@{@{0_3GgheDo
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%0Dg423o0@Tk6X
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
J;))Uo{|4mkcuD{O


From ae028c3343b27f98428e7df982d4b12e9bc9d72b Mon Sep 17 00:00:00 2001
From: bretg 
Date: Wed, 2 Dec 2020 10:23:26 -0500
Subject: [PATCH 034/129] Remove out-dated differences matrix (#1045)

This isn't needed anymore that we have the website feature matrix
---
 docs/differenceBetweenPBSGo-and-Java.md | 46 -------------------------
 1 file changed, 46 deletions(-)
 delete mode 100644 docs/differenceBetweenPBSGo-and-Java.md

diff --git a/docs/differenceBetweenPBSGo-and-Java.md b/docs/differenceBetweenPBSGo-and-Java.md
deleted file mode 100644
index 8d1eb1023da..00000000000
--- a/docs/differenceBetweenPBSGo-and-Java.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Differences Between Prebid Server Go and Java
-
-January 24, 2019
-
-The sister Prebid Server projects are both busy and moving forward at different paces on different features. Sometimes a feature may exist in one implementation
-and not the other for an interim period. This page tracks known differences that may persist for longer than a couple of weeks.
-
-[Feature Checklist Overview](pbs-java-and-go-features-review.md)
-
-## Feature Differences
-
-1) PBS-Java supports Stored Responses [issue 861](https://github.com/prebid/prebid-server/issues/861). PBS-Java [PR 354](https://github.com/prebid/prebid-server-java/pull/354).
-1) PBS-Java supports Currency conversion. PBS-Go has it implemented, but disabled by default(still under dev) [issue 280](https://github.com/prebid/prebid-server/issues/280), [issue 760](https://github.com/prebid/prebid-server/pull/760). PBS-Java [PR 22](https://github.com/prebid/prebid-server-java/pull/22)
-1) PBS-Java Currency conversion supports finding intermediate conversion rate, e.g. if pairs USD : AUD = 1.2 and EUR : AUD = 1.5 are present and EUR to USD conversion is needed, will return (1/1.5) * 1.2 conversion rate.
-1) PBS-Go Currency conversion admin debug endpoint exposes following information: Sync source URL, Internal rates, Update frequency, Last update. PBS-Java `/currency-rates` admin endpoint currently supports checking the latest update time only and is not available if currency conversion is disabled.
-1) PBS-Java supports IP-address lookup in certain scenarios around GDPR. See https://github.com/prebid/prebid-server-java/blob/master/docs/developers/PrebidServerJava_GDPR_Requirements.pdf
-1) PBS-Java supports InfluxDB, Graphite and Prometheus, PBS-Go supports InfluxDB and Prometheus as metrics backend.
-1) PBS-Java has Circuit Breaker mechanism for database, http and geolocation requests. This can protect the server in scenarios where an external service becomes unavailable.
-1) PBS-Java supports `ext.prebid.cache.{bids,vastxml}.returnCreative` field to control creative presence in response (`true` by default).
-1) PBS-Java support caching winning bids only through `auction.cache.only-winning-bids` configuration property or request field `request.ext.prebid.cache.winningonly`. PBS-Java [issue 279](https://github.com/prebid/prebid-server-java/issues/279), [PR 484](https://github.com/prebid/prebid-server-java/pull/484).
-1) PBS-Java has a specific `host-cookie` and `uids` cookie processing for all endpoints, that sets `uids.HOST-BIDDER` from `host-cookie` if first is absent or not equal to second.
-1) PBS-Java has a specific `/cookie-sync` behaviour, that sets `/setuid` as usersync-url for host-bidder if `host-cookie` specified but `uids.HOST-BIDDER` undefined or differs.
-1) PBS-Java has `/event` endpoint to allow Web browsers and mobile applications to notify about different ad events (win, view etc). Filling new bid extensions `response.seatbid.bid.ext.prebid.events.{win,view}` with events url after successful auction completing makes it possible.
-1) PBS-Java supports per-account cache TTL and event URLs configuration in the database in columns `banner_cache_ttl`, `video_cache_ttl` and `events_enabled`.
-1) PBS-Java does not support passing bidder extensions in `imp[...].ext.prebid.bidder`. PBS-Go [PR 846](https://github.com/prebid/prebid-server/pull/846)
-1) PBS-Java responds with active bidders only in `/info/bidders` and `/info/bidders/all` endpoints, although PBS-Go returns all implemented ones. Calling `/info/bidders/{bidderName}` with a disabled bidder name will result in 404 Not Found, which is a desired behaviour, unlike in PBS-Go [issue 988](https://github.com/prebid/prebid-server/issues/988), [PR](https://github.com/prebid/prebid-server/pull/989).
-1) PBS-Java supports video impression tracking [issue 1015](https://github.com/prebid/prebid-server/issues/1015). PBS-Java [PR 437](https://github.com/prebid/prebid-server-java/pull/437). 
-
-## Minor differences
-
-- PBS-Java removes null objects or empty strings (e.g. in Go `/auction` response bid object will have field `hb_cache: ""` whereas in Java it will be absent; also `digitrust: null` in PBS Go is not there in PBS Java). PBS-Go [Issue 476](https://github.com/prebid/prebid-server/issues/476)
-- All adapters have been ported to use OpenRTB directly in PBS-Java. PBS-Go Facebook AudienceNetwork adapter [Issue 211](https://github.com/prebid/prebid-server/issues/211)
-- Java and Go adapter internal interface returns currency in different ways:
-  - in PBS-Go, the adapter sets BidResponse.currency, which is outside of each TypedBid.
-  - in PBS-Java, the adapter set BidderBid[N].currency.
-- PBS-Go use "60 seconds buffer + {bid,imp,mediaType}TTL" approach to determine caching TTL period.
-- PBS-Java has different names for system metrics. For example instead of `active_connections` it uses `vertx.http.servers.[IP]:[PORT].open-netsockets.count`. See [Metrics](metrics.md) for details.
-- PBS-Go `/openrtb2/{auction,amp}` returns HTTP 503 Service Unavailable if request has blacklisted account or app, PBS-Java returns HTTP 403 Forbidden.
-
-## GDPR differences
-- PBS-Java supports geo location service interface to determine the country for incoming client request and provides a default implementation using MaxMind GeoLite2 Country database.
-- Different checking of purpose IDs (1 - `Storage and access of information`, 3 - `Ad selection, delivery, reporting`):
-  - for `/auction` endpoint: in PBS-Java - doesn't support GDPR processing.
-  - for `/openrtb2/{auction,amp}` endpoint: in PBS-Java - 1 and 3 (for each bidder from request); in PBS-Go - doesn't support GDPR processing.
-  - for `/cookie_sync` endpoint: in PBS-Java - doesn't support GDPR processing; in PBS-Go - only 1 checked.
-- PBS-Java allows bidder to enforce GDPR processing. This information available in bidder meta info.

From c846c8b4dc4c8670235f1826c93678acc9d21438 Mon Sep 17 00:00:00 2001
From: Serhii Nahornyi 
Date: Thu, 3 Dec 2020 12:59:19 +0200
Subject: [PATCH 035/129] Review deepintent adapter (#1049)

---
 .../bidder/deepintent/DeepintentBidder.java   |  48 +++--
 .../resources/bidder-config/deepintent.yaml   |   3 -
 .../deepintent/DeepintentBidderTest.java      | 172 ++++++++++--------
 3 files changed, 134 insertions(+), 89 deletions(-)

diff --git a/src/main/java/org/prebid/server/bidder/deepintent/DeepintentBidder.java b/src/main/java/org/prebid/server/bidder/deepintent/DeepintentBidder.java
index 321a220c25b..493db8e78ee 100644
--- a/src/main/java/org/prebid/server/bidder/deepintent/DeepintentBidder.java
+++ b/src/main/java/org/prebid/server/bidder/deepintent/DeepintentBidder.java
@@ -1,7 +1,9 @@
 package org.prebid.server.bidder.deepintent;
 
 import com.fasterxml.jackson.core.type.TypeReference;
+import com.iab.openrtb.request.Banner;
 import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Format;
 import com.iab.openrtb.request.Imp;
 import com.iab.openrtb.response.BidResponse;
 import com.iab.openrtb.response.SeatBid;
@@ -55,8 +57,9 @@ public Result>> makeHttpRequests(BidRequest request
 
         for (Imp imp : request.getImp()) {
             try {
+                final Banner updatedBanner = buildImpBanner(imp.getBanner(), imp.getId());
                 final ExtImpDeepintent extImpDeepintent = parseImpExt(imp);
-                modifiedImps.add(modifyImp(imp, extImpDeepintent.getTagId()));
+                modifiedImps.add(modifyImp(imp, extImpDeepintent.getTagId(), updatedBanner));
             } catch (PreBidException e) {
                 errors.add(BidderError.badInput(e.getMessage()));
             }
@@ -69,16 +72,35 @@ public Result>> makeHttpRequests(BidRequest request
         return Result.of(requests, errors);
     }
 
+    private Banner buildImpBanner(Banner banner, String impId) {
+        if (banner == null) {
+            throw new PreBidException(String.format("We need a Banner Object in "
+                    + "the request, imp : %s", impId));
+        }
+
+        if (banner.getW() == null && banner.getH() == null) {
+            final List bannerFormats = banner.getFormat();
+            if (CollectionUtils.isEmpty(banner.getFormat())) {
+                throw new PreBidException(String.format("At least one size is required, imp : %s", impId));
+            }
+            final Format format = bannerFormats.get(0);
+            return banner.toBuilder().w(format.getW()).h(format.getH()).build();
+        }
+
+        return banner;
+    }
+
     private ExtImpDeepintent parseImpExt(Imp imp) {
         try {
             return mapper.mapper().convertValue(imp.getExt(), DEEPINTENT_EXT_TYPE_REFERENCE).getBidder();
         } catch (IllegalArgumentException e) {
-            throw new PreBidException(e.getMessage());
+            throw new PreBidException(String.format("Impression id=%s, has invalid Ext", imp.getId()));
         }
     }
 
-    private Imp modifyImp(Imp imp, String tagId) {
+    private Imp modifyImp(Imp imp, String tagId, Banner banner) {
         return imp.toBuilder()
+                .banner(banner)
                 .tagid(tagId)
                 .displaymanager(DISPLAY_MANAGER)
                 .displaymanagerver(DISPLAY_MANAGER_VER)
@@ -102,20 +124,23 @@ private HttpRequest createRequest(BidRequest request, Imp imp) {
     @Override
     public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) {
         try {
-            final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
-            return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList());
-        } catch (DecodeException e) {
-            return Result.withError(BidderError.badServerResponse(e.getMessage()));
+            return Result.of(extractBids(httpCall), Collections.emptyList());
         } catch (PreBidException e) {
-            return Result.withError(BidderError.badInput(e.getMessage()));
+            return Result.withError(BidderError.badServerResponse(e.getMessage()));
         }
     }
 
-    private List extractBids(BidRequest bidRequest, BidResponse bidResponse) {
+    private List extractBids(HttpCall httpCall) {
+        final BidResponse bidResponse;
+        try {
+            bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
+        } catch (DecodeException e) {
+            throw new PreBidException(e.getMessage());
+        }
         if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
             return Collections.emptyList();
         }
-        return bidsFromResponse(bidRequest, bidResponse);
+        return bidsFromResponse(httpCall.getRequest().getPayload(), bidResponse);
     }
 
     private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) {
@@ -131,9 +156,6 @@ private List bidsFromResponse(BidRequest bidRequest, BidResponse bidR
     private BidType getBidType(String impId, List imps) {
         for (Imp imp : imps) {
             if (imp.getId().equals(impId)) {
-                if (imp.getVideo() != null && imp.getBanner() == null) {
-                    return BidType.video;
-                }
                 return BidType.banner;
             }
         }
diff --git a/src/main/resources/bidder-config/deepintent.yaml b/src/main/resources/bidder-config/deepintent.yaml
index 4f5896f1bb7..10182ae49d7 100644
--- a/src/main/resources/bidder-config/deepintent.yaml
+++ b/src/main/resources/bidder-config/deepintent.yaml
@@ -11,15 +11,12 @@ adapters:
       maintainer-email: sourabh@deepintent.com
       app-media-types:
         - banner
-        - video
       site-media-types:
         - banner
-        - video
       supported-vendors:
       vendor-id: 541
     usersync:
       url: https://cdn.deepintent.com/syncpixel.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
-      sd:
       redirect-url: /setuid?bidder=deepintent&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=[uid]
       cookie-family-name: deepintent
       type: redirect
diff --git a/src/test/java/org/prebid/server/bidder/deepintent/DeepintentBidderTest.java b/src/test/java/org/prebid/server/bidder/deepintent/DeepintentBidderTest.java
index 14a98370c59..b10e41233d0 100644
--- a/src/test/java/org/prebid/server/bidder/deepintent/DeepintentBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/deepintent/DeepintentBidderTest.java
@@ -3,6 +3,7 @@
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.iab.openrtb.request.Banner;
 import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Format;
 import com.iab.openrtb.request.Imp;
 import com.iab.openrtb.request.Native;
 import com.iab.openrtb.request.Video;
@@ -30,7 +31,6 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
-import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
 
 public class DeepintentBidderTest extends VertxTest {
 
@@ -56,18 +56,78 @@ public void creationShouldFailOnInvalidEndpointUrl() {
     public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
         // given
         final BidRequest bidRequest = givenBidRequest(
-                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
+                impBuilder -> impBuilder
+                        .id("impId")
+                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
 
         // when
         final Result>> result = deepintentBidder.makeHttpRequests(bidRequest);
 
         // then
         assertThat(result.getErrors()).hasSize(1);
-        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_input);
+        assertThat(result.getErrors()).allSatisfy(bidderError -> {
+            assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input);
+            assertThat(bidderError.getMessage()).isEqualTo("Impression id=impId, has invalid Ext");
+        });
     }
 
     @Test
-    public void shouldSetDislplayManagerAndVersionAndTagIdToRequestImp() {
+    public void makeHttpRequestsShouldReturnErrorIfImpBannerIsNull() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder.id("impId").banner(null));
+
+        // when
+        final Result>> result = deepintentBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).hasSize(1);
+        assertThat(result.getErrors()).allSatisfy(bidderError -> {
+            assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input);
+            assertThat(bidderError.getMessage()).isEqualTo("We need a Banner Object in the request, imp : impId");
+        });
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnErrorIfImpBannerHasNoSizeParametersPresent() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder.id("impId").banner(Banner.builder().build()));
+
+        // when
+        final Result>> result = deepintentBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).hasSize(1);
+        assertThat(result.getErrors()).allSatisfy(bidderError -> {
+            assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input);
+            assertThat(bidderError.getMessage()).isEqualTo("At least one size is required, imp : impId");
+        });
+    }
+
+    @Test
+    public void makeHttpRequestsShouldSetMissedBannerSizeFromBannerFormat() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
+                .banner(Banner.builder().format(singletonList(Format.builder().w(77).h(88).build())).build()));
+
+        // when
+        final Result>> result = deepintentBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).hasSize(0);
+        final Imp expectedImp = expectedImp(impBuilder -> impBuilder
+                .banner(Banner.builder().w(77).h(88)
+                        .format(singletonList(Format.builder().w(77).h(88).build()))
+                        .build()));
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .containsExactly(expectedImp);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldSetDislplayManagerAndVersionAndTagIdToRequestImp() {
         // given
         final BidRequest bidRequest = givenBidRequest(identity());
 
@@ -76,12 +136,7 @@ public void shouldSetDislplayManagerAndVersionAndTagIdToRequestImp() {
 
         // then
         assertThat(result.getErrors()).hasSize(0);
-        final Imp expectedImp = Imp.builder()
-                .displaymanager(DISPLAY_MANAGER)
-                .displaymanagerver(DISPLAY_MANAGER_VERSION)
-                .tagid(IMP_EXT_TAG_ID)
-                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of(IMP_EXT_TAG_ID))))
-                .build();
+        final Imp expectedImp = expectedImp(identity());
         assertThat(result.getValue())
                 .extracting(HttpRequest::getPayload)
                 .flatExtracting(BidRequest::getImp)
@@ -92,6 +147,7 @@ public void shouldSetDislplayManagerAndVersionAndTagIdToRequestImp() {
     public void makeRequestShouldCreateRequestForEveryValidImp() {
         // given
         final Imp firstImp = Imp.builder()
+                .banner(Banner.builder().w(23).h(25).build())
                 .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of(IMP_EXT_TAG_ID))))
                 .build();
         final Imp secondImp = Imp.builder()
@@ -106,11 +162,7 @@ public void makeRequestShouldCreateRequestForEveryValidImp() {
 
         // then
         assertThat(result.getErrors()).hasSize(1);
-        final Imp expectedFirstImp = firstImp.toBuilder()
-                .displaymanager(DISPLAY_MANAGER)
-                .displaymanagerver(DISPLAY_MANAGER_VERSION)
-                .tagid(IMP_EXT_TAG_ID)
-                .build();
+        final Imp expectedFirstImp = expectedImp(identity());
 
         assertThat(result.getValue())
                 .hasSize(1)
@@ -122,12 +174,11 @@ public void makeRequestShouldCreateRequestForEveryValidImp() {
     @Test
     public void makeRequestShouldCreateSeparateRequestForEveryImp() {
         // given
-        final Imp firstImp = Imp.builder()
-                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of("firstImpTagId"))))
-                .build();
-        final Imp secondImp = Imp.builder()
-                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of("secondImpTagId"))))
-                .build();
+        final Imp firstImp = givenImp(impBuilder ->
+                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of("firstImpTagId")))));
+        final Imp secondImp = givenImp(impBuilder ->
+                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of("secondImpTagId")))));
+
         final BidRequest bidRequest = BidRequest.builder()
                 .imp(Arrays.asList(firstImp, secondImp))
                 .build();
@@ -137,16 +188,17 @@ public void makeRequestShouldCreateSeparateRequestForEveryImp() {
 
         // then
         assertThat(result.getErrors()).hasSize(0);
-        final Imp expectedFirstImp = firstImp.toBuilder()
-                .displaymanager(DISPLAY_MANAGER)
-                .displaymanagerver(DISPLAY_MANAGER_VERSION)
-                .tagid("firstImpTagId")
-                .build();
-        final Imp expectedSecondImp = secondImp.toBuilder()
-                .displaymanager(DISPLAY_MANAGER)
-                .displaymanagerver(DISPLAY_MANAGER_VERSION)
-                .tagid("secondImpTagId")
-                .build();
+        final Imp expectedFirstImp =
+                expectedImp(impBuilder ->
+                        impBuilder.ext(mapper.valueToTree(
+                                ExtPrebid.of(null, ExtImpDeepintent.of("firstImpTagId"))))
+                                .tagid("firstImpTagId"));
+
+        final Imp expectedSecondImp =
+                expectedImp(impBuilder ->
+                        impBuilder.ext(mapper.valueToTree(
+                                ExtPrebid.of(null, ExtImpDeepintent.of("secondImpTagId"))))
+                                .tagid("secondImpTagId"));
 
         assertThat(result.getValue())
                 .hasSize(2)
@@ -165,10 +217,10 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
 
         // then
         assertThat(result.getErrors()).hasSize(1);
-        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
-        assertThat(result.getErrors())
-                .extracting(BidderError::getType)
-                .containsExactly(BidderError.Type.bad_server_response);
+        assertThat(result.getErrors()).allSatisfy(error -> {
+            assertThat(error.getMessage()).contains("Failed to decode: Unrecognized token");
+            assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
+        });
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -200,43 +252,6 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso
         assertThat(result.getValue()).isEmpty();
     }
 
-    @Test
-    public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
-        // given
-        final HttpCall httpCall = givenHttpCall(
-                BidRequest.builder()
-                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
-                        .build(),
-                mapper.writeValueAsString(
-                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
-
-        // when
-        final Result> result = deepintentBidder.makeBids(httpCall, null);
-
-        // then
-        assertThat(result.getErrors()).isEmpty();
-        assertThat(result.getValue())
-                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, CURRENCY));
-    }
-
-    @Test
-    public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
-        // given
-        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
-                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
-                        .build(),
-                mapper.writeValueAsString(
-                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
-
-        // when
-        final Result> result = deepintentBidder.makeBids(httpCall, null);
-
-        // then
-        assertThat(result.getErrors()).isEmpty();
-        assertThat(result.getValue())
-                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, CURRENCY));
-    }
-
     @Test
     public void shouldReturnErrorIfBidImpIdNotFoundInImps() throws JsonProcessingException {
         // given
@@ -251,7 +266,7 @@ public void shouldReturnErrorIfBidImpIdNotFoundInImps() throws JsonProcessingExc
 
         // then
         assertThat(result.getErrors())
-                .containsExactly(BidderError.badInput("Failed to find impression with id: notFoundId"));
+                .containsExactly(BidderError.badServerResponse("Failed to find impression with id: notFoundId"));
     }
 
     @Test
@@ -288,6 +303,17 @@ private static BidRequest givenBidRequest(Function impCustomizer) {
         return impCustomizer.apply(Imp.builder()
+                .banner(Banner.builder().w(23).h(25).build())
+                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of(IMP_EXT_TAG_ID)))))
+                .build();
+    }
+
+    private Imp expectedImp(Function impCustomizer) {
+        return impCustomizer.apply(Imp.builder()
+                .banner(Banner.builder().w(23).h(25).build())
+                .displaymanager(DISPLAY_MANAGER)
+                .displaymanagerver(DISPLAY_MANAGER_VERSION)
+                .tagid(IMP_EXT_TAG_ID)
                 .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of(IMP_EXT_TAG_ID)))))
                 .build();
     }

From ba48c43219a5553445307e9553f04890976d0c7d Mon Sep 17 00:00:00 2001
From: Dmitriy 
Date: Thu, 3 Dec 2020 13:00:57 +0200
Subject: [PATCH 036/129] Review Triplelift bidder (#1047)

---
 .../bidder/triplelift/TripleliftBidder.java   | 45 ++++++++++---------
 .../triplelift/TripleliftBidderTest.java      | 25 +++++++++--
 .../org/prebid/server/it/TripleliftTest.java  |  7 ++-
 3 files changed, 49 insertions(+), 28 deletions(-)

diff --git a/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java b/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java
index 3f8022e0191..2ce0c27a656 100644
--- a/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java
+++ b/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java
@@ -10,6 +10,7 @@
 import com.iab.openrtb.response.SeatBid;
 import io.vertx.core.http.HttpMethod;
 import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.ObjectUtils;
 import org.prebid.server.bidder.Bidder;
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.bidder.model.BidderError;
@@ -31,6 +32,9 @@
 import java.util.List;
 import java.util.Objects;
 
+/**
+ * Triplelift {@link Bidder} implementation.
+ */
 public class TripleliftBidder implements Bidder {
 
     private static final TypeReference> TRIPLELIFT_EXT_TYPE_REFERENCE =
@@ -82,19 +86,16 @@ private Imp modifyImp(Imp imp) throws PreBidException {
             throw new PreBidException("neither Banner nor Video object specified");
         }
 
-        final ExtImpTriplelift impExt = parseExtImpTriplelift(imp);
-        final Imp.ImpBuilder impBuilder = imp.toBuilder().tagid(impExt.getInventoryCode());
-        if (impExt.getFloor() != null) {
-            impBuilder.bidfloor(impExt.getFloor());
-        }
-
-        return impBuilder.build();
+        final ExtImpTriplelift impExt = parseImpExt(imp);
+        return imp.toBuilder()
+                .tagid(impExt.getInventoryCode())
+                .bidfloor(ObjectUtils.firstNonNull(impExt.getFloor(), imp.getBidfloor()))
+                .build();
     }
 
-    private ExtImpTriplelift parseExtImpTriplelift(Imp imp) {
+    private ExtImpTriplelift parseImpExt(Imp imp) {
         try {
-            return mapper.mapper().convertValue(imp.getExt(),
-                    TRIPLELIFT_EXT_TYPE_REFERENCE).getBidder();
+            return mapper.mapper().convertValue(imp.getExt(), TRIPLELIFT_EXT_TYPE_REFERENCE).getBidder();
         } catch (IllegalArgumentException e) {
             throw new PreBidException(e.getMessage(), e);
         }
@@ -104,7 +105,7 @@ private ExtImpTriplelift parseExtImpTriplelift(Imp imp) {
     public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) {
         final BidResponse bidResponse;
         try {
-            bidResponse = decodeBodyToBidResponse(httpCall);
+            bidResponse = decodeBody(httpCall);
         } catch (PreBidException e) {
             return Result.withError(BidderError.badServerResponse(e.getMessage()));
         }
@@ -117,17 +118,18 @@ public Result> makeBids(HttpCall httpCall, BidReques
         final List bidderBids = new ArrayList<>();
         for (SeatBid seatBid : bidResponse.getSeatbid()) {
             for (Bid bid : seatBid.getBid()) {
-                final ObjectNode ext = bid.getExt();
-                if (ext == null) {
+                final ObjectNode bidExt = bid.getExt();
+                if (bidExt == null) {
                     errors.add(BidderError.badServerResponse(String.format("Empty ext in bid %s", bid.getId())));
                     break;
                 }
+
                 try {
-                    final TripleliftResponseExt tripleliftResponseExt = mapper.mapper().treeToValue(ext,
+                    final TripleliftResponseExt tripleliftResponseExt = mapper.mapper().treeToValue(bidExt,
                             TripleliftResponseExt.class);
-                    final BidderBid bidderBid = BidderBid.of(bid, getBidType(tripleliftResponseExt),
-                            bidResponse.getCur());
-                    bidderBids.add(bidderBid);
+                    final BidType bidType = getBidType(tripleliftResponseExt);
+
+                    bidderBids.add(BidderBid.of(bid, bidType, bidResponse.getCur()));
                 } catch (JsonProcessingException e) {
                     errors.add(BidderError.badServerResponse(e.getMessage()));
                 }
@@ -136,7 +138,7 @@ public Result> makeBids(HttpCall httpCall, BidReques
         return Result.of(bidderBids, errors);
     }
 
-    private BidResponse decodeBodyToBidResponse(HttpCall httpCall) {
+    private BidResponse decodeBody(HttpCall httpCall) {
         try {
             return mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
         } catch (DecodeException e) {
@@ -149,9 +151,8 @@ private static BidType getBidType(TripleliftResponseExt tripleliftResponseExt) {
                 ? tripleliftResponseExt.getTripleliftPb()
                 : null;
 
-        if (tripleliftInnerExt != null && tripleliftInnerExt.getFormat() == 11) {
-            return BidType.video;
-        }
-        return BidType.banner;
+        return tripleliftInnerExt != null && Objects.equals(tripleliftInnerExt.getFormat(), 11)
+                ? BidType.video
+                : BidType.banner;
     }
 }
diff --git a/src/test/java/org/prebid/server/bidder/triplelift/TripleliftBidderTest.java b/src/test/java/org/prebid/server/bidder/triplelift/TripleliftBidderTest.java
index f797cfcae9e..9acc63382f8 100644
--- a/src/test/java/org/prebid/server/bidder/triplelift/TripleliftBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/triplelift/TripleliftBidderTest.java
@@ -115,7 +115,7 @@ public void makeHttpRequestsShouldModifyTagIdFromImpExt() {
     @Test
     public void makeHttpRequestsShouldModifyBidFloorFromImpExtWhenFloorIsPresent() {
         // given
-        final BigDecimal floor = new BigDecimal(12f);
+        final BigDecimal floor = BigDecimal.valueOf(12.32);
         final BidRequest bidRequest = BidRequest.builder()
                 .imp(singletonList(Imp.builder()
                         .bidfloor(new BigDecimal(1))
@@ -155,8 +155,7 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
     @Test
     public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
         // given
-        final HttpCall httpCall = givenHttpCall(null,
-                mapper.writeValueAsString(null));
+        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
 
         // when
         final Result> result = tripleliftBidder.makeBids(httpCall, null);
@@ -169,7 +168,8 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces
     @Test
     public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
         // given
-        final HttpCall httpCall = givenHttpCall(null,
+        final HttpCall httpCall = givenHttpCall(
+                null,
                 mapper.writeValueAsString(BidResponse.builder().build()));
 
         // when
@@ -214,6 +214,23 @@ public void makeBidsShouldReturnTypeBannerWhenTripleliftResponseExtIsEmpty() thr
                 .containsOnly(BidderBid.of(Bid.builder().ext(ext).build(), banner, "USD"));
     }
 
+    @Test
+    public void makeBidsShouldReturnTypeBannerWhenTripleliftInnerExtIsNull() throws JsonProcessingException {
+        // given
+        final ObjectNode ext = mapper.valueToTree(TripleliftResponseExt.of(TripleliftInnerExt.of(null)));
+        final HttpCall httpCall = givenHttpCall(
+                null,
+                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.ext(ext))));
+
+        // when
+        final Result> result = tripleliftBidder.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .containsOnly(BidderBid.of(Bid.builder().ext(ext).build(), banner, "USD"));
+    }
+
     @Test
     public void makeBidsShouldReturnTypeBannerWhenTripleliftInnerExtIsNotEleven() throws JsonProcessingException {
         // given
diff --git a/src/test/java/org/prebid/server/it/TripleliftTest.java b/src/test/java/org/prebid/server/it/TripleliftTest.java
index f10a63e3787..d8016f3ae24 100644
--- a/src/test/java/org/prebid/server/it/TripleliftTest.java
+++ b/src/test/java/org/prebid/server/it/TripleliftTest.java
@@ -11,6 +11,7 @@
 import java.io.IOException;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
 import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
 import static com.github.tomakehurst.wiremock.client.WireMock.post;
 import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
@@ -24,6 +25,8 @@ public class TripleliftTest extends IntegrationTest {
     public void openrtb2AuctionShouldRespondWithBidsFromTriplelift() throws IOException, JSONException {
         // given
         WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/triplelift-exchange"))
+                .withHeader("Accept", equalTo("application/json"))
+                .withHeader("Content-Type", equalTo("application/json;charset=UTF-8"))
                 .withRequestBody(equalToJson(jsonFrom("openrtb2/triplelift/test-triplelift-bid-request.json")))
                 .willReturn(aResponse().withBody(jsonFrom("openrtb2/triplelift/test-triplelift-bid-response.json"))));
 
@@ -47,8 +50,8 @@ public void openrtb2AuctionShouldRespondWithBidsFromTriplelift() throws IOExcept
         final String expectedAuctionResponse = openrtbAuctionResponseFrom(
                 "openrtb2/triplelift/test-auction-triplelift-response.json",
                 response, singletonList("triplelift"));
-        final String actualStr = response.asString();
-        JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE);
+
+        JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE);
     }
 }
 

From dbe83a68cb4c64ec7f9c91f8dd973b451921a519 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 3 Dec 2020 13:05:35 +0200
Subject: [PATCH 037/129] Bump jetty.version from 9.4.34.v20201102 to
 9.4.35.v20201120 (#1048)

Bumps `jetty.version` from 9.4.34.v20201102 to 9.4.35.v20201120.

Updates `jetty-server` from 9.4.34.v20201102 to 9.4.35.v20201120
- [Release notes](https://github.com/eclipse/jetty.project/releases)
- [Commits](https://github.com/eclipse/jetty.project/compare/jetty-9.4.34.v20201102...jetty-9.4.35.v20201120)

Updates `jetty-servlet` from 9.4.34.v20201102 to 9.4.35.v20201120
- [Release notes](https://github.com/eclipse/jetty.project/releases)
- [Commits](https://github.com/eclipse/jetty.project/compare/jetty-9.4.34.v20201102...jetty-9.4.35.v20201120)

Updates `jetty-servlets` from 9.4.34.v20201102 to 9.4.35.v20201120
- [Release notes](https://github.com/eclipse/jetty.project/releases)
- [Commits](https://github.com/eclipse/jetty.project/compare/jetty-9.4.34.v20201102...jetty-9.4.35.v20201120)

Updates `jetty-webapp` from 9.4.34.v20201102 to 9.4.35.v20201120
- [Release notes](https://github.com/eclipse/jetty.project/releases)
- [Commits](https://github.com/eclipse/jetty.project/compare/jetty-9.4.34.v20201102...jetty-9.4.35.v20201120)

Updates `jetty-http` from 9.4.34.v20201102 to 9.4.35.v20201120
- [Release notes](https://github.com/eclipse/jetty.project/releases)
- [Commits](https://github.com/eclipse/jetty.project/compare/jetty-9.4.34.v20201102...jetty-9.4.35.v20201120)

Updates `jetty-io` from 9.4.34.v20201102 to 9.4.35.v20201120
- [Release notes](https://github.com/eclipse/jetty.project/releases)
- [Commits](https://github.com/eclipse/jetty.project/compare/jetty-9.4.34.v20201102...jetty-9.4.35.v20201120)

Updates `jetty-security` from 9.4.34.v20201102 to 9.4.35.v20201120
- [Release notes](https://github.com/eclipse/jetty.project/releases)
- [Commits](https://github.com/eclipse/jetty.project/compare/jetty-9.4.34.v20201102...jetty-9.4.35.v20201120)

Updates `jetty-continuation` from 9.4.34.v20201102 to 9.4.35.v20201120

Updates `jetty-util` from 9.4.34.v20201102 to 9.4.35.v20201120
- [Release notes](https://github.com/eclipse/jetty.project/releases)
- [Commits](https://github.com/eclipse/jetty.project/compare/jetty-9.4.34.v20201102...jetty-9.4.35.v20201120)

Updates `jetty-xml` from 9.4.34.v20201102 to 9.4.35.v20201120
- [Release notes](https://github.com/eclipse/jetty.project/releases)
- [Commits](https://github.com/eclipse/jetty.project/compare/jetty-9.4.34.v20201102...jetty-9.4.35.v20201120)

Signed-off-by: dependabot[bot] 

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 50b7b23f548..2cce470ad84 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,7 +55,7 @@
         2.23.4
         3.8.0
         2.26.3
-        9.4.34.v20201102
+        9.4.35.v20201120
         3.0.6
         1.4.196
 

From 17cca28fbd2ede26bed564248eab9cce455f032d Mon Sep 17 00:00:00 2001
From: Serhii Nahornyi 
Date: Thu, 3 Dec 2020 14:39:02 +0200
Subject: [PATCH 038/129] Add video support to ttx bidder (#1032)

---
 .../prebid/server/bidder/ttx/TtxBidder.java   | 217 +++++++++++++++---
 .../server/bidder/ttx/response/TtxBidExt.java |  11 +
 .../bidder/ttx/response/TtxBidExtTtx.java     |  15 ++
 src/main/resources/bidder-config/ttx.yaml     |   3 +
 .../server/bidder/ttx/TtxBidderTest.java      | 215 ++++++++++++++++-
 5 files changed, 416 insertions(+), 45 deletions(-)
 create mode 100644 src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExt.java
 create mode 100644 src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExtTtx.java

diff --git a/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java b/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java
index 968b73166e3..3ecebbe7f76 100644
--- a/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java
+++ b/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java
@@ -1,64 +1,215 @@
 package org.prebid.server.bidder.ttx;
 
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.iab.openrtb.request.BidRequest;
 import com.iab.openrtb.request.Imp;
 import com.iab.openrtb.request.Site;
+import com.iab.openrtb.request.Video;
+import com.iab.openrtb.response.Bid;
+import com.iab.openrtb.response.BidResponse;
+import com.iab.openrtb.response.SeatBid;
+import io.vertx.core.http.HttpMethod;
+import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
-import org.prebid.server.bidder.OpenrtbBidder;
-import org.prebid.server.bidder.model.ImpWithExt;
+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.bidder.ttx.proto.TtxImpExt;
 import org.prebid.server.bidder.ttx.proto.TtxImpExtTtx;
+import org.prebid.server.bidder.ttx.response.TtxBidExt;
+import org.prebid.server.bidder.ttx.response.TtxBidExtTtx;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.proto.openrtb.ext.ExtPrebid;
 import org.prebid.server.proto.openrtb.ext.request.ttx.ExtImpTtx;
 import org.prebid.server.proto.openrtb.ext.response.BidType;
+import org.prebid.server.util.HttpUtil;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
-public class TtxBidder extends OpenrtbBidder {
+/**
+ * 33across {@link Bidder} implementation.
+ */
+public class TtxBidder implements Bidder {
+
+    private static final TypeReference> TTX_EXT_TYPE_REFERENCE =
+            new TypeReference>() {
+            };
+
+    private final String endpointUrl;
+    private final JacksonMapper mapper;
 
     public TtxBidder(String endpointUrl, JacksonMapper mapper) {
-        super(endpointUrl, RequestCreationStrategy.SINGLE_REQUEST, ExtImpTtx.class, mapper);
+        this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
+        this.mapper = Objects.requireNonNull(mapper);
     }
 
     @Override
-    protected void modifyRequest(BidRequest bidRequest, BidRequest.BidRequestBuilder requestBuilder,
-                                 List> impsWithExts) {
-        final List modifiedImps = impsWithExts.stream()
-                .map(ImpWithExt::getImp)
-                .collect(Collectors.toList());
-        final Imp firstImp = modifiedImps.get(0);
-        final ExtImpTtx firstImpExt = impsWithExts.get(0).getImpExt();
-
-        final String zoneId = firstImpExt.getZoneId();
-        final TtxImpExt ttxImpExt = TtxImpExt.of(
-                TtxImpExtTtx.of(firstImpExt.getProductId(), StringUtils.isNotBlank(zoneId) ? zoneId : null));
-
-        final Imp modifiedFirstImp = firstImp.toBuilder().ext(mapper.mapper().valueToTree(ttxImpExt)).build();
-
-        if (modifiedImps.size() == 1) {
-            requestBuilder.imp(Collections.singletonList(modifiedFirstImp));
-        } else {
-            final List subList = modifiedImps.subList(1, modifiedImps.size());
-            final List finalizedImps = new ArrayList<>(subList.size() + 1);
-            finalizedImps.add(modifiedFirstImp);
-            finalizedImps.addAll(subList);
-            requestBuilder.imp(finalizedImps);
+    public Result>> makeHttpRequests(BidRequest request) {
+        final Imp firstImp = request.getImp().get(0);
+        final List errors = new ArrayList<>();
+        Imp updatedFirstImp = null;
+        ExtImpTtx extImpTtx = null;
+        try {
+            extImpTtx = parseImpExt(firstImp);
+            updatedFirstImp = updatedImp(firstImp, extImpTtx.getProductId(), extImpTtx.getZoneId(), errors);
+        } catch (PreBidException e) {
+            errors.add(BidderError.badInput(e.getMessage()));
         }
-        requestBuilder.site(modifySite(bidRequest.getSite(), firstImpExt.getSiteId()));
+
+        if (updatedFirstImp != null && updatedFirstImp.getBanner() == null && updatedFirstImp.getVideo() == null) {
+            return Result.withError(BidderError.badInput("At least one of [banner, video] "
+                    + "formats must be defined in Imp. None found"));
+        }
+
+        final HttpRequest httpRequest = createRequest(request, extImpTtx, updatedFirstImp);
+
+        return Result.of(Collections.singletonList(httpRequest), errors);
+    }
+
+    private ExtImpTtx parseImpExt(Imp imp) {
+        try {
+            return mapper.mapper().convertValue(imp.getExt(), TTX_EXT_TYPE_REFERENCE).getBidder();
+        } catch (IllegalArgumentException e) {
+            throw new PreBidException(e.getMessage());
+        }
+    }
+
+    private Imp updatedImp(Imp imp, String productId, String zoneId, List errors) {
+
+        return imp.toBuilder()
+                .video(updatedVideo(imp.getVideo(), productId, errors))
+                .ext(createImpExt(productId, zoneId))
+                .build();
     }
 
-    private static Site modifySite(Site site, String siteId) {
-        final Site.SiteBuilder siteBuilder = site == null ? Site.builder() : site.toBuilder();
-        return siteBuilder
-                .id(siteId)
+    private static Video updatedVideo(Video video, String productId, List errors) {
+        if (video == null) {
+            return null;
+        }
+        if (isZeroOrNullInteger(video.getW())
+                || isZeroOrNullInteger(video.getH())
+                || CollectionUtils.isEmpty(video.getProtocols())
+                || CollectionUtils.isEmpty(video.getMimes())
+                || CollectionUtils.isEmpty(video.getPlaybackmethod())) {
+            errors.add(BidderError.badInput("One or more invalid or missing video field(s) "
+                    + "w, h, protocols, mimes, playbackmethod"));
+            return null;
+        }
+        final Integer videoPlacement = video.getPlacement();
+
+        return video.toBuilder()
+                .startdelay(resolveStartDelay(video.getStartdelay(), productId))
+                .placement(resolvePlacement(videoPlacement, productId))
                 .build();
     }
 
+    private static boolean isZeroOrNullInteger(Integer integer) {
+        return integer == null || integer == 0;
+    }
+
+    private static Integer resolveStartDelay(Integer startDelay, String productId) {
+        return Objects.equals(productId, "instream") ? Integer.valueOf(0) : startDelay;
+    }
+
+    private static Integer resolvePlacement(Integer videoPlacement, String productId) {
+        if (Objects.equals(productId, "instream")) {
+            return 1;
+        }
+        if (isZeroOrNullInteger(videoPlacement)) {
+            return 2;
+        }
+        return videoPlacement;
+    }
+
+    private ObjectNode createImpExt(String productId, String zoneId) {
+        final TtxImpExt ttxImpExt = TtxImpExt.of(TtxImpExtTtx.of(productId, StringUtils.stripToNull(zoneId)));
+        return mapper.mapper().valueToTree(ttxImpExt);
+    }
+
+    private HttpRequest createRequest(BidRequest request, ExtImpTtx extImpTtx, Imp updatedFirstImp) {
+        final Site updatedSite = extImpTtx != null ? updateSite(request.getSite(), extImpTtx.getSiteId()) : null;
+        final BidRequest modifiedRequest = updateRequest(request, updatedSite, updatedFirstImp);
+
+        return HttpRequest.builder()
+                .method(HttpMethod.POST)
+                .uri(endpointUrl)
+                .headers(HttpUtil.headers())
+                .payload(modifiedRequest)
+                .body(mapper.encode(modifiedRequest))
+                .build();
+    }
+
+    private static Site updateSite(Site site, String siteId) {
+        return site == null ? null : site.toBuilder().id(siteId).build();
+    }
+
+    private static BidRequest updateRequest(BidRequest request, Site updatedSite, Imp updatedFirstImp) {
+        if (updatedSite == null && updatedFirstImp == null) {
+            return request;
+        }
+        final List requestImps = request.getImp();
+        return request.toBuilder()
+                .site(updatedSite != null ? updatedSite : request.getSite())
+                .imp(updatedFirstImp != null ? replaceFirstImp(requestImps, updatedFirstImp) : requestImps)
+                .build();
+    }
+
+    private static List replaceFirstImp(List imps, Imp firstImp) {
+        final List updatedImpList = new ArrayList<>(imps);
+        updatedImpList.set(0, firstImp);
+        return updatedImpList;
+    }
+
     @Override
-    protected BidType getBidType(String impId, List imps) {
-        return BidType.banner;
+    public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) {
+        try {
+            final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
+            return Result.of(extractBids(bidResponse), Collections.emptyList());
+        } catch (DecodeException | PreBidException e) {
+            return Result.withError(BidderError.badServerResponse(e.getMessage()));
+        }
+    }
+
+    private List extractBids(BidResponse bidResponse) {
+        if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
+            return Collections.emptyList();
+        }
+        return bidsFromResponse(bidResponse);
+    }
+
+    private List bidsFromResponse(BidResponse bidResponse) {
+        return bidResponse.getSeatbid().stream()
+                .filter(Objects::nonNull)
+                .map(SeatBid::getBid)
+                .filter(Objects::nonNull)
+                .flatMap(Collection::stream)
+                .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur()))
+                .collect(Collectors.toList());
+    }
+
+    private BidType getBidType(Bid bid) {
+        try {
+            final TtxBidExt ttxBidExt = mapper.mapper().convertValue(bid.getExt(), TtxBidExt.class);
+            return ttxBidExt != null ? getBidTypeByTtx(ttxBidExt.getTtx()) : BidType.banner;
+        } catch (IllegalArgumentException e) {
+            return BidType.banner;
+        }
+    }
+
+    private static BidType getBidTypeByTtx(TtxBidExtTtx bidExt) {
+        return bidExt != null && Objects.equals(bidExt.getMediaType(), "video")
+                ? BidType.video
+                : BidType.banner;
     }
 }
diff --git a/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExt.java b/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExt.java
new file mode 100644
index 00000000000..6f4ba1b5702
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExt.java
@@ -0,0 +1,11 @@
+package org.prebid.server.bidder.ttx.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Value;
+
+@AllArgsConstructor(staticName = "of")
+@Value
+public class TtxBidExt {
+
+    TtxBidExtTtx ttx;
+}
diff --git a/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExtTtx.java b/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExtTtx.java
new file mode 100644
index 00000000000..6a66b3322f7
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExtTtx.java
@@ -0,0 +1,15 @@
+package org.prebid.server.bidder.ttx.response;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Value;
+
+@AllArgsConstructor(staticName = "of")
+@Value
+public class TtxBidExtTtx {
+
+    @JsonProperty("mediaType")
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    String mediaType;
+}
diff --git a/src/main/resources/bidder-config/ttx.yaml b/src/main/resources/bidder-config/ttx.yaml
index 53b5dc71c91..454437d2bb8 100644
--- a/src/main/resources/bidder-config/ttx.yaml
+++ b/src/main/resources/bidder-config/ttx.yaml
@@ -1,6 +1,7 @@
 adapters:
   ttx:
     enabled: false
+    # Old http://ssc.33across.com/api/v1/hb
     endpoint: https://ssc.33across.com/api/v1/s2s
     pbs-enforces-gdpr: true
     pbs-enforces-ccpa: true
@@ -11,8 +12,10 @@ adapters:
       maintainer-email: headerbidding@33across.com
       app-media-types:
         - banner
+        - video
       site-media-types:
         - banner
+        - video
       supported-vendors:
       vendor-id: 58
     usersync:
diff --git a/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java b/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java
index a2a414191bf..124ac7a0810 100644
--- a/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java
@@ -5,6 +5,7 @@
 import com.iab.openrtb.request.BidRequest;
 import com.iab.openrtb.request.Imp;
 import com.iab.openrtb.request.Site;
+import com.iab.openrtb.request.Video;
 import com.iab.openrtb.response.Bid;
 import com.iab.openrtb.response.BidResponse;
 import com.iab.openrtb.response.SeatBid;
@@ -19,6 +20,8 @@
 import org.prebid.server.bidder.model.Result;
 import org.prebid.server.bidder.ttx.proto.TtxImpExt;
 import org.prebid.server.bidder.ttx.proto.TtxImpExtTtx;
+import org.prebid.server.bidder.ttx.response.TtxBidExt;
+import org.prebid.server.bidder.ttx.response.TtxBidExtTtx;
 import org.prebid.server.proto.openrtb.ext.ExtPrebid;
 import org.prebid.server.proto.openrtb.ext.request.ttx.ExtImpTtx;
 
@@ -30,7 +33,9 @@
 import static java.util.function.Function.identity;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.tuple;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
 
 public class TtxBidderTest extends VertxTest {
 
@@ -49,7 +54,7 @@ public void creationShouldFailOnInvalidEndpointUrl() {
     }
 
     @Test
-    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
+    public void makeHttpRequestsShouldAppendErrorIfImpExtCouldNotBeParsed() {
         // given
         final BidRequest bidRequest = givenBidRequest(
                 impBuilder -> impBuilder
@@ -60,14 +65,20 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
 
         // then
         assertThat(result.getErrors()).hasSize(1);
-        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
-        assertThat(result.getValue()).isEmpty();
+        assertThat(result.getErrors())
+                .allSatisfy(error -> {
+                    assertThat(error.getMessage()).startsWith("Cannot deserialize instance of");
+                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
+                });
+
+        assertThat(result.getValue()).hasSize(1);
     }
 
     @Test
     public void makeHttpRequestsShouldSetSiteIdFromImpExt() {
         // given
-        final BidRequest bidRequest = givenBidRequest(identity());
+        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder ->
+                bidRequestBuilder.site(Site.builder().build()), identity());
 
         // when
         final Result>> result = ttxBidder.makeHttpRequests(bidRequest);
@@ -81,15 +92,31 @@ public void makeHttpRequestsShouldSetSiteIdFromImpExt() {
                 .containsOnly("siteId");
     }
 
+    @Test
+    public void makeHttpRequestsShouldNotCreateNewSiteIfSiteNotPresentInBidRequest() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(identity());
+
+        // when
+        final Result>> result = ttxBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+                .flatExtracting(BidRequest::getSite)
+                .containsNull();
+    }
+
     @Test
     public void makeHttpRequestsShouldGetDetailsOnlyFromFirstImpExt() {
         // given
         final BidRequest bidRequest = BidRequest.builder()
+                .site(Site.builder().build())
                 .imp(asList(
                         givenImp(identity()),
                         givenImp(impBuilder -> impBuilder
-                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", "2", "3")))))
-                ))
+                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", "2", "3")))))))
                 .build();
 
         // when
@@ -111,8 +138,7 @@ public void makeHttpRequestsShouldChangeOnlyFirstImpExt() {
                 .imp(asList(
                         givenImp(identity()),
                         givenImp(impBuilder -> impBuilder
-                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3")))))
-                ))
+                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3")))))))
                 .build();
 
         // when
@@ -129,6 +155,111 @@ public void makeHttpRequestsShouldChangeOnlyFirstImpExt() {
                         mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3"))));
     }
 
+    @Test
+    public void makeHttpRequestsShouldReturnErrorIfVideoParamsNotPresent() {
+        // given
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(
+                        givenImp(impBuilder -> impBuilder
+                                .video(Video.builder().build())
+                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3")))))))
+                .build();
+
+        // when
+        final Result>> result = ttxBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors())
+                .containsExactly(BidderError.badInput("One or more invalid or missing video field(s) w, h, "
+                        + "protocols, mimes, playbackmethod"));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldUpdateNotPresentPlacement() {
+        // given
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(
+                        givenImp(impBuilder -> impBuilder
+                                .video(validVideo())
+                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3")))))))
+                .build();
+
+        // when
+        final Result>> result = ttxBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getVideo)
+                .extracting(Video::getPlacement)
+                .containsExactly(2);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldNotUpdatePlacementWhenProductIdIsNotInstreamAndPlacementIsNotZero() {
+        // given
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(
+                        givenImp(impBuilder -> impBuilder
+                                .video(validVideo().toBuilder().placement(23).build())
+                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3")))))))
+                .build();
+
+        // when
+        final Result>> result = ttxBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getVideo)
+                .extracting(Video::getPlacement)
+                .containsExactly(23);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldUpdatePlacementAndStartDelayIfProdIsInstream() {
+        // given
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(
+                        givenImp(impBuilder -> impBuilder
+                                .video(validVideo())
+                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "instream")))))))
+                .build();
+
+        // when
+        final Result>> result = ttxBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getVideo)
+                .extracting(Video::getPlacement, Video::getStartdelay)
+                .containsExactly(tuple(1, 0));
+    }
+
+    @Test
+    public void makeBidsShouldReturnErrorIfNoBannerOrVideoPresent() {
+        // given
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(givenImp(impBuilder -> impBuilder.banner(null))))
+                .build();
+
+        // when
+        final Result>> result = ttxBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors())
+                .containsExactly(BidderError.badInput("At least one of [banner, video] "
+                        + "formats must be defined in Imp. None found"));
+        assertThat(result.getValue()).isEmpty();
+    }
+
     @Test
     public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
         // given
@@ -138,9 +269,11 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
         final Result> result = ttxBidder.makeBids(httpCall, null);
 
         // then
-        assertThat(result.getErrors()).hasSize(1);
-        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
-        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
+        assertThat(result.getErrors())
+                .allSatisfy(error -> {
+                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
+                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
+                });
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -173,7 +306,7 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso
     }
 
     @Test
-    public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
+    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
         // given
         final HttpCall httpCall = givenHttpCall(
                 BidRequest.builder()
@@ -191,6 +324,64 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
                 .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
     }
 
+    @Test
+    public void makeBidsShouldReturnVideoBidIfVideoInBidExt() throws JsonProcessingException {
+        // given
+        final TtxBidExt ttxBidExt = TtxBidExt.of(TtxBidExtTtx.of("video"));
+        final HttpCall httpCall = givenHttpCall(
+                BidRequest.builder()
+                        .imp(singletonList(Imp.builder().build()))
+                        .build(),
+                mapper.writeValueAsString(
+                        givenBidResponse(bidBuilder -> bidBuilder
+                                .ext(mapper.valueToTree(ttxBidExt)))));
+
+        // when
+        final Result> result = ttxBidder.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        final Bid expectedBid = Bid.builder()
+                .ext(mapper.valueToTree(ttxBidExt))
+                .build();
+        assertThat(result.getValue())
+                .containsOnly(BidderBid.of(expectedBid, video, "USD"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnBannerBidIfExtNotContainVideoString() throws JsonProcessingException {
+        // given
+        final TtxBidExt ttxBidExt = TtxBidExt.of(TtxBidExtTtx.of("notVideo"));
+        final HttpCall httpCall = givenHttpCall(
+                BidRequest.builder()
+                        .imp(singletonList(Imp.builder().build()))
+                        .build(),
+                mapper.writeValueAsString(
+                        givenBidResponse(bidBuilder -> bidBuilder
+                                .ext(mapper.valueToTree(ttxBidExt)))));
+
+        // when
+        final Result> result = ttxBidder.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        final Bid expectedBid = Bid.builder()
+                .ext(mapper.valueToTree(ttxBidExt))
+                .build();
+        assertThat(result.getValue())
+                .containsOnly(BidderBid.of(expectedBid, banner, "USD"));
+    }
+
+    private static Video validVideo() {
+        return Video.builder()
+                .w(23)
+                .h(23)
+                .mimes(singletonList("mime"))
+                .protocols(singletonList(23))
+                .playbackmethod(singletonList(27))
+                .build();
+    }
+
     private static BidRequest givenBidRequest(
             Function bidRequestCustomizer,
             Function impCustomizer) {

From d0c70dfa9b727f913c2ee755f073a58c42ca9b42 Mon Sep 17 00:00:00 2001
From: Dmitriy 
Date: Thu, 3 Dec 2020 16:21:39 +0200
Subject: [PATCH 039/129] Fix Conversant bidder for changing banner or video
 (#1050)

---
 .../bidder/conversant/ConversantBidder.java   | 52 +++++++++----------
 .../conversant/ConversantBidderTest.java      |  9 ++--
 2 files changed, 31 insertions(+), 30 deletions(-)

diff --git a/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java b/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java
index 3ddd89ae758..583c5ed3320 100644
--- a/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java
+++ b/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java
@@ -86,32 +86,25 @@ public Result>> makeHttpRequests(BidRequest bidRequ
     }
 
     private BidRequest createOutgoingRequest(BidRequest bidRequest) {
-        final BidRequest.BidRequestBuilder requestBuilder = bidRequest.toBuilder();
         final List modifiedImps = new ArrayList<>();
         final List requestImps = bidRequest.getImp();
         for (int i = 0; i < requestImps.size(); i++) {
-            final Imp imp = bidRequest.getImp().get(i);
+            final Imp imp = requestImps.get(i);
             final ExtImpConversant impExt = parseImpExt(imp, i);
-            if (i == 0) {
-                final String siteId = impExt.getSiteId();
-                if (bidRequest.getSite() != null) {
-                    requestBuilder.site(updateSite(bidRequest.getSite(), siteId));
-                } else if (bidRequest.getApp() != null) {
-                    requestBuilder.app(updateApp(bidRequest.getApp(), siteId));
-                }
-            }
             modifiedImps.add(modifyImp(imp, impExt));
         }
-        requestBuilder.imp(modifiedImps);
-        return requestBuilder.build();
-    }
 
-    private static Site updateSite(Site site, String siteId) {
-        return site.toBuilder().id(siteId).build();
-    }
+        final Imp firstImp = requestImps.get(0);
+        final ExtImpConversant extImp = parseImpExt(firstImp, 0);
+        final String siteId = extImp.getSiteId();
+        final Site requestSite = bidRequest.getSite();
+        final App requestApp = bidRequest.getApp();
 
-    private static App updateApp(App app, String siteId) {
-        return app.toBuilder().id(siteId).build();
+        return bidRequest.toBuilder()
+                .site(updateSite(requestSite, siteId))
+                .app(requestSite == null ? updateApp(requestApp, siteId) : requestApp)
+                .imp(modifiedImps)
+                .build();
     }
 
     private ExtImpConversant parseImpExt(Imp imp, int impIndex) {
@@ -128,6 +121,14 @@ private ExtImpConversant parseImpExt(Imp imp, int impIndex) {
         return extImp;
     }
 
+    private static Site updateSite(Site site, String siteId) {
+        return site == null ? null : site.toBuilder().id(siteId).build();
+    }
+
+    private static App updateApp(App app, String siteId) {
+        return app == null ? null : app.toBuilder().id(siteId).build();
+    }
+
     private static Imp modifyImp(Imp imp, ExtImpConversant impExt) {
         final BigDecimal extBidfloor = impExt.getBidfloor();
         final String extTagId = impExt.getTagId();
@@ -144,17 +145,16 @@ private static Imp modifyImp(Imp imp, ExtImpConversant impExt) {
                 .bidfloor(extBidfloor != null ? extBidfloor : imp.getBidfloor())
                 .tagid(extTagId != null ? extTagId : imp.getTagid())
                 .secure(shouldChangeSecure ? extSecure : imp.getSecure())
-                .video(impVideo != null ? modifyVideo(impVideo, impExt) : null)
+                .video(impVideo != null && impBanner == null ? modifyVideo(impVideo, impExt) : impVideo)
                 .build();
     }
 
     private static Banner modifyBanner(Banner impBanner, Integer extPosition) {
-        if (impBanner != null && extPosition != null) {
-            return impBanner.toBuilder()
-                    .pos(AD_POSITIONS.contains(extPosition) ? extPosition : null)
-                    .build();
-        }
-        return impBanner;
+        return impBanner == null || extPosition == null
+                ? impBanner
+                : impBanner.toBuilder()
+                .pos(AD_POSITIONS.contains(extPosition) ? extPosition : null)
+                .build();
     }
 
     private static Video modifyVideo(Video video, ExtImpConversant impExt) {
@@ -196,7 +196,7 @@ private static List makeProtocols(List extProtocols, List> makeBids(HttpCall httpCall, BidRequest bidRequest) {
         try {
             final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
-            return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList());
+            return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse));
         } catch (DecodeException | PreBidException e) {
             return Result.withError(BidderError.badServerResponse(e.getMessage()));
         }
diff --git a/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java b/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java
index ec8e9ddc873..17090fff0b4 100644
--- a/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java
@@ -258,10 +258,11 @@ public void makeHttpRequestsShouldNotChangeImpSecure() {
     }
 
     @Test
-    public void makeHttpRequestsShouldSetImpBannerAndVideoPosFromImpExtIfPresent() {
+    public void makeHttpRequestsShouldSetImpForBannerOnlyFromImpExtWhenVideoIsPresent() {
         // given
+        final Video requestVideo = Video.builder().pos(1).build();
         final BidRequest bidRequest = givenBidRequest(
-                impBuilder -> impBuilder.video(Video.builder().pos(1).build()),
+                impBuilder -> impBuilder.banner(Banner.builder().pos(1).build()).video(requestVideo),
                 extBuilder -> extBuilder.position(5));
 
         // when
@@ -275,7 +276,7 @@ public void makeHttpRequestsShouldSetImpBannerAndVideoPosFromImpExtIfPresent() {
                 .extracting(Imp::getBanner, Imp::getVideo)
                 .containsOnly(tuple(
                         Banner.builder().pos(5).build(),
-                        Video.builder().pos(5).build()));
+                        requestVideo));
     }
 
     @Test
@@ -552,7 +553,7 @@ private static Imp givenImp(
 
         return impCustomizer.apply(Imp.builder()
                 .id("123")
-                .banner(Banner.builder().build())
+
                 .ext(mapper.valueToTree(ExtPrebid.of(null,
                         extCustomizer.apply(ExtImpConversant.builder().siteId("site id")).build()))))
                 .build();

From e27ceb6d071946a353d5c40c8414ccc2858cf96f Mon Sep 17 00:00:00 2001
From: bretg 
Date: Fri, 4 Dec 2020 10:05:01 -0500
Subject: [PATCH 040/129] updating README docs links (#1051)

---
 README.md | 17 +++++++----------
 1 file changed, 7 insertions(+), 10 deletions(-)

diff --git a/README.md b/README.md
index fa5fc1a7dba..6f4788e5bb1 100644
--- a/README.md
+++ b/README.md
@@ -10,17 +10,17 @@
 [![GitHub pull-requests closed](https://img.shields.io/github/issues-pr-closed/prebid/prebid-server-java.svg)](https://GitHub.com/prebid/prebid-server-java/pull/)
 
 Prebid Server is an open source implementation of Server-Side Header Bidding.
-It is managed by [Prebid.org](http://prebid.org/overview/what-is-prebid-org.html),
-and upholds the principles from the [Prebid Code of Conduct](http://prebid.org/wrapper_code_of_conduct.html).
+It is managed by Prebid.org,
+and upholds the principles from the [Prebid Code of Conduct](https://prebid.org/wrapper_code_of_conduct.html).
 
 This project does not support the same set of Bidders as Prebid.js, although there is overlap.
 The current set can be found in the [adapters](./src/main/java/org/prebid/server/bidder) package. If you don't see the one you want, feel free to [contribute it](docs/developers/add-new-bidder.md).
 
 For more information, see:
 
-- [What is Prebid?](http://prebid.org/overview/intro.html)
-- [Getting started with Prebid Server](http://prebid.org/dev-docs/get-started-with-prebid-server.html)
-- [Current Bidders](http://prebid.org/dev-docs/prebid-server-bidders.html)
+- [What is Prebid?](https://prebid.org/why-prebid/)
+- [Getting started with Prebid Server](https://docs.prebid.org/prebid-server/overview/prebid-server-overview.html)
+- [Current Bidders](https://docs.prebid.org/dev-docs/pbs-bidders.html)
 
 # Getting Started
 
@@ -28,16 +28,14 @@ The server makes the following assumptions:
 - No ranking or decisioning is performed by this server. It just proxies requests.
 - No ad quality management (e.g., malware, viruses, deceptive creatives) is performed by this server.
 - This server does no fraud scanning and does nothing to prevent bad traffic.
-- This server does no logging.
-- This server has not user profiling or user data collection capabilities.
+- This server logs errors but not requests.
+- This server has no user profiling or user data collection capabilities.
 
 This project is built upon [Vert.x](http://vertx.io) to achieve high request throughput. 
 We use [Maven](https://maven.apache.org) and attempt to introduce minimal dependencies.
 
 When running, the server responds to several HTTP [endpoints](docs/endpoints).
 
-To start the Prebid Server you need to do the following steps:
-
 ## Building
 
 Follow next steps to create JAR file which can be deployed locally.
@@ -84,7 +82,6 @@ and verify response status is `200 OK`.
 # Documentation
 
 ## Development
-- [Differences Between Prebid Server Go and Java](docs/differenceBetweenPBSGo-and-Java.md)
 - [Endpoints](docs/endpoints)
 - [Adding new bidder](docs/developers/add-new-bidder.md)
 - [Adding new analytics module](docs/developers/add-new-analytics-module.md)

From 0e805f2e7f29e14daad3bd7f46704cf7001d4987 Mon Sep 17 00:00:00 2001
From: bretg 
Date: Fri, 4 Dec 2020 10:05:23 -0500
Subject: [PATCH 041/129] removing in favor of website matrix (#1052)

---
 docs/pbs-java-and-go-features-review.md | 42 -------------------------
 1 file changed, 42 deletions(-)
 delete mode 100644 docs/pbs-java-and-go-features-review.md

diff --git a/docs/pbs-java-and-go-features-review.md b/docs/pbs-java-and-go-features-review.md
deleted file mode 100644
index 44eeae86428..00000000000
--- a/docs/pbs-java-and-go-features-review.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Feature Differences Overview
-
-[Detailed Differences Description](differenceBetweenPBSGo-and-Java.md)
-
- Feature | Java | Go 
-| --- | :---: | :---:|
-GDPR TCF1.1 |+|+
-GDPR TCF2 |+|+
-Geo location (used for GDPR) |+|-
-COPPA |+|+
-CCPA |+|+
-AMP |+|+
-Stored Requests |+|+
-Stored Responses |+|-
-PBJS First Party data |+|-
-Currency Conversion** |+|+
-Multiple root schains |+|-
-Price Granularity |+|+
-Price Granularity per MediaType|+|-
-User ID Module support |+|+
-Account exclude list  |+|+
-Event Notification endpoint |+|-
-Video ad support |+|+
-Long-form video endpoint |-|+
-IAB advertiser category mapping |-|+
-Aliases |+|+
-Video Impression Tracking endpoint |+|-
-Cooperative Cookie Syncing |+|-
-Circuit Breaker (Http, DB) |+|-
-Operational metrics |+|+
-Supports both "debug" and "test" flags |+|-
-All adapters ported to OpenRTB |+|-
-Echo stored request video attributes in response |+|-
-Accept account ID on AMP requests |+|-
-Cache only-winning-bids flag |+|-
-Remote File Downloader |+|-
-Bidder Generator |+|-
-Passing `request.ext.prebid.bidders.BIDDER` to corresponding bidder |+|-
-
-**
-* PBS-Java Currency conversion supports finding intermediate conversion rate;
-* PBS-Go Currency Conversion debug endpoint exposes more information, PBS-Java currently provides last updated time only;

From 271b2013ebe6ba20d7e4e127fe14d2c6ac1235c9 Mon Sep 17 00:00:00 2001
From: Serhii Nahornyi 
Date: Mon, 7 Dec 2020 12:56:01 +0200
Subject: [PATCH 042/129] Review conversant adapter (#1053)

---
 .../bidder/conversant/ConversantBidder.java   |  97 ++++++------
 .../conversant/ConversantBidderTest.java      | 139 +++++++++++++-----
 2 files changed, 157 insertions(+), 79 deletions(-)

diff --git a/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java b/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java
index 583c5ed3320..cae7fcab8b4 100644
--- a/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java
+++ b/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java
@@ -7,6 +7,7 @@
 import com.iab.openrtb.request.Imp;
 import com.iab.openrtb.request.Site;
 import com.iab.openrtb.request.Video;
+import com.iab.openrtb.response.Bid;
 import com.iab.openrtb.response.BidResponse;
 import com.iab.openrtb.response.SeatBid;
 import io.vertx.core.http.HttpMethod;
@@ -26,7 +27,6 @@
 import org.prebid.server.proto.openrtb.ext.response.BidType;
 import org.prebid.server.util.HttpUtil;
 
-import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -40,8 +40,8 @@
  */
 public class ConversantBidder implements Bidder {
 
-    private static final TypeReference> CONVERSANT_EXT_TYPE_REFERENCE = new
-            TypeReference>() {
+    private static final TypeReference> CONVERSANT_EXT_TYPE_REFERENCE =
+            new TypeReference>() {
             };
 
     // List of API frameworks supported by the publisher
@@ -72,14 +72,13 @@ public Result>> makeHttpRequests(BidRequest bidRequ
         } catch (PreBidException e) {
             return Result.withError(BidderError.badInput(e.getMessage()));
         }
-        final String body = mapper.encode(outgoingRequest);
 
         return Result.of(Collections.singletonList(
                 HttpRequest.builder()
                         .method(HttpMethod.POST)
                         .uri(endpointUrl)
-                        .body(body)
                         .headers(HttpUtil.headers())
+                        .body(mapper.encode(outgoingRequest))
                         .payload(outgoingRequest)
                         .build()),
                 Collections.emptyList());
@@ -115,7 +114,7 @@ private ExtImpConversant parseImpExt(Imp imp, int impIndex) {
             throw new PreBidException(String.format("Impression[%d] missing ext.bidder object", impIndex));
         }
 
-        if (StringUtils.isBlank(extImp.getSiteId())) {
+        if (StringUtils.isEmpty(extImp.getSiteId())) {
             throw new PreBidException(String.format("Impression[%d] requires ext.bidder.site_id", impIndex));
         }
         return extImp;
@@ -130,88 +129,96 @@ private static App updateApp(App app, String siteId) {
     }
 
     private static Imp modifyImp(Imp imp, ExtImpConversant impExt) {
-        final BigDecimal extBidfloor = impExt.getBidfloor();
-        final String extTagId = impExt.getTagId();
-        final Integer extSecure = impExt.getSecure();
-        final boolean shouldChangeSecure = extSecure != null && (imp.getSecure() == null || imp.getSecure() == 0);
-        final Banner impBanner = imp.getBanner();
-        final Integer extPosition = impExt.getPosition();
-        final Video impVideo = imp.getVideo();
+        final Banner banner = imp.getBanner();
+        final Video video = imp.getVideo();
 
         return imp.toBuilder()
                 .displaymanager(DISPLAY_MANAGER)
                 .displaymanagerver(DISPLAY_MANAGER_VER)
-                .banner(modifyBanner(impBanner, extPosition))
-                .bidfloor(extBidfloor != null ? extBidfloor : imp.getBidfloor())
-                .tagid(extTagId != null ? extTagId : imp.getTagid())
-                .secure(shouldChangeSecure ? extSecure : imp.getSecure())
-                .video(impVideo != null && impBanner == null ? modifyVideo(impVideo, impExt) : impVideo)
+                .bidfloor(impExt.getBidfloor())
+                .tagid(impExt.getTagId())
+                .secure(getSecure(imp, impExt))
+                .banner(modifyBanner(banner, impExt.getPosition()))
+                .video(video != null && banner == null ? modifyVideo(video, impExt) : video)
                 .build();
     }
 
+    private static Integer getSecure(Imp imp, ExtImpConversant impExt) {
+        final Integer extSecure = impExt.getSecure();
+        final Integer impSecure = imp.getSecure();
+
+        return extSecure != null && (impSecure == null || impSecure == 0) ? extSecure : impSecure;
+    }
+
     private static Banner modifyBanner(Banner impBanner, Integer extPosition) {
-        return impBanner == null || extPosition == null
-                ? impBanner
+        return impBanner == null
+                ? null
                 : impBanner.toBuilder()
-                .pos(AD_POSITIONS.contains(extPosition) ? extPosition : null)
+                .pos(isValidPosition(extPosition) ? extPosition : null)
                 .build();
     }
 
     private static Video modifyVideo(Video video, ExtImpConversant impExt) {
         final List extMimes = impExt.getMimes();
-        final Integer extMaxduration = impExt.getMaxduration();
+        final Integer extMaxDuration = impExt.getMaxduration();
         final Integer extPosition = impExt.getPosition();
-        final List extProtocols = impExt.getProtocols();
-        final List extApi = impExt.getApi();
         return video.toBuilder()
-                .mimes(extMimes != null ? extMimes : video.getMimes())
-                .maxduration(extMaxduration != null ? extMaxduration : video.getMaxduration())
-                .pos(makePosition(extPosition, video.getPos()))
-                .api(makeApi(extApi, video.getApi()))
-                .protocols(makeProtocols(extProtocols, video.getProtocols()))
+                .mimes(CollectionUtils.isNotEmpty(extMimes) ? extMimes : video.getMimes())
+                .maxduration(extMaxDuration != null ? extMaxDuration : video.getMaxduration())
+                .pos(isValidPosition(extPosition) ? extPosition : null)
+                .api(makeApi(impExt.getApi(), video.getApi()))
+                .protocols(makeProtocols(impExt.getProtocols(), video.getProtocols()))
                 .build();
     }
 
-    private static Integer makePosition(Integer position, Integer videoPos) {
-        return isValidPosition(position) ? position : isValidPosition(videoPos) ? videoPos : null;
-    }
-
     private static boolean isValidPosition(Integer position) {
         return position != null && AD_POSITIONS.contains(position);
     }
 
     private static List makeApi(List extApi, List videoApi) {
-        final List protocols = CollectionUtils.isNotEmpty(extApi) ? extApi : videoApi;
-        return CollectionUtils.isNotEmpty(protocols)
-                ? protocols.stream().filter(APIS::contains).collect(Collectors.toList()) : videoApi;
+        final List api = CollectionUtils.isNotEmpty(extApi) ? extApi : videoApi;
+        return CollectionUtils.isNotEmpty(api)
+                ? api.stream().filter(APIS::contains).collect(Collectors.toList())
+                : videoApi;
     }
 
     private static List makeProtocols(List extProtocols, List videoProtocols) {
         final List protocols = CollectionUtils.isNotEmpty(extProtocols) ? extProtocols : videoProtocols;
         return CollectionUtils.isNotEmpty(protocols)
-                ? protocols.stream().filter(PROTOCOLS::contains).collect(Collectors.toList()) : videoProtocols;
+                ? protocols.stream().filter(PROTOCOLS::contains).collect(Collectors.toList())
+                : videoProtocols;
     }
 
     @Override
     public 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 | PreBidException e) {
+            return Result.withValues(extractBids(httpCall));
+        } catch (PreBidException e) {
             return Result.withError(BidderError.badServerResponse(e.getMessage()));
         }
     }
 
-    private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) {
+    private List extractBids(HttpCall httpCall) {
+        final BidResponse bidResponse;
+        try {
+            bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
+        } catch (DecodeException e) {
+            throw new PreBidException(e.getMessage());
+        }
         if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
             throw new PreBidException("Empty bid request");
         }
-        return bidsFromResponse(bidRequest, bidResponse);
+        return bidsFromResponse(httpCall.getRequest().getPayload(), bidResponse);
     }
 
     private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) {
         final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0);
-        return firstSeatBid.getBid().stream()
+        final List bids = firstSeatBid.getBid();
+
+        if (CollectionUtils.isEmpty(bids)) {
+            throw new PreBidException("Empty bids array");
+        }
+        return bids.stream()
                 .filter(Objects::nonNull)
                 .map(bid -> BidderBid.of(bid, getType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur()))
                 .collect(Collectors.toList());
@@ -219,8 +226,8 @@ private static List bidsFromResponse(BidRequest bidRequest, BidRespon
 
     private static BidType getType(String impId, List imps) {
         for (Imp imp : imps) {
-            if (imp.getId().equals(impId) && imp.getVideo() != null) {
-                return BidType.video;
+            if (imp.getId().equals(impId)) {
+                return imp.getVideo() != null ? BidType.video : BidType.banner;
             }
         }
         return BidType.banner;
diff --git a/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java b/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java
index 17090fff0b4..e87a25067bb 100644
--- a/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java
@@ -23,6 +23,7 @@
 import org.prebid.server.proto.openrtb.ext.request.conversant.ExtImpConversant;
 
 import java.math.BigDecimal;
+import java.util.Collections;
 import java.util.List;
 import java.util.function.Function;
 
@@ -79,8 +80,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
 
         // then
         assertThat(result.getErrors())
-                .hasSize(1)
-                .containsOnly(BidderError.badInput("Impression[0] missing ext.bidder object"));
+                .containsExactly(BidderError.badInput("Impression[0] missing ext.bidder object"));
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -98,8 +98,8 @@ public void makeHttpRequestsShouldReturnErrorIfRequestHasSiteAndImpExtSiteIdIsNu
         final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
 
         // then
-        assertThat(result.getErrors()).hasSize(1)
-                .containsOnly(BidderError.badInput("Impression[0] requires ext.bidder.site_id"));
+        assertThat(result.getErrors())
+                .containsExactly(BidderError.badInput("Impression[0] requires ext.bidder.site_id"));
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -120,7 +120,7 @@ public void makeHttpRequestsShouldSetSiteIdFromImpExtForSiteRequest() {
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .extracting(BidRequest::getSite)
                 .extracting(Site::getId)
-                .containsOnly("site id");
+                .containsExactly("site id");
     }
 
     @Test
@@ -140,7 +140,7 @@ public void makeHttpRequestsShouldSetAppIdFromExtSiteIdIfAppIdIsNullOrEmpty() {
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .extracting(BidRequest::getApp)
                 .extracting(App::getId)
-                .containsOnly("site id");
+                .containsExactly("site id");
     }
 
     @Test
@@ -161,11 +161,11 @@ public void makeHttpRequestsShouldSetSiteIdFromExtSiteIdIfSiteIdIsNullOrEmpty()
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .extracting(BidRequest::getSite)
                 .extracting(Site::getId)
-                .containsOnly("site id");
+                .containsExactly("site id");
     }
 
     @Test
-    public void makeHttpRequestsShouldSetImpDisplaymanagerAndDisplaymanagerver() {
+    public void makeHttpRequestsShouldSetImpDisplayManagerAndDisplayManagerVer() {
         // given
         final BidRequest bidRequest = givenBidRequest(identity());
 
@@ -178,11 +178,11 @@ public void makeHttpRequestsShouldSetImpDisplaymanagerAndDisplaymanagerver() {
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getDisplaymanager, Imp::getDisplaymanagerver)
-                .containsOnly(tuple("prebid-s2s", "2.0.0"));
+                .containsExactly(tuple("prebid-s2s", "2.0.0"));
     }
 
     @Test
-    public void makeHttpRequestsShouldSetImpBidfloorFromImpExtIfPresent() {
+    public void makeHttpRequestsShouldSetImpBidFloorFromImpExtIfPresent() {
         // given
         final BidRequest bidRequest = givenBidRequest(
                 identity(),
@@ -197,7 +197,7 @@ public void makeHttpRequestsShouldSetImpBidfloorFromImpExtIfPresent() {
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getBidfloor)
-                .containsOnly(BigDecimal.valueOf(9.99));
+                .containsExactly(BigDecimal.valueOf(9.99));
     }
 
     @Test
@@ -216,7 +216,7 @@ public void makeHttpRequestsShouldSetImpTagIdFromImpExtIfPresent() {
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getTagid)
-                .containsOnly("tag id");
+                .containsExactly("tag id");
     }
 
     @Test
@@ -235,7 +235,7 @@ public void makeHttpRequestsShouldChangeImpSecureFromImpExtIfPresent() {
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getSecure)
-                .containsOnly(1);
+                .containsExactly(1);
     }
 
     @Test
@@ -254,7 +254,7 @@ public void makeHttpRequestsShouldNotChangeImpSecure() {
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getSecure)
-                .containsOnly(1);
+                .containsExactly(1);
     }
 
     @Test
@@ -274,16 +274,35 @@ public void makeHttpRequestsShouldSetImpForBannerOnlyFromImpExtWhenVideoIsPresen
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getBanner, Imp::getVideo)
-                .containsOnly(tuple(
+                .containsExactly(tuple(
                         Banner.builder().pos(5).build(),
                         requestVideo));
     }
 
     @Test
-    public void makeHttpRequestsShouldNotChangeVideoPosFromImpExtIfNotInRange() {
+    public void makeHttpRequestsShouldSetPosBannerToNullIfExtPosIsNull() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder.banner(Banner.builder().pos(23).build()),
+                extBuilder -> extBuilder.position(null));
+
+        // when
+        final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getBanner)
+                .containsExactly(Banner.builder().pos(null).build());
+    }
+
+    @Test
+    public void makeHttpRequestsShouldSetPosBannerToNullIfExtPosIsNotValid() {
         // given
         final BidRequest bidRequest = givenBidRequest(
-                impBuilder -> impBuilder.video(Video.builder().pos(7).build()),
+                impBuilder -> impBuilder.banner(Banner.builder().pos(23).build()),
                 extBuilder -> extBuilder.position(9));
 
         // when
@@ -294,9 +313,27 @@ public void makeHttpRequestsShouldNotChangeVideoPosFromImpExtIfNotInRange() {
         assertThat(result.getValue()).hasSize(1)
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .flatExtracting(BidRequest::getImp)
-                .extracting(Imp::getVideo)
-                .extracting(Video::getPos)
-                .containsOnly(7);
+                .extracting(Imp::getBanner)
+                .containsExactly(Banner.builder().pos(null).build());
+    }
+
+    @Test
+    public void makeHttpRequestsShouldSetPosBannerToPosFromExt() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder.banner(Banner.builder().pos(23).build()),
+                extBuilder -> extBuilder.position(7));
+
+        // when
+        final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getBanner)
+                .containsExactly(Banner.builder().pos(7).build());
     }
 
     @Test
@@ -336,7 +373,27 @@ public void makeHttpRequestsShouldSetVideoMimesFromImpExtIfPresent() {
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getVideo)
                 .flatExtracting(Video::getMimes)
-                .containsOnly("mime");
+                .containsExactly("mime");
+    }
+
+    @Test
+    public void makeHttpRequestsShouldNotSetMimesFromExtIfExtMimesNotPresent() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder.video(Video.builder().mimes(singletonList("videoMimes")).build()),
+                extBuilder -> extBuilder.mimes(Collections.emptyList()));
+
+        // when
+        final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getVideo)
+                .flatExtracting(Video::getMimes)
+                .containsExactly("videoMimes");
     }
 
     @Test
@@ -356,7 +413,7 @@ public void makeHttpRequestsShouldSetVideoMaxdurationFromImpExtIfPresent() {
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getVideo)
                 .extracting(Video::getMaxduration)
-                .containsOnly(1000);
+                .containsExactly(1000);
     }
 
     @Test
@@ -376,7 +433,7 @@ public void makeHttpRequestsShouldChangeVideoApiFromImpExtIfPresent() {
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getVideo)
                 .flatExtracting(Video::getApi)
-                .containsOnly(2, 5);
+                .containsExactly(2, 5);
     }
 
     @Test
@@ -416,7 +473,7 @@ public void makeHttpRequestsShouldChangeVideoProtocolsFromImpExtIfPresent() {
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getVideo)
                 .flatExtracting(Video::getProtocols)
-                .containsOnly(1, 10);
+                .containsExactly(1, 10);
     }
 
     @Test
@@ -448,9 +505,11 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
         final Result> result = conversantBidder.makeBids(httpCall, null);
 
         // then
-        assertThat(result.getErrors()).hasSize(1);
-        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
-        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
+        assertThat(result.getErrors()).hasSize(1)
+                .allSatisfy(error -> {
+                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
+                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
+                });
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -465,8 +524,7 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces
 
         // then
         assertThat(result.getErrors())
-                .hasSize(1)
-                .containsOnly(BidderError.badServerResponse("Empty bid request"));
+                .containsExactly(BidderError.badServerResponse("Empty bid request"));
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -481,8 +539,7 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso
 
         // then
         assertThat(result.getErrors())
-                .hasSize(1)
-                .containsOnly(BidderError.badServerResponse("Empty bid request"));
+                .containsExactly(BidderError.badServerResponse("Empty bid request"));
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -501,7 +558,22 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
         // then
         assertThat(result.getErrors()).isEmpty();
         assertThat(result.getValue())
-                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
+                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnErrorIfNoBidsFromSeatArePresent() throws JsonProcessingException {
+        // given
+        final HttpCall httpCall = givenHttpCall(null,
+                mapper.writeValueAsString(BidResponse.builder()
+                        .seatbid(singletonList(SeatBid.builder().build())).build()));
+
+        // when
+        final Result> result = conversantBidder.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).containsOnly(BidderError.badServerResponse("Empty bids array"));
+        assertThat(result.getValue()).isEmpty();
     }
 
     @Test
@@ -520,7 +592,7 @@ public void makeBidsShouldReturnVideoBidIfRequestImpHasVideo() throws JsonProces
         // then
         assertThat(result.getErrors()).isEmpty();
         assertThat(result.getValue())
-                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
+                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
     }
 
     private static BidRequest givenBidRequest(
@@ -553,7 +625,6 @@ private static Imp givenImp(
 
         return impCustomizer.apply(Imp.builder()
                 .id("123")
-
                 .ext(mapper.valueToTree(ExtPrebid.of(null,
                         extCustomizer.apply(ExtImpConversant.builder().siteId("site id")).build()))))
                 .build();

From 128112191c19bb9e03647a41741fb1b4e57efb09 Mon Sep 17 00:00:00 2001
From: rpanchyk 
Date: Tue, 8 Dec 2020 12:28:05 +0200
Subject: [PATCH 043/129] Prebid Server prepare release 1.51.0

---
 pom.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index 2cce470ad84..fee11a0139e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 
     org.prebid
     prebid-server
-    1.51.0-SNAPSHOT
+    1.51.0
 
     prebid-server
     Prebid Server (Server-side Header Bidding)
@@ -15,7 +15,7 @@
 
     
         scm:git:git@github.com:prebid/prebid-server-java.git
-        HEAD
+        1.51.0
     
 
     

From 81669c4d172686e880071563323466ad6e87ee23 Mon Sep 17 00:00:00 2001
From: rpanchyk 
Date: Tue, 8 Dec 2020 12:28:20 +0200
Subject: [PATCH 044/129] Prebid Server prepare for next development iteration

---
 pom.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index fee11a0139e..c05133abc29 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 
     org.prebid
     prebid-server
-    1.51.0
+    1.52.0-SNAPSHOT
 
     prebid-server
     Prebid Server (Server-side Header Bidding)
@@ -15,7 +15,7 @@
 
     
         scm:git:git@github.com:prebid/prebid-server-java.git
-        1.51.0
+        HEAD
     
 
     

From 1badbe3733696f2e97a9d5e2645db8b8092fc4ce Mon Sep 17 00:00:00 2001
From: Serhii Nahornyi 
Date: Tue, 8 Dec 2020 16:38:14 +0200
Subject: [PATCH 045/129] GumGum bidder review (#1059)

---
 .../server/bidder/gumgum/GumgumBidder.java    |  54 +++++----
 .../bidder/gumgum/GumgumBidderTest.java       | 111 +++++++++++++-----
 .../java/org/prebid/server/it/GumgumTest.java |   6 +-
 .../gumgum/test-auction-gumgum-request.json   |   3 +-
 .../gumgum/test-gumgum-bid-request-1.json     |   3 +-
 5 files changed, 125 insertions(+), 52 deletions(-)

diff --git a/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java b/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java
index cbede418bbd..e5cfe5dcacc 100644
--- a/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java
+++ b/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java
@@ -13,6 +13,8 @@
 import io.vertx.core.http.HttpMethod;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.bidder.Bidder;
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.bidder.model.BidderError;
@@ -21,13 +23,13 @@
 import org.prebid.server.bidder.model.Result;
 import org.prebid.server.exception.PreBidException;
 import org.prebid.server.json.DecodeException;
-import org.prebid.server.json.EncodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.proto.openrtb.ext.ExtPrebid;
 import org.prebid.server.proto.openrtb.ext.request.gumgum.ExtImpGumgum;
 import org.prebid.server.proto.openrtb.ext.response.BidType;
 import org.prebid.server.util.HttpUtil;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -36,6 +38,9 @@
 import java.util.Objects;
 import java.util.stream.Collectors;
 
+/**
+ * Gumgum {@link Bidder} implementation.
+ */
 public class GumgumBidder implements Bidder {
 
     private static final TypeReference> GUMGUM_EXT_TYPE_REFERENCE =
@@ -53,27 +58,20 @@ public GumgumBidder(String endpointUrl, JacksonMapper mapper) {
     @Override
     public Result>> makeHttpRequests(BidRequest bidRequest) {
         final List errors = new ArrayList<>();
+
         final BidRequest outgoingRequest;
         try {
             outgoingRequest = createBidRequest(bidRequest, errors);
         } catch (PreBidException e) {
             errors.add(BidderError.badInput(e.getMessage()));
-            return Result.of(Collections.emptyList(), errors);
-        }
-
-        String body;
-        try {
-            body = mapper.encode(outgoingRequest);
-        } catch (EncodeException e) {
-            errors.add(BidderError.badInput(String.format("Failed to encode request body, error: %s", e.getMessage())));
-            return Result.of(Collections.emptyList(), errors);
+            return Result.withErrors(errors);
         }
 
         return Result.of(Collections.singletonList(
                 HttpRequest.builder()
                         .method(HttpMethod.POST)
                         .uri(endpointUrl)
-                        .body(body)
+                        .body(mapper.encode(outgoingRequest))
                         .headers(HttpUtil.headers())
                         .payload(outgoingRequest)
                         .build()),
@@ -118,23 +116,29 @@ private ExtImpGumgum parseImpExt(Imp imp) {
         try {
             return mapper.mapper().convertValue(imp.getExt(), GUMGUM_EXT_TYPE_REFERENCE).getBidder();
         } catch (IllegalArgumentException e) {
-            throw new PreBidException(e.getMessage(), e);
+            throw new PreBidException(e.getMessage());
         }
     }
 
     private static Imp modifyImp(Imp imp) {
-        final Banner banner = imp.getBanner();
-        final List format = banner.getFormat();
-        if (banner.getH() == null && banner.getW() == null && CollectionUtils.isNotEmpty(format)) {
-            final Format firstFormat = format.get(0);
-            final Banner modifiedBanner = banner.toBuilder().w(firstFormat.getW()).h(firstFormat.getH()).build();
+        final Banner resolvedBanner = resolveBanner(imp.getBanner());
+        if (resolvedBanner != null) {
             return imp.toBuilder()
-                    .banner(modifiedBanner)
+                    .banner(resolvedBanner)
                     .build();
         }
         return imp;
     }
 
+    private static Banner resolveBanner(Banner banner) {
+        final List format = banner.getFormat();
+        if (banner.getH() == null && banner.getW() == null && CollectionUtils.isNotEmpty(format)) {
+            final Format firstFormat = format.get(0);
+            return banner.toBuilder().w(firstFormat.getW()).h(firstFormat.getH()).build();
+        }
+        return null;
+    }
+
     private void validateVideoParams(Video video) {
         if (anyOfNull(
                 video.getW(),
@@ -156,7 +160,7 @@ private static boolean isNullOrZero(Integer number) {
     }
 
     private static Site modifySite(Site site, String trackingId) {
-        return site != null ? site.toBuilder().id(trackingId).build() : null;
+        return site != null ? site.toBuilder().id(ObjectUtils.defaultIfNull(trackingId, "")).build() : null;
     }
 
     @Override
@@ -177,8 +181,10 @@ private static List extractBids(BidResponse bidResponse, BidRequest b
 
     private static List bidsFromResponse(BidResponse bidResponse, BidRequest bidRequest) {
         return bidResponse.getSeatbid().stream()
+                .filter(Objects::nonNull)
                 .map(SeatBid::getBid)
                 .flatMap(Collection::stream)
+                .filter(Objects::nonNull)
                 .map(bid -> toBidderBid(bid, bidRequest, bidResponse.getCur()))
                 .collect(Collectors.toList());
     }
@@ -186,17 +192,21 @@ private static List bidsFromResponse(BidResponse bidResponse, BidRequ
     private static BidderBid toBidderBid(Bid bid, BidRequest bidRequest, String currency) {
         final BidType bidType = getMediaType(bid.getImpid(), bidRequest.getImp());
         final Bid updatedBid = bidType == BidType.video
-                ? bid.toBuilder().adm(bid.getAdm().replace("${AUCTION_PRICE}", String.valueOf(bid.getPrice()))).build()
+                ? bid.toBuilder().adm(resolveAdm(bid.getAdm(), bid.getPrice())).build()
                 : bid;
         return BidderBid.of(updatedBid, bidType, currency);
     }
 
     private static BidType getMediaType(String impId, List requestImps) {
         for (Imp imp : requestImps) {
-            if (imp.getId().equals(impId) && imp.getBanner() != null) {
-                return BidType.banner;
+            if (imp.getId().equals(impId)) {
+                return imp.getBanner() != null ? BidType.banner : BidType.video;
             }
         }
         return BidType.video;
     }
+
+    private static String resolveAdm(String bidAdm, BigDecimal price) {
+        return StringUtils.isNotBlank(bidAdm) ? bidAdm.replace("${AUCTION_PRICE}", String.valueOf(price)) : bidAdm;
+    }
 }
diff --git a/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java b/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java
index efb7e949868..30eba2a7588 100644
--- a/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java
@@ -54,7 +54,7 @@ public void creationShouldFailOnInvalidEndpointUrl() {
     }
 
     @Test
-    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
+    public void makeHttpRequestsShouldReturnErrorsIfImpExtCouldNotBeParsed() {
         // given
         final BidRequest bidRequest = givenBidRequest(
                 impBuilder -> impBuilder
@@ -64,8 +64,15 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
         final Result>> result = gumgumBidder.makeHttpRequests(bidRequest);
 
         // then
-        assertThat(result.getErrors()).hasSize(2);
-        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
+        assertThat(result.getErrors()).hasSize(2)
+                .anySatisfy(error -> {
+                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
+                    assertThat(error.getMessage()).isEqualTo("No valid impressions");
+                })
+                .anySatisfy(error -> {
+                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
+                    assertThat(error.getMessage()).startsWith("Cannot deserialize instance");
+                });
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -82,8 +89,8 @@ public void makeHttpRequestsShouldReturnErrorIfNoValidImpressions() {
         final Result>> result = gumgumBidder.makeHttpRequests(bidRequest);
 
         // then
-        assertThat(result.getErrors()).hasSize(1)
-                .containsOnly(BidderError.badInput("No valid impressions"));
+        assertThat(result.getErrors())
+                .containsExactly(BidderError.badInput("No valid impressions"));
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -101,9 +108,9 @@ public void makeHttpRequestsShouldReturnErrorIfVideoFieldsAreNotValid() {
         final Result>> result = gumgumBidder.makeHttpRequests(bidRequest);
 
         // then
-        assertThat(result.getErrors()).hasSize(2);
-        assertThat(result.getErrors().get(0).getMessage()).startsWith("Invalid or missing video field(s)");
-        assertThat(result.getErrors().get(1).getMessage()).startsWith("No valid impressions");
+        assertThat(result.getErrors())
+                .containsExactlyInAnyOrder(BidderError.badInput("Invalid or missing video field(s)"),
+                        BidderError.badInput("No valid impressions"));
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -121,12 +128,37 @@ public void makeHttpRequestsShouldSkipImpressionsWithoutBannerOrVideo() {
 
         // then
         assertThat(result.getErrors()).isEmpty();
-        assertThat(result.getValue()).hasSize(1)
-                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getBanner)
                 .extracting(Banner::getId)
-                .containsOnly("banner_id");
+                .containsExactly("banner_id");
+    }
+
+    @Test
+    public void makeHttpRequestsShouldSetEmtyStringIfZoneIsNull() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder ->
+                        bidRequestBuilder.site(Site.builder().build()),
+                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpGumgum.of(null)))));
+        BidRequest.builder()
+                .site(Site.builder().build())
+                .imp(asList(
+                        givenImp(impBuilder -> impBuilder.banner(null).video(null).audio(Audio.builder().build())),
+                        givenImp(identity())))
+                .build();
+
+        // when
+        final Result>> result = gumgumBidder.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
+                .extracting(BidRequest::getSite)
+                .extracting(Site::getId)
+                .containsExactly("");
     }
 
     @Test
@@ -144,20 +176,19 @@ public void makeHttpRequestsShouldNotChangeBannerWidthAndHeightIfPresent() {
 
         // then
         assertThat(result.getErrors()).isEmpty();
-        assertThat(result.getValue()).hasSize(1)
+        assertThat(result.getValue())
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getBanner)
                 .extracting(Banner::getW, Banner::getH)
-                .containsOnly(tuple(600, 900));
+                .containsExactly(tuple(600, 900));
     }
 
     @Test
     public void makeHttpRequestsShouldSetBannerWidthAndHeightFromfirstFormatIfAbsent() {
         // given
         final BidRequest bidRequest = givenBidRequest(
-                impBuilder -> impBuilder
-                        .banner(Banner.builder()
+                impBuilder -> impBuilder.banner(Banner.builder()
                                 .format(singletonList(Format.builder().w(300).h(450).build()))
                                 .build()));
 
@@ -166,12 +197,12 @@ public void makeHttpRequestsShouldSetBannerWidthAndHeightFromfirstFormatIfAbsent
 
         // then
         assertThat(result.getErrors()).isEmpty();
-        assertThat(result.getValue()).hasSize(1)
-                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getBanner)
                 .extracting(Banner::getW, Banner::getH)
-                .containsOnly(tuple(300, 450));
+                .containsExactly(tuple(300, 450));
     }
 
     @Test
@@ -185,7 +216,7 @@ public void makeHttpRequestsShouldNotMakeSiteIfAbsent() {
         // then
         assertThat(result.getErrors()).isEmpty();
         assertThat(result.getValue()).hasSize(1)
-                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+                .extracting(HttpRequest::getPayload)
                 .extracting(BidRequest::getSite)
                 .containsNull();
     }
@@ -211,11 +242,11 @@ public void makeHttpRequestsShouldSetSiteIdFromLastValidImpExtZone() {
 
         // then
         assertThat(result.getErrors()).isEmpty();
-        assertThat(result.getValue()).hasSize(1)
-                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
                 .extracting(BidRequest::getSite)
                 .extracting(Site::getId)
-                .containsOnly("zone");
+                .containsExactly("zone");
     }
 
     @Test
@@ -227,9 +258,11 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
         final Result> result = gumgumBidder.makeBids(httpCall, null);
 
         // then
-        assertThat(result.getErrors()).hasSize(1);
-        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
-        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
+        assertThat(result.getErrors()).hasSize(1)
+                .allSatisfy(error -> {
+                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
+                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
+                });
         assertThat(result.getValue()).isEmpty();
     }
 
@@ -284,7 +317,7 @@ public void makeBidsShouldReturnVideoBid() throws JsonProcessingException {
                 .build();
         assertThat(result.getErrors()).isEmpty();
         assertThat(result.getValue())
-                .containsOnly(BidderBid.of(expectedBid, video, "USD"));
+                .containsExactly(BidderBid.of(expectedBid, video, "USD"));
     }
 
     @Test
@@ -302,7 +335,31 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
         // then
         assertThat(result.getErrors()).isEmpty();
         assertThat(result.getValue())
-                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
+                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
+    }
+
+    @Test
+    public void makeBidsShouldTolerateWithNullSeatOrBidValues() throws JsonProcessingException {
+        // given
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
+                .build();
+
+        final BidResponse bidResponse = BidResponse.builder()
+                .cur("USD")
+                .seatbid(asList(SeatBid.builder()
+                        .bid(asList(Bid.builder().id("123").build(), null))
+                        .build(), null))
+                .build();
+
+        final HttpCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse));
+
+        // when
+        final Result> result = gumgumBidder.makeBids(httpCall, bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1);
     }
 
     private static BidRequest givenBidRequest(
diff --git a/src/test/java/org/prebid/server/it/GumgumTest.java b/src/test/java/org/prebid/server/it/GumgumTest.java
index 76e13f5aeaa..16e53a7385c 100644
--- a/src/test/java/org/prebid/server/it/GumgumTest.java
+++ b/src/test/java/org/prebid/server/it/GumgumTest.java
@@ -11,6 +11,8 @@
 import java.io.IOException;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToIgnoreCase;
 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;
@@ -25,6 +27,8 @@ public void openrtb2AuctionShouldRespondWithBidsFromGumGum() throws IOException,
         // given
         // GumGum bid response for imp 001 and 002
         WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/gumgum-exchange"))
+                .withHeader("Accept", equalTo("application/json"))
+                .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8"))
                 .withRequestBody(equalToJson(jsonFrom("openrtb2/gumgum/test-gumgum-bid-request-1.json")))
                 .willReturn(aResponse().withBody(jsonFrom("openrtb2/gumgum/test-gumgum-bid-response-1.json"))));
 
@@ -40,7 +44,7 @@ public void openrtb2AuctionShouldRespondWithBidsFromGumGum() throws IOException,
                 .header("User-Agent", "userAgent")
                 .header("Origin", "http://www.example.com")
                 // this uids cookie value stands for {"uids":{"gum":"GUM-UID"}}
-                .cookie("uids", "eyJ1aWRzIjp7Imd1bSI6IkdVTS1VSUQifX0=")
+                .cookie("uids", "eyJ1aWRzIjp7Imd1bWd1bSI6IkdVTS1VSUQifX0=")
                 .body(jsonFrom("openrtb2/gumgum/test-auction-gumgum-request.json"))
                 .post("/openrtb2/auction");
 
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json
index 0f7bd5cd45e..f7a9dbc5f0a 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json
@@ -103,6 +103,7 @@
     }
   },
   "user": {
+    "buyeruid" : "GUM-UID",
     "ext": {
       "consent": "consentValue",
       "digitrust": {
@@ -117,4 +118,4 @@
       "gdpr": 0
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request-1.json
index 62062dd5d81..0a29b8e6729 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request-1.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request-1.json
@@ -82,6 +82,7 @@
     "ifa": "ifaId"
   },
   "user": {
+    "buyeruid" : "GUM-UID",
     "ext": {
       "consent" : "consentValue",
       "digitrust": {
@@ -132,4 +133,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}

From 95efc19a03534e8e771ee2c5e1305b30a55ac1f5 Mon Sep 17 00:00:00 2001
From: Dmitriy 
Date: Tue, 8 Dec 2020 16:38:47 +0200
Subject: [PATCH 046/129] Review Smaato bidder (#1060)

---
 .../server/bidder/smaato/SmaatoBidder.java    | 129 +++++++++---------
 .../bidder/smaato/SmaatoBidderTest.java       |  71 +++++++++-
 .../java/org/prebid/server/it/SmaatoTest.java |   1 -
 3 files changed, 138 insertions(+), 63 deletions(-)

diff --git a/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java b/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java
index aa903fd7d2b..4b4b3569fe1 100644
--- a/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java
+++ b/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java
@@ -15,6 +15,7 @@
 import io.vertx.core.MultiMap;
 import io.vertx.core.http.HttpMethod;
 import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.bidder.Bidder;
 import org.prebid.server.bidder.model.BidderBid;
@@ -50,6 +51,9 @@
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+/**
+ * Smaato {@link Bidder} implementation.
+ */
 public class SmaatoBidder implements Bidder {
 
     private static final TypeReference> SMAATO_EXT_TYPE_REFERENCE =
@@ -57,6 +61,7 @@ public class SmaatoBidder implements Bidder {
             };
 
     private static final String CLIENT_VERSION = "prebid_server_0.1";
+    private static final String SMT_ADTYPE_HEADER = "X-SMT-ADTYPE";
     private static final String SMT_AD_TYPE_IMG = "Img";
     private static final String SMT_ADTYPE_RICHMEDIA = "Richmedia";
     private static final String SMT_ADTYPE_VIDEO = "Video";
@@ -86,23 +91,19 @@ public Result>> makeHttpRequests(BidRequest request
             }
         }
 
-        final Site modifiedSite;
-        final User modifiedUser;
+        final BidRequest outgoingRequest;
         try {
-            modifiedSite = request.getSite() != null ? modifySite(request.getSite(), firstPublisherId) : null;
-            modifiedUser = request.getUser() != null ? modifyUser(request.getUser()) : null;
+            outgoingRequest = request.toBuilder()
+                    .imp(imps)
+                    .site(modifySite(request.getSite(), firstPublisherId))
+                    .user(modifyUser(request.getUser()))
+                    .ext(mapper.fillExtension(ExtRequest.empty(), SmaatoBidRequestExt.of(CLIENT_VERSION)))
+                    .build();
         } catch (PreBidException e) {
             errors.add(BidderError.badInput(e.getMessage()));
             return Result.withErrors(errors);
         }
 
-        final BidRequest outgoingRequest = request.toBuilder()
-                .imp(imps)
-                .site(modifiedSite)
-                .user(modifiedUser)
-                .ext(mapper.fillExtension(ExtRequest.empty(), SmaatoBidRequestExt.of(CLIENT_VERSION)))
-                .build();
-
         return Result.of(Collections.singletonList(
                 HttpRequest.builder()
                         .method(HttpMethod.POST)
@@ -148,6 +149,10 @@ private static Banner modifyBanner(Banner banner) {
     }
 
     private Site modifySite(Site site, String firstPublisherId) {
+        if (site == null) {
+            return null;
+        }
+
         final Site.SiteBuilder siteBuilder = site.toBuilder()
                 .publisher(Publisher.builder().id(firstPublisherId).build());
 
@@ -162,37 +167,37 @@ private Site modifySite(Site site, String firstPublisherId) {
     }
 
     private User modifyUser(User user) {
-        final ExtUser ext = user.getExt();
-        if (ext == null) {
-            return user;
+        if (user == null) {
+            return null;
         }
 
-        final ObjectNode extDataNode = ext.getData();
-        if (extDataNode == null) {
+        final ExtUser userExt = user.getExt();
+        final ObjectNode extDataNode = userExt != null ? userExt.getData() : null;
+        if (extDataNode == null || extDataNode.isEmpty()) {
             return user;
         }
 
+        final SmaatoUserExtData smaatoUserExtData = convertExt(extDataNode, SmaatoUserExtData.class);
         final User.UserBuilder userBuilder = user.toBuilder();
-        final SmaatoUserExtData data = convertExt(extDataNode, SmaatoUserExtData.class);
 
-        final String gender = data != null ? data.getGender() : null;
+        final String gender = smaatoUserExtData.getGender();
         if (StringUtils.isNotBlank(gender)) {
             userBuilder.gender(gender);
         }
 
-        final Integer yob = data != null ? data.getYob() : null;
+        final Integer yob = smaatoUserExtData.getYob();
         if (yob != null && yob != 0) {
             userBuilder.yob(yob);
         }
 
-        final String keywords = data != null ? data.getKeywords() : null;
+        final String keywords = smaatoUserExtData.getKeywords();
         if (StringUtils.isNotBlank(keywords)) {
             userBuilder.keywords(keywords);
         }
 
-        userBuilder.ext(ext.toBuilder().data(null).build());
-
-        return userBuilder.build();
+        return userBuilder
+                .ext(userExt.toBuilder().data(null).build())
+                .build();
     }
 
     private  T convertExt(ObjectNode ext, Class className) {
@@ -210,6 +215,8 @@ public Result> makeBids(HttpCall httpCall, BidReques
             return extractBids(bidResponse, httpCall.getResponse().getHeaders());
         } catch (DecodeException e) {
             return Result.withError(BidderError.badServerResponse(e.getMessage()));
+        } catch (PreBidException e) {
+            return Result.withError(BidderError.badInput(e.getMessage()));
         }
     }
 
@@ -218,31 +225,29 @@ private Result> extractBids(BidResponse bidResponse, MultiMap he
             return Result.empty();
         }
 
-        final List errors = new ArrayList<>();
-        final List bidderBids = bidResponse.getSeatbid().stream()
+        return Result.withValues(bidResponse.getSeatbid().stream()
                 .filter(Objects::nonNull)
                 .map(SeatBid::getBid)
                 .filter(Objects::nonNull)
                 .flatMap(Collection::stream)
-                .map(bid -> bidderBid(bid, bidResponse.getCur(), headers, errors))
+                .map(bid -> bidderBid(bid, bidResponse.getCur(), headers))
                 .filter(Objects::nonNull)
-                .collect(Collectors.toList());
-        return Result.of(bidderBids, errors);
+                .collect(Collectors.toList()));
     }
 
-    private BidderBid bidderBid(Bid bid, String currency, MultiMap headers, List errors) {
-        try {
-            final String markupType = getAdMarkupType(headers, bid.getAdm());
-            final Bid updateBid = bid.toBuilder().adm(renderAdMarkup(markupType, bid.getAdm())).build();
-            return BidderBid.of(updateBid, getBidType(markupType), currency);
-        } catch (PreBidException e) {
-            errors.add(BidderError.badInput(e.getMessage()));
-            return null;
+    private BidderBid bidderBid(Bid bid, String currency, MultiMap headers) {
+        final String bidAdm = bid.getAdm();
+        if (StringUtils.isBlank(bidAdm)) {
+            throw new PreBidException(String.format("Empty ad markup in bid with id: %s", bid.getId()));
         }
+
+        final String markupType = getAdMarkupType(headers, bidAdm);
+        final Bid updateBid = bid.toBuilder().adm(renderAdMarkup(markupType, bidAdm)).build();
+        return BidderBid.of(updateBid, getBidType(markupType), currency);
     }
 
     private static String getAdMarkupType(MultiMap headers, String adm) {
-        final String adMarkupType = headers.get("X-SMT-ADTYPE");
+        final String adMarkupType = headers.get(SMT_ADTYPE_HEADER);
         if (StringUtils.isNotBlank(adMarkupType)) {
             return adMarkupType;
         }
@@ -271,30 +276,18 @@ private String renderAdMarkup(String markupType, String adm) {
         }
     }
 
-    private static BidType getBidType(String markupType) {
-        switch (markupType) {
-            case SMT_AD_TYPE_IMG:
-            case SMT_ADTYPE_RICHMEDIA:
-                return BidType.banner;
-            case SMT_ADTYPE_VIDEO:
-                return BidType.video;
-            default:
-                throw new PreBidException(String.format("Invalid markupType %s", markupType));
-        }
-    }
-
     private String extractAdmImage(String adm) {
-        final SmaatoImageAd imageAd = admToAd(adm, SmaatoImageAd.class);
+        final SmaatoImageAd imageAd = convertAdmToAd(adm, SmaatoImageAd.class);
         final SmaatoImage image = imageAd.getImage();
         if (image == null) {
-            return adm;
+            throw new PreBidException("bid.adm.image is empty");
         }
 
         final StringBuilder clickEvent = new StringBuilder();
         CollectionUtils.emptyIfNull(image.getClickTrackers())
                 .forEach(tracker -> clickEvent.append(String.format(
                         "fetch(decodeURIComponent('%s'.replace(/\\+/g, ' ')), {cache: 'no-cache'});",
-                        HttpUtil.encodeUrl(tracker))));
+                        HttpUtil.encodeUrl(StringUtils.stripToEmpty(tracker)))));
 
         final StringBuilder impressionTracker = new StringBuilder();
         CollectionUtils.emptyIfNull(image.getImpressionTrackers())
@@ -302,35 +295,33 @@ private String extractAdmImage(String adm) {
                         String.format("\"\"", tracker)));
 
         final SmaatoImg img = image.getImg();
-
         return String.format("
%s
", clickEvent.toString(), HttpUtil.encodeUrl(StringUtils.stripToEmpty(getIfNotNull(img, SmaatoImg::getCtaurl))), StringUtils.stripToEmpty(getIfNotNull(img, SmaatoImg::getUrl)), - getIfNotNull(img, SmaatoImg::getW), - getIfNotNull(img, SmaatoImg::getH), + stripToZero(getIfNotNull(img, SmaatoImg::getW)), + stripToZero(getIfNotNull(img, SmaatoImg::getH)), impressionTracker.toString()); } private String extractAdmRichMedia(String adm) { - final SmaatoRichMediaAd richMediaAd = admToAd(adm, SmaatoRichMediaAd.class); + final SmaatoRichMediaAd richMediaAd = convertAdmToAd(adm, SmaatoRichMediaAd.class); final SmaatoRichmedia richmedia = richMediaAd.getRichmedia(); if (richmedia == null) { - return adm; + throw new PreBidException("bid.adm.richmedia is empty"); } final StringBuilder clickEvent = new StringBuilder(); CollectionUtils.emptyIfNull(richmedia.getClickTrackers()) .forEach(tracker -> clickEvent.append( String.format("fetch(decodeURIComponent('%s'), {cache: 'no-cache'});", - HttpUtil.encodeUrl(tracker)))); + HttpUtil.encodeUrl(StringUtils.stripToEmpty(tracker))))); final StringBuilder impressionTracker = new StringBuilder(); CollectionUtils.emptyIfNull(richmedia.getImpressionTrackers()) .forEach(tracker -> impressionTracker.append( - String.format("\"\"", - tracker))); + String.format("\"\"", tracker))); return String.format("
%s%s
", clickEvent.toString(), @@ -338,7 +329,7 @@ private String extractAdmRichMedia(String adm) { impressionTracker.toString()); } - private T admToAd(String value, Class className) { + private T convertAdmToAd(String value, Class className) { try { return mapper.decodeValue(value, className); } catch (DecodeException e) { @@ -346,7 +337,23 @@ private T admToAd(String value, Class className) { } } + private static BidType getBidType(String markupType) { + switch (markupType) { + case SMT_AD_TYPE_IMG: + case SMT_ADTYPE_RICHMEDIA: + return BidType.banner; + case SMT_ADTYPE_VIDEO: + return BidType.video; + default: + throw new PreBidException(String.format("Invalid markupType %s", markupType)); + } + } + private static R getIfNotNull(T target, Function getter) { return target != null ? getter.apply(target) : null; } + + private static int stripToZero(Integer target) { + return ObjectUtils.defaultIfNull(target, 0); + } } diff --git a/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java b/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java index 28354f611f1..d23ebbcf1ef 100644 --- a/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java @@ -249,6 +249,21 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces assertThat(result.getValue()).isEmpty(); } + @Test + public void makeBidsShouldReturnErrorIfNoBidAdm() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(BidRequest.builder().build(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.id("test").impid("123"))), null); + + // when + final Result> result = smaatoBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Empty ad markup in bid with id: test")); + assertThat(result.getValue()).isEmpty(); + } + @Test public void makeBidsShouldReturnErrorIfNotSupportedMarkupType() throws JsonProcessingException { // given @@ -257,7 +272,7 @@ public void makeBidsShouldReturnErrorIfNotSupportedMarkupType() throws JsonProce .imp(singletonList(Imp.builder().id("123").build())) .build(), mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123"))), headers); + givenBidResponse(bidBuilder -> bidBuilder.impid("123").adm("adm"))), headers); // when final Result> result = smaatoBidder.makeBids(httpCall, null); @@ -287,6 +302,25 @@ public void makeBidsShouldReturnErrorIfMarkupTypeIsBlank() throws JsonProcessing assertThat(result.getValue()).isEmpty(); } + @Test + public void makeBidsShouldReturnErrorIfAdmIsInvalid() throws JsonProcessingException { + // given + final MultiMap headers = MultiMap.caseInsensitiveMultiMap().set("X-SMT-ADTYPE", ""); + final HttpCall httpCall = givenHttpCall(BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").adm("{\"image\": invalid"))), headers); + + // when + final Result> result = smaatoBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Invalid ad markup {\"image\": invalid")); + assertThat(result.getValue()).isEmpty(); + } + @Test public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsReachmedia() throws JsonProcessingException { // given @@ -353,6 +387,41 @@ public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsImg() throws JsonProcess .containsOnly(BidderBid.of(expectedBid, banner, "USD")); } + @Test + public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsImgAndParametersAreEmpty() throws JsonProcessingException { + // given + final MultiMap headers = MultiMap.caseInsensitiveMultiMap().set("X-SMT-ADTYPE", "Img"); + final HttpCall httpCall = givenHttpCall(BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").adm("{\"image\":{\"img\":{\"url\":\"" + + "//prebid-test.smaatolabs.net/img/320x50.jpg\",\"ctaurl\":\"" + + "//prebid-test.smaatolabs.net/track/ctaurl/1\"},\"impressiontrackers\":[\"" + + "//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/" + + "imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"" + + "//prebid-test.smaatolabs.net/track/click/2\"]}}"))), headers); + + // when + final Result> result = smaatoBidder.makeBids(httpCall, null); + + // then + final Bid expectedBid = Bid.builder() + .impid("123") + .adm("
\"\"\"\"
") + .build(); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(expectedBid, banner, "USD")); + } + @Test public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsVideo() throws JsonProcessingException { // given diff --git a/src/test/java/org/prebid/server/it/SmaatoTest.java b/src/test/java/org/prebid/server/it/SmaatoTest.java index 0ed182b2aac..a7d5d7ab579 100644 --- a/src/test/java/org/prebid/server/it/SmaatoTest.java +++ b/src/test/java/org/prebid/server/it/SmaatoTest.java @@ -24,7 +24,6 @@ public class SmaatoTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromSmaato() throws IOException, JSONException { // given - // SmaatoBidder bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smaato-exchange")) .withHeader("Accept", equalTo("application/json")) .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) From b67d88432839f74b361074d0728e720bc15f4737 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Tue, 8 Dec 2020 16:39:09 +0200 Subject: [PATCH 047/129] Set 1.51.1 project version (#1061) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c05133abc29..1c2caad2482 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.52.0-SNAPSHOT + 1.51.1-SNAPSHOT prebid-server Prebid Server (Server-side Header Bidding) From 16c70c3d498dd9498b007c73cdffb985201ec84c Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Tue, 8 Dec 2020 17:39:28 +0200 Subject: [PATCH 048/129] Deepintent redirect-url fix (#1062) --- src/main/resources/bidder-config/deepintent.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/bidder-config/deepintent.yaml b/src/main/resources/bidder-config/deepintent.yaml index 10182ae49d7..a833dff3bba 100644 --- a/src/main/resources/bidder-config/deepintent.yaml +++ b/src/main/resources/bidder-config/deepintent.yaml @@ -17,7 +17,7 @@ adapters: vendor-id: 541 usersync: url: https://cdn.deepintent.com/syncpixel.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir= - redirect-url: /setuid?bidder=deepintent&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=[uid] + redirect-url: /setuid?bidder=deepintent&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=[UID] cookie-family-name: deepintent - type: redirect + type: iframe support-cors: false From 5f4d70cb387bf0d007887661f8f00c70641dc654 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Wed, 9 Dec 2020 12:39:17 +0200 Subject: [PATCH 049/129] Prebid Server prepare release 1.51.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 1c2caad2482..6068445e44e 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.51.1-SNAPSHOT + 1.51.1 prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 1.51.1 From 7867aeead17d657c2c1e3cf35ff184cf139220d3 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Wed, 9 Dec 2020 12:39:31 +0200 Subject: [PATCH 050/129] Prebid Server prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6068445e44e..c05133abc29 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.51.1 + 1.52.0-SNAPSHOT prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - 1.51.1 + HEAD From 783670e351072f92e5bf2039b4bd66195814cef4 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Fri, 11 Dec 2020 12:48:44 +0200 Subject: [PATCH 051/129] Add invalid ccpa notification in debug error response (#787) --- .../server/auction/AmpRequestFactory.java | 3 +- .../server/auction/AuctionRequestFactory.java | 2 +- .../auction/PrivacyEnforcementService.java | 4 +- .../server/privacy/PrivacyExtractor.java | 34 ++++++++----- .../auction/AuctionRequestFactoryTest.java | 4 +- .../PrivacyEnforcementServiceTest.java | 7 +-- .../server/privacy/PrivacyExtractorTest.java | 48 +++++++++++++------ 7 files changed, 65 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/AmpRequestFactory.java index 8aa778542b2..4c8a45ade5a 100644 --- a/src/main/java/org/prebid/server/auction/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AmpRequestFactory.java @@ -249,8 +249,7 @@ private static Integer debugFromQueryStringParam(RoutingContext context) { /** * Extracts parameters from http request and overrides corresponding attributes in {@link BidRequest}. */ - private BidRequest overrideParameters(BidRequest bidRequest, HttpServerRequest request, - List errors) { + private BidRequest overrideParameters(BidRequest bidRequest, HttpServerRequest request, List errors) { final String requestConsentParam = request.getParam(CONSENT_PARAM); final String requestGdprConsentParam = request.getParam(GDPR_CONSENT_PARAM); final String consentString = ObjectUtils.firstNonNull(requestConsentParam, requestGdprConsentParam); diff --git a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java index f4eff501dd3..78a1bc9d835 100644 --- a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java @@ -200,7 +200,7 @@ Future toAuctionContext(RoutingContext routingContext, return accountFrom(bidRequest, timeout, routingContext) .compose(account -> privacyEnforcementService.contextFromBidRequest( - bidRequest, account, requestTypeMetric, timeout) + bidRequest, account, requestTypeMetric, timeout, errors) .map(privacyContext -> AuctionContext.builder() .routingContext(routingContext) .uidsCookie(uidsCookieService.parseFromRequest(routingContext)) diff --git a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java index eb7fbf023c9..a3058136594 100644 --- a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java +++ b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java @@ -82,9 +82,9 @@ public PrivacyEnforcementService(BidderCatalog bidderCatalog, } Future contextFromBidRequest( - BidRequest bidRequest, Account account, MetricName requestType, Timeout timeout) { + BidRequest bidRequest, Account account, MetricName requestType, Timeout timeout, List errors) { - final Privacy privacy = privacyExtractor.validPrivacyFrom(bidRequest); + final Privacy privacy = privacyExtractor.validPrivacyFrom(bidRequest, errors); final Device device = bidRequest.getDevice(); final String ipAddress = device != null ? device.getIp() : null; diff --git a/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java b/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java index cd11b584461..0ccc6b6d385 100644 --- a/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java +++ b/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java @@ -15,6 +15,8 @@ import org.prebid.server.proto.request.CookieSyncRequest; import org.prebid.server.proto.request.PreBidRequest; +import java.util.List; + /** * GDPR-aware utilities */ @@ -40,12 +42,13 @@ public class PrivacyExtractor { *

* And construct {@link Privacy} from them. Use default values in case of invalid value. */ - public Privacy validPrivacyFrom(BidRequest bidRequest) { - return extractPrivacy(bidRequest.getRegs(), bidRequest.getUser()); + public Privacy validPrivacyFrom(BidRequest bidRequest, List errors) { + return extractPrivacy(bidRequest.getRegs(), bidRequest.getUser(), errors); } + @Deprecated public Privacy validPrivacyFrom(PreBidRequest preBidRequest) { - return extractPrivacy(preBidRequest.getRegs(), preBidRequest.getUser()); + return extractPrivacy(preBidRequest.getRegs(), preBidRequest.getUser(), null); } public Privacy validPrivacyFrom(CookieSyncRequest request) { @@ -54,17 +57,17 @@ public Privacy validPrivacyFrom(CookieSyncRequest request) { final String gdprConsent = request.getGdprConsent(); final String usPrivacy = request.getUsPrivacy(); - return toValidPrivacy(gdpr, gdprConsent, usPrivacy, null); + return toValidPrivacy(gdpr, gdprConsent, usPrivacy, null, null); } public Privacy validPrivacyFromSetuidRequest(HttpServerRequest request) { final String gdpr = request.getParam(SETUID_GDPR_PARAM); final String gdprConsent = request.getParam(SETUID_GDPR_CONSENT_PARAM); - return toValidPrivacy(gdpr, gdprConsent, null, null); + return toValidPrivacy(gdpr, gdprConsent, null, null, null); } - private Privacy extractPrivacy(Regs regs, User user) { + private Privacy extractPrivacy(Regs regs, User user, List errors) { final ExtRegs extRegs = regs != null ? regs.getExt() : null; final ExtUser extUser = user != null ? user.getExt() : null; @@ -74,27 +77,34 @@ private Privacy extractPrivacy(Regs regs, User user) { final String usPrivacy = extRegs != null ? extRegs.getUsPrivacy() : null; final Integer coppa = regs != null ? regs.getCoppa() : null; - return toValidPrivacy(gdpr, consent, usPrivacy, coppa); + return toValidPrivacy(gdpr, consent, usPrivacy, coppa, errors); } - private static Privacy toValidPrivacy(String gdpr, String consent, String usPrivacy, Integer coppa) { + private static Privacy toValidPrivacy(String gdpr, + String consent, + String usPrivacy, + Integer coppa, + List errors) { final String validGdpr = ObjectUtils.notEqual(gdpr, "1") && ObjectUtils.notEqual(gdpr, "0") ? DEFAULT_GDPR_VALUE : gdpr; final String validConsent = consent == null ? DEFAULT_CONSENT_VALUE : consent; - final Ccpa validCcpa = usPrivacy == null ? DEFAULT_CCPA_VALUE : toValidCcpa(usPrivacy); + final Ccpa validCcpa = usPrivacy == null ? DEFAULT_CCPA_VALUE : toValidCcpa(usPrivacy, errors); final Integer validCoppa = coppa == null ? DEFAULT_COPPA_VALUE : coppa; return Privacy.of(validGdpr, validConsent, validCcpa, validCoppa); } - private static Ccpa toValidCcpa(String usPrivacy) { + private static Ccpa toValidCcpa(String usPrivacy, List errors) { try { Ccpa.validateUsPrivacy(usPrivacy); return Ccpa.of(usPrivacy); } catch (PreBidException e) { - // TODO add error to PBS response, not only in logs (See PR #758) - logger.debug("CCPA consent {0} has invalid format: {1}", usPrivacy, e.getMessage()); + final String message = String.format("CCPA consent %s has invalid format: %s", usPrivacy, e.getMessage()); + logger.debug(message); + if (errors != null) { + errors.add(message); + } return DEFAULT_CCPA_VALUE; } } diff --git a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java index 2a78f3a8cc7..a9bbdcd00b6 100644 --- a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java @@ -144,7 +144,7 @@ public void setUp() { given(timeoutResolver.resolve(any())).willReturn(2000L); given(timeoutResolver.adjustTimeout(anyLong())).willReturn(1900L); - given(privacyEnforcementService.contextFromBidRequest(any(), any(), any(), any())) + given(privacyEnforcementService.contextFromBidRequest(any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture(PrivacyContext.of( Privacy.of("0", EMPTY, Ccpa.EMPTY, 0), TcfContext.empty()))); @@ -1693,7 +1693,7 @@ public void shouldEnrichRequestWithIpAddressAndCountryAndSaveAuctionContext() { .geoInfo(GeoInfo.builder().vendor("v").country("ua").build()) .build(), "ip"); - given(privacyEnforcementService.contextFromBidRequest(any(), any(), any(), any())) + given(privacyEnforcementService.contextFromBidRequest(any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture(privacyContext)); // when diff --git a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java index f720c3cfa1e..9387dd62ca6 100644 --- a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java +++ b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java @@ -53,6 +53,7 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneId; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -131,7 +132,7 @@ public void contextFromBidRequestShouldReturnCoppaContext() { // when final Future privacyContext = privacyEnforcementService.contextFromBidRequest( - bidRequest, Account.empty("account"), null, null); + bidRequest, Account.empty("account"), null, null, new ArrayList<>()); // then FutureAssertion.assertThat(privacyContext).succeededWith( @@ -165,7 +166,7 @@ public void contextFromBidRequestShouldReturnTcfContext() { // when final Future privacyContext = privacyEnforcementService.contextFromBidRequest( - bidRequest, Account.empty(accountId), requestType, null); + bidRequest, Account.empty(accountId), requestType, null, new ArrayList<>()); // then final Privacy privacy = Privacy.of("1", "consent", Ccpa.of("1YYY"), 0); @@ -207,7 +208,7 @@ public void contextFromBidRequestShouldReturnTcfContextWithIpMasked() { // when final Future privacyContext = privacyEnforcementService.contextFromBidRequest( - bidRequest, Account.empty("account"), MetricName.openrtb2web, null); + bidRequest, Account.empty("account"), MetricName.openrtb2web, null, new ArrayList<>()); // then final Privacy privacy = Privacy.of("1", "consent", Ccpa.of("1YYY"), 0); diff --git a/src/test/java/org/prebid/server/privacy/PrivacyExtractorTest.java b/src/test/java/org/prebid/server/privacy/PrivacyExtractorTest.java index 9fec35b41de..73fbe881bb1 100644 --- a/src/test/java/org/prebid/server/privacy/PrivacyExtractorTest.java +++ b/src/test/java/org/prebid/server/privacy/PrivacyExtractorTest.java @@ -17,6 +17,8 @@ import org.prebid.server.proto.request.CookieSyncRequest; import org.prebid.server.proto.request.PreBidRequest; +import java.util.ArrayList; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -37,7 +39,8 @@ public void setUp() { @Test public void shouldReturnGdprEmptyValueWhenRegsIsNull() { // given and when - final String gdpr = privacyExtractor.validPrivacyFrom(BidRequest.builder().build()).getGdpr(); + final String gdpr = + privacyExtractor.validPrivacyFrom(BidRequest.builder().build(), new ArrayList<>()).getGdpr(); // then assertThat(gdpr).isEmpty(); @@ -47,7 +50,7 @@ public void shouldReturnGdprEmptyValueWhenRegsIsNull() { public void shouldReturnGdprEmptyValueWhenRegsExtIsNull() { // given and when final String gdpr = privacyExtractor.validPrivacyFrom( - BidRequest.builder().regs(Regs.of(null, null)).build()) + BidRequest.builder().regs(Regs.of(null, null)).build(), new ArrayList<>()) .getGdpr(); // then @@ -60,7 +63,8 @@ public void shouldReturnGdprEmptyValueWhenRegsExtGdprIsNoEqualsToOneOrZero() { final Regs regs = Regs.of(null, ExtRegs.of(2, null)); // when - final String gdpr = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getGdpr(); + final String gdpr = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), new ArrayList<>()).getGdpr(); // then assertThat(gdpr).isEmpty(); @@ -72,7 +76,8 @@ public void shouldReturnGdprOneWhenExtRegsContainsGdprOne() { final Regs regs = Regs.of(null, ExtRegs.of(1, null)); // when - final String gdpr = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getGdpr(); + final String gdpr = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), new ArrayList<>()).getGdpr(); // then assertThat(gdpr).isEqualTo("1"); @@ -84,7 +89,8 @@ public void shouldReturnGdprZeroWhenExtRegsContainsGdprZero() { final Regs regs = Regs.of(null, ExtRegs.of(0, null)); // when - final String gdpr = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getGdpr(); + final String gdpr = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), new ArrayList<>()).getGdpr(); // then assertThat(gdpr).isEqualTo("0"); @@ -93,7 +99,8 @@ public void shouldReturnGdprZeroWhenExtRegsContainsGdprZero() { @Test public void shouldReturnConsentEmptyValueWhenExtUserIsNull() { // given and when - final String consent = privacyExtractor.validPrivacyFrom(BidRequest.builder().build()).getConsentString(); + final String consent = privacyExtractor.validPrivacyFrom(BidRequest.builder().build(), new ArrayList<>()) + .getConsentString(); // then assertThat(consent).isEmpty(); @@ -105,8 +112,9 @@ public void shouldReturnConsentEmptyValueWhenUserConsentIsNull() { final User user = User.builder().ext(ExtUser.builder().build()).build(); // when - final String consent = privacyExtractor.validPrivacyFrom(BidRequest.builder().user(user).build()) - .getConsentString(); + final String consent = + privacyExtractor.validPrivacyFrom(BidRequest.builder().user(user).build(), new ArrayList<>()) + .getConsentString(); // then assertThat(consent).isEmpty(); @@ -118,23 +126,28 @@ public void shouldReturnConsentWhenUserContainsConsent() { final User user = User.builder().ext(ExtUser.builder().consent("consent").build()).build(); // when - final String consent = privacyExtractor.validPrivacyFrom(BidRequest.builder().user(user).build()) - .getConsentString(); + final String consent = + privacyExtractor.validPrivacyFrom(BidRequest.builder().user(user).build(), new ArrayList<>()) + .getConsentString(); // then assertThat(consent).isEqualTo("consent"); } @Test - public void shouldReturnDefaultCcpaIfNotValid() { + public void shouldReturnDefaultCcpaWhenNotValidAndAddError() { // given final Regs regs = Regs.of(null, ExtRegs.of(null, "invalid")); + final ArrayList errors = new ArrayList<>(); // when - final Ccpa ccpa = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getCcpa(); + final Ccpa ccpa = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), errors).getCcpa(); // then assertThat(ccpa).isEqualTo(Ccpa.EMPTY); + assertThat(errors).containsOnly( + "CCPA consent invalid has invalid format: us_privacy must contain 4 characters"); } @Test @@ -143,7 +156,9 @@ public void shouldReturnDefaultCoppaIfNull() { final Regs regs = Regs.of(null, null); // when - final Integer coppa = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getCoppa(); + final Integer coppa = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), new ArrayList<>()) + .getCoppa(); // then assertThat(coppa).isZero(); @@ -155,7 +170,9 @@ public void shouldReturnCoppaIfNotNull() { final Regs regs = Regs.of(42, null); // when - final Integer coppa = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getCoppa(); + final Integer coppa = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), new ArrayList<>()) + .getCoppa(); // then assertThat(coppa).isEqualTo(42); @@ -168,7 +185,8 @@ public void shouldReturnPrivacyWithParametersExtractedFromBidRequest() { final User user = User.builder().ext(ExtUser.builder().consent("consent").build()).build(); // when - final Privacy privacy = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).user(user).build()); + final Privacy privacy = privacyExtractor + .validPrivacyFrom(BidRequest.builder().regs(regs).user(user).build(), new ArrayList<>()); // then assertThat(privacy).isEqualTo(Privacy.of("0", "consent", Ccpa.of("1Yn-"), 0)); From 5e7b6c40ccffa87187b49c44c771c2d9595c5860 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Fri, 11 Dec 2020 12:51:21 +0200 Subject: [PATCH 052/129] Add configuration for lmt enforcement (#826) --- docs/config-app.md | 3 + .../auction/PrivacyEnforcementService.java | 9 ++- .../spring/config/ServiceConfiguration.java | 5 +- src/main/resources/application.yaml | 2 + .../PrivacyEnforcementServiceTest.java | 61 ++++++++++++++++--- 5 files changed, 66 insertions(+), 14 deletions(-) diff --git a/docs/config-app.md b/docs/config-app.md index e13eb89ce47..b88d090d892 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -323,6 +323,9 @@ If not defined in config all other Health Checkers would be disabled and endpoin ## CCPA - `ccpa.enforce` - if equals to `true` enforces to check ccpa policy, otherwise ignore ccpa verification. +## LMT +- `lmt.enforce` - if equals to `true` enforces to check lmt policy, otherwise ignore lmt verification. + ## Geo Location - `geolocation.enabled` - if equals to `true` the geo location service will be used to determine the country for client request. - `geolocation.circuit-breaker.enabled` - if equals to `true` circuit breaker will be used to make geo location client more robust. diff --git a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java index a3058136594..82b2b3fd547 100644 --- a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java +++ b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java @@ -65,13 +65,15 @@ public class PrivacyEnforcementService { private final IpAddressHelper ipAddressHelper; private final Metrics metrics; private final boolean ccpaEnforce; + private final boolean lmtEnforce; public PrivacyEnforcementService(BidderCatalog bidderCatalog, PrivacyExtractor privacyExtractor, TcfDefinerService tcfDefinerService, IpAddressHelper ipAddressHelper, Metrics metrics, - boolean ccpaEnforce) { + boolean ccpaEnforce, + boolean lmtEnforce) { this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.privacyExtractor = Objects.requireNonNull(privacyExtractor); @@ -79,6 +81,7 @@ public PrivacyEnforcementService(BidderCatalog bidderCatalog, this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); this.metrics = Objects.requireNonNull(metrics); this.ccpaEnforce = ccpaEnforce; + this.lmtEnforce = lmtEnforce; } Future contextFromBidRequest( @@ -369,7 +372,7 @@ private Map updatePrivacyMetrics( enforcement.isBlockAnalyticsReport()); } - if (isLmtEnabled(device)) { + if (lmtEnforce && isLmtEnabled(device)) { metrics.updatePrivacyLmtMetric(); } @@ -386,7 +389,7 @@ private List getBidderToPrivacyResult( Device device, Map bidderToEnforcement) { - final boolean isLmtEnabled = isLmtEnabled(device); + final boolean isLmtEnabled = lmtEnforce && isLmtEnabled(device); return bidderToUser.entrySet().stream() .filter(entry -> bidders.contains(entry.getKey())) .map(bidderUserEntry -> createBidderPrivacyResult( diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 8a39a5f498c..1e9df6476d7 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -523,10 +523,11 @@ PrivacyEnforcementService privacyEnforcementService( TcfDefinerService tcfDefinerService, IpAddressHelper ipAddressHelper, Metrics metrics, - @Value("${ccpa.enforce}") boolean ccpaEnforce) { + @Value("${ccpa.enforce}") boolean ccpaEnforce, + @Value("${lmt.enforce}") boolean lmtEnforce) { return new PrivacyEnforcementService( - bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, ccpaEnforce); + bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, ccpaEnforce, lmtEnforce); } @Bean diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 1cbc0ec041b..722a769c10f 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -190,6 +190,8 @@ gdpr: purpose-one-treatment-interpretation: ignore ccpa: enforce: true +lmt: + enforce: true geolocation: enabled: false type: maxmind diff --git a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java index 9387dd62ca6..e5db7e2daac 100644 --- a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java +++ b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java @@ -120,7 +120,7 @@ public void setUp() { privacyExtractor = new PrivacyExtractor(); privacyEnforcementService = new PrivacyEnforcementService( - bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, false); + bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, false, false); } @Test @@ -324,7 +324,7 @@ public void contextFromCookieSyncRequestShouldReturnContext() { } @Test - public void shouldMaskForCoppaWhenDeviceLmtIsOneAndRegsCoppaIsOneAndDoesNotCallTcfServices() { + public void shouldMaskForCoppaWhenDeviceLmtIsEnforceAndOneAndRegsCoppaIsOneAndDoesNotCallTcfServices() { // given final User user = notMaskedUser(notMaskedExtUser()); final Device device = givenNotMaskedDevice(deviceBuilder -> deviceBuilder.lmt(1)); @@ -359,7 +359,7 @@ public void shouldMaskForCoppaWhenDeviceLmtIsOneAndRegsCoppaIsOneAndDoesNotCallT public void shouldMaskForCcpaWhenUsPolicyIsValidAndCoppaIsZero() { // given privacyEnforcementService = new PrivacyEnforcementService( - bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true); + bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true, false); given(tcfDefinerService.resultForBidderNames(anySet(), any(), any(), any())) .willReturn(Future.succeededFuture(TcfResponse.of(true, emptyMap(), null))); @@ -493,8 +493,11 @@ public void shouldNotMaskWhenDeviceLmtIsZeroAndCoppaIsZeroAndGdprIsZeroAndTcfDef } @Test - public void shouldMaskForTcfWhenTcfServiceAllowAllAndDeviceLmtIsOne() { + public void shouldMaskForTcfWhenTcfServiceAllowAllAndDeviceLmtIsOneAndLmtIsEnforced() { // given + privacyEnforcementService = new PrivacyEnforcementService( + bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, false, true); + given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any())) .willReturn(Future.succeededFuture( TcfResponse.of(true, singletonMap(BIDDER_NAME, PrivacyEnforcementAction.allowAll()), null))); @@ -529,6 +532,46 @@ public void shouldMaskForTcfWhenTcfServiceAllowAllAndDeviceLmtIsOne() { verify(tcfDefinerService).resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), any(), any()); } + @Test + public void shouldNotMaskForTcfWhenTcfServiceAllowAllAndDeviceLmtIsOneAndLmtIsNotEnforced() { + // given + given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any())) + .willReturn(Future.succeededFuture( + TcfResponse.of(true, singletonMap(BIDDER_NAME, PrivacyEnforcementAction.allowAll()), null))); + + final User user = notMaskedUser(); + final Device device = givenNotMaskedDevice(deviceBuilder -> deviceBuilder.lmt(1)); + final Regs regs = Regs.of(0, null); + final Map bidderToUser = singletonMap(BIDDER_NAME, user); + + final BidRequest bidRequest = givenBidRequest(givenSingleImp( + singletonMap(BIDDER_NAME, 1)), + bidRequestBuilder -> bidRequestBuilder + .user(user) + .device(device) + .regs(regs)); + + final PrivacyContext privacyContext = givenPrivacyContext("0", Ccpa.EMPTY, 0); + + final AuctionContext context = auctionContext(bidRequest, privacyContext); + + // when + final List result = privacyEnforcementService + .mask(context, bidderToUser, singletonList(BIDDER_NAME), BidderAliases.of(null, null, bidderCatalog)) + .result(); + + // then + final BidderPrivacyResult expectedBidderPrivacy = BidderPrivacyResult.builder() + .user(user) + .device(device) + .requestBidder(BIDDER_NAME) + .build(); + assertThat(result).containsOnly(expectedBidderPrivacy); + + verify(tcfDefinerService) + .resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), any(), any()); + } + @Test public void shouldMaskForTcfWhenTcfDefinerServiceRestrictDeviceAndUser() { // given @@ -968,7 +1011,7 @@ public void shouldReturnFailedFutureWhenTcfServiceIsReturnFailedFuture() { public void shouldMaskForCcpaAndTcfWhenUsPolicyIsValidAndGdprIsEnforcedAndCOPPAIsZero() { // given privacyEnforcementService = new PrivacyEnforcementService( - bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true); + bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true, false); final String bidder1Name = "bidder1Name"; final String bidder2Name = "bidder2Name"; @@ -1038,7 +1081,7 @@ public void shouldMaskForCcpaAndTcfWhenUsPolicyIsValidAndGdprIsEnforcedAndCOPPAI public void shouldNotMaskForCcpaWhenCatchAllWildcardIsPresentInNosaleList() { // given privacyEnforcementService = new PrivacyEnforcementService( - bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true); + bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true, false); given(tcfDefinerService.resultForBidderNames(anySet(), any(), any(), any())) .willReturn(Future.succeededFuture( @@ -1092,7 +1135,7 @@ public void isCcpaEnforcedShouldReturnFalseWhenEnforcedPropertyIsFalseInConfigur public void isCcpaEnforcedShouldReturnFalseWhenEnforcedPropertyIsTrueInConfigurationAndFalseInAccount() { // given privacyEnforcementService = new PrivacyEnforcementService( - bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true); + bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true, false); final Ccpa ccpa = Ccpa.of("1YYY"); final Account account = Account.builder().enforceCcpa(false).build(); @@ -1105,7 +1148,7 @@ public void isCcpaEnforcedShouldReturnFalseWhenEnforcedPropertyIsTrueInConfigura public void isCcpaEnforcedShouldReturnFalseWhenEnforcedPropertyIsTrue() { // given privacyEnforcementService = new PrivacyEnforcementService( - bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true); + bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true, false); final Ccpa ccpa = Ccpa.of("1YNY"); final Account account = Account.builder().build(); @@ -1118,7 +1161,7 @@ public void isCcpaEnforcedShouldReturnFalseWhenEnforcedPropertyIsTrue() { public void isCcpaEnforcedShouldReturnTrueWhenEnforcedPropertyIsTrueAndCcpaReturnsTrue() { // given privacyEnforcementService = new PrivacyEnforcementService( - bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true); + bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true, false); final Ccpa ccpa = Ccpa.of("1YYY"); final Account account = Account.builder().build(); From 99e31f252f5700a4e29be7d0bb10a783b7033596 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Fri, 11 Dec 2020 12:52:44 +0200 Subject: [PATCH 053/129] Smaato usersync update (#1064) --- src/main/resources/bidder-config/smaato.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/bidder-config/smaato.yaml b/src/main/resources/bidder-config/smaato.yaml index a91590e7561..1600a328546 100644 --- a/src/main/resources/bidder-config/smaato.yaml +++ b/src/main/resources/bidder-config/smaato.yaml @@ -18,8 +18,8 @@ adapters: supported-vendors: vendor-id: 82 usersync: - url: - redirect-url: + url: https://s.ad.smaato.net/c/?adExInit=n&redir= + redirect-url: /setuid?bidder=smaato&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID cookie-family-name: smaato type: redirect support-cors: false From 205560c3777762c3fa7a3122061c0454bc75baa5 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Fri, 11 Dec 2020 12:53:11 +0200 Subject: [PATCH 054/129] Mobilefuse endpoint update (#1066) --- src/main/resources/bidder-config/mobilefuse.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/bidder-config/mobilefuse.yaml b/src/main/resources/bidder-config/mobilefuse.yaml index 6cefd0b9b6b..2f47ae1796c 100644 --- a/src/main/resources/bidder-config/mobilefuse.yaml +++ b/src/main/resources/bidder-config/mobilefuse.yaml @@ -1,7 +1,7 @@ adapters: mobilefuse: enabled: false - endpoint: http://mfx-us-east.mobilefuse.com/openrtb?pub_id= + endpoint: http://mfx.mobilefuse.com/openrtb?pub_id= pbs-enforces-gdpr: true pbs-enforces-ccpa: true modifying-vast-xml-allowed: true From de4a833c28200e9380cc436be50e68ecbe8fcb0c Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Fri, 11 Dec 2020 13:07:32 +0200 Subject: [PATCH 055/129] Make TCF metrics to reflect real bidding (#798) --- docs/metrics.md | 3 + .../auction/PrivacyEnforcementService.java | 82 +++++-- .../org/prebid/server/metric/Metrics.java | 19 +- .../privacy/gdpr/TcfDefinerService.java | 4 +- .../purpose/PurposeStrategy.java | 2 +- .../PrivacyEnforcementServiceTest.java | 126 ++++++++++- .../org/prebid/server/metric/MetricsTest.java | 210 +++++++++--------- .../privacy/gdpr/TcfDefinerServiceTest.java | 21 ++ 8 files changed, 333 insertions(+), 134 deletions(-) diff --git a/docs/metrics.md b/docs/metrics.md index 13bd4b7ba53..c3b3f2d00a6 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -116,9 +116,12 @@ Following metrics are collected and submitted if account is configured with `det ## Privacy metrics - `privacy.tcf.(missing|invalid)` - number of requests lacking a valid consent string +- `privacy.tcf.(v1,v2).requests` - number of requests by TCF version - `privacy.tcf.(v1,v2).unknown-geo` - number of requests received from unknown geo region with consent string of particular version - `privacy.tcf.(v1,v2).in-geo` - number of requests received from TCF-concerned geo region with consent string of particular version - `privacy.tcf.(v1,v2).out-geo` - number of requests received outside of TCF-concerned geo region with consent string of particular version - `privacy.tcf.(v1,v2).vendorlist.(missing|ok|err|fallback)` - number of processed vendor lists of particular version - `privacy.usp.specified` - number of requests with a valid US Privacy string (CCPA) - `privacy.usp.opt-out` - number of requests that required privacy enforcement according to CCPA rules +- `privacy.lmt` - number of requests that required privacy enforcement according to LMT flag +- `privacy.coppa` - number of requests that required privacy enforcement according to COPPA rules diff --git a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java index 82b2b3fd547..d8136ad81d7 100644 --- a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java +++ b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java @@ -7,6 +7,7 @@ import com.iab.openrtb.request.User; import io.vertx.core.Future; import io.vertx.core.http.HttpServerRequest; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.BooleanUtils; import org.prebid.server.auction.model.AuctionContext; @@ -111,9 +112,10 @@ Future contextFromBidRequest( .map(tcfContext -> PrivacyContext.of(privacy, tcfContext, tcfContext.getIpAddress())); } - public Future contextFromLegacyRequest(PreBidRequestContext preBidRequestContext, Account account) { - final Privacy privacy = privacyExtractor.validPrivacyFrom(preBidRequestContext.getPreBidRequest()); + public Future contextFromLegacyRequest( + PreBidRequestContext preBidRequestContext, Account account) { + final Privacy privacy = privacyExtractor.validPrivacyFrom(preBidRequestContext.getPreBidRequest()); final AccountGdprConfig accountGdpr = account.getGdpr(); final String accountId = account.getId(); final RequestLogInfo requestLogInfo = requestLogInfo(MetricName.legacy, null, accountId); @@ -190,9 +192,9 @@ Future> mask(AuctionContext auctionContext, return getBidderToEnforcementAction(privacyContext.getTcfContext(), biddersToApplyTcf, aliases, account) .map(bidderToEnforcement -> updatePrivacyMetrics( - bidderToEnforcement, aliases, requestType, device)) + bidderToEnforcement, aliases, requestType, bidderToUser, device)) .map(bidderToEnforcement -> getBidderToPrivacyResult( - biddersToApplyTcf, bidderToUser, device, bidderToEnforcement)) + bidderToEnforcement, biddersToApplyTcf, bidderToUser, device)) .map(gdprResult -> merge(ccpaResult, gdprResult)); } @@ -317,7 +319,7 @@ private Future> getBidderToEnforcementActi TcfContext tcfContext, Set bidders, BidderAliases aliases, Account account) { return tcfDefinerService.resultForBidderNames( - new HashSet<>(bidders), VendorIdResolver.of(aliases), tcfContext, account.getGdpr()) + Collections.unmodifiableSet(bidders), VendorIdResolver.of(aliases), tcfContext, account.getGdpr()) .map(tcfResponse -> mapTcfResponseToEachBidder(tcfResponse, bidders)); } @@ -342,7 +344,7 @@ private Set extractCcpaEnforcedBidders(List bidders, BidRequest return ccpaEnforcedBidders; } - private Map mapTcfResponseToEachBidder( + private static Map mapTcfResponseToEachBidder( TcfResponse tcfResponse, Set bidders) { final Map bidderNameToAction = tcfResponse.getActions(); @@ -357,19 +359,37 @@ private Map updatePrivacyMetrics( Map bidderToEnforcement, BidderAliases aliases, MetricName requestType, + Map bidderToUser, Device device) { + // Metrics should represent real picture of the bidding process, so if bidder request is blocked + // by privacy then no reason to increment another metrics, like geo masked, etc. for (final Map.Entry bidderEnforcement : bidderToEnforcement.entrySet()) { - final String bidder = aliases.resolveBidder(bidderEnforcement.getKey()); + final String bidder = bidderEnforcement.getKey(); final PrivacyEnforcementAction enforcement = bidderEnforcement.getValue(); + final boolean requestBlocked = enforcement.isBlockBidderRequest(); + + final User user = bidderToUser.get(bidder); + boolean userIdRemoved = enforcement.isRemoveUserIds(); + if (requestBlocked || (userIdRemoved && !shouldMaskUser(user))) { + userIdRemoved = false; + } + + boolean geoMasked = enforcement.isMaskGeo(); + if (requestBlocked || (geoMasked && !shouldMaskGeo(user, device))) { + geoMasked = false; + } + + final boolean analyticsBlocked = !requestBlocked && enforcement.isBlockAnalyticsReport(); + metrics.updateAuctionTcfMetrics( - bidder, + aliases.resolveBidder(bidder), requestType, - enforcement.isRemoveUserIds(), - enforcement.isMaskGeo(), - enforcement.isBlockBidderRequest(), - enforcement.isBlockAnalyticsReport()); + userIdRemoved, + geoMasked, + analyticsBlocked, + requestBlocked); } if (lmtEnforce && isLmtEnabled(device)) { @@ -379,15 +399,36 @@ private Map updatePrivacyMetrics( return bidderToEnforcement; } + /** + * Returns true if {@link User} has sensitive privacy information that can be masked. + */ + private static boolean shouldMaskUser(User user) { + if (user == null) { + return false; + } + if (user.getId() != null || user.getBuyeruid() != null) { + return true; + } + final ExtUser extUser = user.getExt(); + return extUser != null && (CollectionUtils.isNotEmpty(extUser.getEids()) || extUser.getDigitrust() != null); + } + + /** + * Returns true if {@link User} or {@link Device} has {@link Geo} information that can be masked. + */ + private static boolean shouldMaskGeo(User user, Device device) { + return (user != null && user.getGeo() != null) || (device != null && device.getGeo() != null); + } + /** * Returns {@link Map}<{@link String}, {@link BidderPrivacyResult}>, where bidder name mapped to masked * {@link BidderPrivacyResult}. Masking depends on GDPR and COPPA. */ private List getBidderToPrivacyResult( + Map bidderToEnforcement, Set bidders, Map bidderToUser, - Device device, - Map bidderToEnforcement) { + Device device) { final boolean isLmtEnabled = lmtEnforce && isLmtEnabled(device); return bidderToUser.entrySet().stream() @@ -404,11 +445,12 @@ private List getBidderToPrivacyResult( /** * Returns {@link BidderPrivacyResult} with GDPR masking. */ - private BidderPrivacyResult createBidderPrivacyResult(User user, - Device device, - String bidder, - boolean isLmtEnabled, - Map bidderToEnforcement) { + private BidderPrivacyResult createBidderPrivacyResult( + User user, + Device device, + String bidder, + boolean isLmtEnabled, + Map bidderToEnforcement) { final PrivacyEnforcementAction privacyEnforcementAction = bidderToEnforcement.get(bidder); final boolean blockBidderRequest = privacyEnforcementAction.isBlockBidderRequest(); @@ -510,7 +552,7 @@ private static Float maskGeoCoordinate(Float coordinate) { /** * Returns masked digitrust and eids of user ext. */ - private ExtUser maskUserExt(ExtUser userExt) { + private static ExtUser maskUserExt(ExtUser userExt) { return userExt != null ? nullIfEmpty(userExt.toBuilder().eids(null).digitrust(null).build()) : null; diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 021d745ec4e..bef85c06058 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -310,25 +310,25 @@ public void updateCookieSyncTcfBlockedMetric(String bidder) { public void updateAuctionTcfMetrics(String bidder, MetricName requestType, - boolean useridRemoved, + boolean userIdRemoved, boolean geoMasked, - boolean requestBlocked, - boolean analyticsBlocked) { + boolean analyticsBlocked, + boolean requestBlocked) { final TcfMetrics tcf = forAdapter(resolveMetricsBidderName(bidder)).requestType(requestType).tcf(); - if (useridRemoved) { + if (userIdRemoved) { tcf.incCounter(MetricName.userid_removed); } if (geoMasked) { tcf.incCounter(MetricName.geo_masked); } - if (requestBlocked) { - tcf.incCounter(MetricName.request_blocked); - } if (analyticsBlocked) { tcf.incCounter(MetricName.analytics_blocked); } + if (requestBlocked) { + tcf.incCounter(MetricName.request_blocked); + } } public void updatePrivacyCoppaMetric() { @@ -356,6 +356,11 @@ public void updatePrivacyTcfInvalidMetric() { privacy().tcf().incCounter(MetricName.invalid); } + public void updatePrivacyTcfRequestsMetric(int version) { + final UpdatableMetrics versionMetrics = version == 2 ? privacy().tcf().v2() : privacy().tcf().v1(); + versionMetrics.incCounter(MetricName.requests); + } + public void updatePrivacyTcfGeoMetric(int version, Boolean inEea) { final UpdatableMetrics versionMetrics = version == 2 ? privacy().tcf().v2() : privacy().tcf().v1(); diff --git a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java index 55b4943ce93..05b4dab71a2 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java @@ -398,6 +398,8 @@ private TCString parseConsentString(String consentString, RequestLogInfo request return TCStringEmpty.create(); } + final int version = tcString.getVersion(); + metrics.updatePrivacyTcfRequestsMetric(version); return tcString; } @@ -450,7 +452,7 @@ private static String logMessage(String consent, String type, RequestLogInfo req consent, type, requestLogInfo.getAccountId(), requestLogInfo.getRefUrl(), message); } - public static boolean isConsentValid(TCString consent) { + private static boolean isConsentValid(TCString consent) { return consent != null && !(consent instanceof TCStringEmpty); } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java index be414332f58..71ae7eeb7bf 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java @@ -42,7 +42,7 @@ public PurposeStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, /** * This method represents allowance of permission that purpose should provide after full enforcement - * (can downgrade to basic if GCL failed) despite of host company or account configuration. + * (can downgrade to basic if GVL failed) despite of host company or account configuration. */ public abstract void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction); diff --git a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java index e5db7e2daac..9307cafef01 100644 --- a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java +++ b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java @@ -67,9 +67,11 @@ import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; +import static java.util.function.UnaryOperator.identity; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -646,6 +648,9 @@ public void shouldMaskUserIdsWhenTcfDefinerServiceRestrictUserIds() { assertThat(result).containsOnly(expectedBidderPrivacy); verify(tcfDefinerService).resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), any(), any()); + + verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), eq(true), anyBoolean(), anyBoolean(), + anyBoolean()); } @Test @@ -730,6 +735,9 @@ public void shouldMaskGeoWhenTcfDefinerServiceRestrictGeo() { assertThat(result).containsOnly(expectedBidderPrivacy); verify(tcfDefinerService).resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), any(), any()); + + verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), anyBoolean(), eq(true), anyBoolean(), + anyBoolean()); } @Test @@ -819,6 +827,118 @@ public void shouldMaskDeviceInfoWhenTcfDefinerServiceRestrictDeviceInfo() { verify(tcfDefinerService).resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), any(), any()); } + @Test + public void shouldSendAnalyticsBlockedMetricIfRestrictedByPrivacyEnforcement() { + // given + final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.allowAll(); + privacyEnforcementAction.setBlockAnalyticsReport(true); + + given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any())) + .willReturn(Future.succeededFuture( + TcfResponse.of(true, singletonMap(BIDDER_NAME, privacyEnforcementAction), null))); + + final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap(BIDDER_NAME, 1)), identity()); + final PrivacyContext privacyContext = givenPrivacyContext("0", Ccpa.EMPTY, 0); + final AuctionContext context = auctionContext(bidRequest, privacyContext); + + // when + privacyEnforcementService.mask(context, emptyMap(), singletonList(BIDDER_NAME), aliases); + + // then + verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), anyBoolean(), anyBoolean(), eq(true), + anyBoolean()); + } + + @Test + public void shouldNotSendRelatedMetricsIfBlockBidderRequestEnforcementIsPresent() { + // given + final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.allowAll(); + privacyEnforcementAction.setBlockBidderRequest(true); // has highest priority + privacyEnforcementAction.setRemoveUserIds(true); + privacyEnforcementAction.setMaskGeo(true); + privacyEnforcementAction.setBlockAnalyticsReport(true); + + given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any())) + .willReturn(Future.succeededFuture( + TcfResponse.of(true, singletonMap(BIDDER_NAME, privacyEnforcementAction), null))); + + final ExtUser extUser = notMaskedExtUser(); + final User user = notMaskedUser(extUser); + final Map bidderToUser = singletonMap(BIDDER_NAME, user); + + final BidRequest bidRequest = givenBidRequest(givenSingleImp( + singletonMap(BIDDER_NAME, 1)), + bidRequestBuilder -> bidRequestBuilder + .user(user) + .device(notMaskedDevice())); + final PrivacyContext privacyContext = givenPrivacyContext("0", Ccpa.EMPTY, 0); + + final AuctionContext context = auctionContext(bidRequest, privacyContext); + + // when + privacyEnforcementService.mask(context, bidderToUser, singletonList(BIDDER_NAME), aliases); + + // then + verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), eq(false), eq(false), eq(false), + eq(true)); + } + + @Test + public void shouldNotSendUserIdRemovedMetricIfNoPrivateUserInformation() { + // given + final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.allowAll(); + privacyEnforcementAction.setRemoveUserIds(true); + + given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any())) + .willReturn(Future.succeededFuture( + TcfResponse.of(true, singletonMap(BIDDER_NAME, privacyEnforcementAction), null))); + + final ExtUser extUser = ExtUser.builder().consent("consent").build(); + final User user = User.builder().gender("gender").ext(extUser).build(); + final Map bidderToUser = singletonMap(BIDDER_NAME, user); + + final BidRequest bidRequest = givenBidRequest(givenSingleImp( + singletonMap(BIDDER_NAME, 1)), + bidRequestBuilder -> bidRequestBuilder + .user(user)); + final PrivacyContext privacyContext = givenPrivacyContext("0", Ccpa.EMPTY, 0); + + final AuctionContext context = auctionContext(bidRequest, privacyContext); + + // when + privacyEnforcementService.mask(context, bidderToUser, singletonList(BIDDER_NAME), aliases); + + // then + verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), eq(false), anyBoolean(), anyBoolean(), + anyBoolean()); + } + + @Test + public void shouldNotSendGeoMaskedMetricIfNoPrivateGeoInformation() { + // given + final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.allowAll(); + privacyEnforcementAction.setMaskGeo(true); + + given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any())) + .willReturn(Future.succeededFuture( + TcfResponse.of(true, singletonMap(BIDDER_NAME, privacyEnforcementAction), null))); + + final BidRequest bidRequest = givenBidRequest(givenSingleImp( + singletonMap(BIDDER_NAME, 1)), + bidRequestBuilder -> bidRequestBuilder + .device(Device.builder().model("blackberry").build())); + final PrivacyContext privacyContext = givenPrivacyContext("0", Ccpa.EMPTY, 0); + + final AuctionContext context = auctionContext(bidRequest, privacyContext); + + // when + privacyEnforcementService.mask(context, emptyMap(), singletonList(BIDDER_NAME), aliases); + + // then + verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), eq(false), anyBoolean(), anyBoolean(), + anyBoolean()); + } + @Test public void shouldRerunEmptyResultWhenTcfDefinerServiceRestrictRequest() { // given @@ -846,7 +966,6 @@ public void shouldRerunEmptyResultWhenTcfDefinerServiceRestrictRequest() { .result(); // then - final BidderPrivacyResult expectedBidderPrivacy = BidderPrivacyResult.builder() .requestBidder(BIDDER_NAME) .blockedRequestByTcf(true) @@ -1227,7 +1346,8 @@ public void shouldReturnCorrectMaskedForMultipleBidders() { .user(notMaskedUser()) .device(notMaskedDevice()) .build(); - assertThat(result).hasSize(3).containsOnly(expectedBidder1Masked, expectedBidder2Masked, expectedBidder3Masked); + assertThat(result).hasSize(3) + .containsOnly(expectedBidder1Masked, expectedBidder2Masked, expectedBidder3Masked); final HashSet bidderNames = new HashSet<>(asList(bidder1Name, bidder2Name, bidder3Name)); verify(tcfDefinerService).resultForBidderNames(eq(bidderNames), any(), any(), any()); @@ -1383,7 +1503,7 @@ private static Device givenCoppaMaskedDevice(UnaryOperator } private static List givenSingleImp(T ext) { - return singletonList(givenImp(ext, UnaryOperator.identity())); + return singletonList(givenImp(ext, identity())); } private static Imp givenImp(T ext, UnaryOperator impBuilderCustomizer) { diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index d34a4b9d98e..b0f63bbd6cf 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -82,7 +82,7 @@ public void forAccountShouldReturnAccountMetricsConfiguredWithAccount() { metrics.forAccount(ACCOUNT_ID).incCounter(MetricName.requests); // then - assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isOne(); } @Test @@ -102,7 +102,7 @@ public void forAdapterShouldReturnAdapterMetricsConfiguredWithAdapterType() { metrics.forAdapter(RUBICON).incCounter(MetricName.bids_received); // then - assertThat(metricRegistry.counter("adapter.rubicon.bids_received").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.bids_received").getCount()).isOne(); } @Test @@ -125,7 +125,7 @@ public void shouldReturnAdapterRequestTypeMetricsConfiguredWithAdapterType() { metrics.forAdapter(RUBICON).requestType(MetricName.openrtb2web).incCounter(MetricName.requests); // then - assertThat(metricRegistry.counter("adapter.rubicon.requests.type.openrtb2-web").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.requests.type.openrtb2-web").getCount()).isOne(); } @Test @@ -148,7 +148,7 @@ public void shouldReturnAdapterRequestMetricsConfiguredWithAdapterType() { metrics.forAdapter(RUBICON).request().incCounter(MetricName.gotbids); // then - assertThat(metricRegistry.counter("adapter.rubicon.requests.gotbids").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.requests.gotbids").getCount()).isOne(); } @Test @@ -171,7 +171,7 @@ public void shouldReturnAccountAdapterMetricsConfiguredWithAccountAndAdapterType metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON).incCounter(MetricName.bids_received); // then - assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isOne(); } @Test @@ -195,7 +195,7 @@ public void shouldReturnAccountAdapterRequestMetricsConfiguredWithAccountAndAdap metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON).request().incCounter(MetricName.gotbids); // then - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isOne(); } @Test @@ -218,7 +218,7 @@ public void shouldReturnAccountRequestTypeMetricsConfiguredWithAccount() { metrics.forAccount(ACCOUNT_ID).requestType(MetricName.openrtb2web).incCounter(MetricName.requests); // then - assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isOne(); } @Test @@ -238,7 +238,7 @@ public void userSyncShouldReturnUserSyncMetricsConfiguredWithPrefix() { metrics.userSync().incCounter(MetricName.opt_outs); // then - assertThat(metricRegistry.counter("usersync.opt_outs").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.opt_outs").getCount()).isOne(); } @Test @@ -260,7 +260,7 @@ public void shouldReturnBidderUserSyncMetricsConfiguredWithBidder() { metrics.userSync().forBidder(RUBICON).incCounter(MetricName.sets); // then - assertThat(metricRegistry.counter("usersync.rubicon.sets").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.rubicon.sets").getCount()).isOne(); } @Test @@ -280,7 +280,7 @@ public void cookieSyncShouldReturnCookieSyncMetricsConfiguredWithPrefix() { metrics.cookieSync().incCounter(MetricName.gen); // then - assertThat(metricRegistry.counter("cookie_sync.gen").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("cookie_sync.gen").getCount()).isOne(); } @Test @@ -302,7 +302,7 @@ public void shouldReturnBidderCookieSyncMetricsConfiguredWithBidder() { metrics.cookieSync().forBidder(RUBICON).incCounter(MetricName.gen); // then - assertThat(metricRegistry.counter("cookie_sync.rubicon.gen").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("cookie_sync.rubicon.gen").getCount()).isOne(); } @Test @@ -324,7 +324,7 @@ public void forRequestTypeShouldReturnRequestStatusMetricsConfiguredWithRequestT metrics.forRequestType(MetricName.openrtb2web).incCounter(MetricName.ok); // then - assertThat(metricRegistry.counter("requests.ok.openrtb2-web").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("requests.ok.openrtb2-web").getCount()).isOne(); } @Test @@ -348,7 +348,7 @@ public void updateSafariRequestsMetricShouldIncrementMetric() { metrics.updateSafariRequestsMetric(false); // then - assertThat(metricRegistry.counter("safari_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("safari_requests").getCount()).isOne(); } @Test @@ -360,9 +360,9 @@ public void updateAppAndNoCookieAndImpsRequestedMetricsShouldIncrementMetrics() metrics.updateAppAndNoCookieAndImpsRequestedMetrics(false, true, false, 1); // then - assertThat(metricRegistry.counter("app_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("app_requests").getCount()).isOne(); assertThat(metricRegistry.counter("no_cookie_requests").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("safari_no_cookie_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("safari_no_cookie_requests").getCount()).isOne(); assertThat(metricRegistry.counter("imps_requested").getCount()).isEqualTo(5); } @@ -382,7 +382,7 @@ public void updateImpTypesMetricsByCountPerMediaTypeShouldIncrementMetrics() { // then assertThat(metricRegistry.counter("imps_banner").getCount()).isEqualTo(3); assertThat(metricRegistry.counter("imps_video").getCount()).isEqualTo(5); - assertThat(metricRegistry.counter("imps_native").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("imps_native").getCount()).isOne(); assertThat(metricRegistry.counter("imps_audio").getCount()).isEqualTo(4); } @@ -409,9 +409,9 @@ public void updateImpTypesMetricsByImpsShouldGroupCountByMediaTypeAndCallOverloa verify(metricsSpy).updateImpTypesMetrics(eq(expectedMap)); - assertThat(metricRegistry.counter("imps_banner").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("imps_banner").getCount()).isOne(); assertThat(metricRegistry.counter("imps_video").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("imps_native").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("imps_native").getCount()).isOne(); assertThat(metricRegistry.counter("imps_audio").getCount()).isEqualTo(2); } @@ -421,7 +421,7 @@ public void updateRequestTimeMetricShouldUpdateMetric() { metrics.updateRequestTimeMetric(456L); // then - assertThat(metricRegistry.timer("request_time").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("request_time").getCount()).isOne(); } @Test @@ -435,12 +435,12 @@ public void updateRequestTypeMetricShouldIncrementMetric() { metrics.updateRequestTypeMetric(MetricName.amp, MetricName.networkerr); // then - assertThat(metricRegistry.counter("requests.ok.openrtb2-web").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("requests.blacklisted_account.openrtb2-web").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("requests.blacklisted_app.openrtb2-app").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("requests.err.openrtb2-app").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("requests.badinput.amp").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("requests.networkerr.amp").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("requests.ok.openrtb2-web").getCount()).isOne(); + assertThat(metricRegistry.counter("requests.blacklisted_account.openrtb2-web").getCount()).isOne(); + assertThat(metricRegistry.counter("requests.blacklisted_app.openrtb2-app").getCount()).isOne(); + assertThat(metricRegistry.counter("requests.err.openrtb2-app").getCount()).isOne(); + assertThat(metricRegistry.counter("requests.badinput.amp").getCount()).isOne(); + assertThat(metricRegistry.counter("requests.networkerr.amp").getCount()).isOne(); } @Test @@ -458,8 +458,8 @@ public void updateAccountRequestMetricsShouldIncrementMetrics() { metrics.updateAccountRequestMetrics(ACCOUNT_ID, MetricName.openrtb2web); // then - assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isOne(); } @Test @@ -475,10 +475,10 @@ public void updateAdapterRequestTypeAndNoCookieMetricsShouldUpdateMetricsAsExpec // then assertThat(metricRegistry.counter("adapter.rubicon.requests.type.openrtb2-app").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.no_cookie_requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.requests.type.amp").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.type.openrtb2-app").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.type.amp").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.no_cookie_requests").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.rubicon.requests.type.amp").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.type.openrtb2-app").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.type.amp").getCount()).isOne(); } @Test @@ -492,8 +492,8 @@ public void updateAdapterResponseTimeShouldUpdateMetrics() { metrics.updateAdapterResponseTime(INVALID_BIDDER, ACCOUNT_ID, 500); // then - assertThat(metricRegistry.timer("adapter.rubicon.request_time").getCount()).isEqualTo(1); - assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("adapter.rubicon.request_time").getCount()).isOne(); + assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isOne(); assertThat(metricRegistry.timer("adapter.UNKNOWN.request_time").getCount()).isEqualTo(2); assertThat(metricRegistry.timer("account.accountId.UNKNOWN.request_time").getCount()).isEqualTo(2); } @@ -509,8 +509,8 @@ public void updateAdapterRequestNobidMetricsShouldIncrementMetrics() { metrics.updateAdapterRequestNobidMetrics(INVALID_BIDDER, ACCOUNT_ID); // then - assertThat(metricRegistry.counter("adapter.rubicon.requests.nobid").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.requests.nobid").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isOne(); assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.nobid").getCount()).isEqualTo(2); assertThat(metricRegistry.counter("account.accountId.UNKNOWN.requests.nobid").getCount()).isEqualTo(2); } @@ -526,8 +526,8 @@ public void updateAdapterRequestGotbidsMetricsShouldIncrementMetrics() { metrics.updateAdapterRequestGotbidsMetrics(INVALID_BIDDER, ACCOUNT_ID); // then - assertThat(metricRegistry.counter("adapter.rubicon.requests.gotbids").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.requests.gotbids").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isOne(); assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.gotbids").getCount()).isEqualTo(2); assertThat(metricRegistry.counter("account.accountId.UNKNOWN.requests.gotbids").getCount()).isEqualTo(2); } @@ -548,8 +548,8 @@ public void updateAdapterBidMetricsShouldUpdateMetrics() { assertThat(metricRegistry.histogram("account.accountId.rubicon.prices").getCount()).isEqualTo(2); assertThat(metricRegistry.counter("adapter.rubicon.bids_received").getCount()).isEqualTo(2); assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("adapter.rubicon.banner.adm_bids_received").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.video.nurl_bids_received").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.banner.adm_bids_received").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.rubicon.video.nurl_bids_received").getCount()).isOne(); assertThat(metricRegistry.histogram("adapter.UNKNOWN.prices").getCount()).isEqualTo(2); assertThat(metricRegistry.histogram("account.accountId.UNKNOWN.prices").getCount()).isEqualTo(2); assertThat(metricRegistry.counter("adapter.UNKNOWN.bids_received").getCount()).isEqualTo(2); @@ -568,7 +568,7 @@ public void updateAdapterRequestErrorMetricShouldIncrementMetrics() { metrics.updateAdapterRequestErrorMetric(INVALID_BIDDER, MetricName.badinput); // then - assertThat(metricRegistry.counter("adapter.rubicon.requests.badinput").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.requests.badinput").getCount()).isOne(); assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.badinput").getCount()).isEqualTo(2); } @@ -578,7 +578,7 @@ public void updateCookieSyncRequestMetricShouldIncrementMetric() { metrics.updateCookieSyncRequestMetric(); // then - assertThat(metricRegistry.counter("cookie_sync_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("cookie_sync_requests").getCount()).isOne(); } @Test @@ -587,7 +587,7 @@ public void updateUserSyncOptoutMetricShouldIncrementMetric() { metrics.updateUserSyncOptoutMetric(); // then - assertThat(metricRegistry.counter("usersync.opt_outs").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.opt_outs").getCount()).isOne(); } @Test @@ -596,7 +596,7 @@ public void updateUserSyncBadRequestMetricShouldIncrementMetric() { metrics.updateUserSyncBadRequestMetric(); // then - assertThat(metricRegistry.counter("usersync.bad_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.bad_requests").getCount()).isOne(); } @Test @@ -605,7 +605,7 @@ public void updateUserSyncSetsMetricShouldIncrementMetric() { metrics.updateUserSyncSetsMetric(RUBICON); // then - assertThat(metricRegistry.counter("usersync.rubicon.sets").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.rubicon.sets").getCount()).isOne(); } @Test @@ -614,7 +614,7 @@ public void updateUserSyncTcfBlockedMetricShouldIncrementMetric() { metrics.updateUserSyncTcfBlockedMetric(RUBICON); // then - assertThat(metricRegistry.counter("usersync.rubicon.tcf.blocked").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.rubicon.tcf.blocked").getCount()).isOne(); } @Test @@ -628,7 +628,7 @@ public void updateCookieSyncTcfBlockedMetricShouldIncrementMetric() { metrics.updateCookieSyncTcfBlockedMetric(INVALID_BIDDER); // then - assertThat(metricRegistry.counter("cookie_sync.rubicon.tcf.blocked").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("cookie_sync.rubicon.tcf.blocked").getCount()).isOne(); assertThat(metricRegistry.counter("cookie_sync.UNKNOWN.tcf.blocked").getCount()).isEqualTo(2); } @@ -638,7 +638,7 @@ public void updateCookieSyncGenMetricShouldIncrementMetric() { metrics.updateCookieSyncGenMetric(RUBICON); // then - assertThat(metricRegistry.counter("cookie_sync.rubicon.gen").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("cookie_sync.rubicon.gen").getCount()).isOne(); } @Test @@ -647,30 +647,27 @@ public void updateCookieSyncMatchesMetricShouldIncrementMetric() { metrics.updateCookieSyncMatchesMetric(RUBICON); // then - assertThat(metricRegistry.counter("cookie_sync.rubicon.matches").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("cookie_sync.rubicon.matches").getCount()).isOne(); } @Test - public void updateAuctionTcfMetricShouldIncrementMetrics() { + public void updateAuctionTcfMetricsShouldIncrementMetrics() { // given given(bidderCatalog.isValidName(INVALID_BIDDER)).willReturn(false); - // when metrics.updateAuctionTcfMetrics(RUBICON, MetricName.openrtb2web, true, true, true, true); - metrics.updateAuctionTcfMetrics(INVALID_BIDDER, MetricName.openrtb2web, false, true, false, true); - metrics.updateAuctionTcfMetrics(INVALID_BIDDER, MetricName.openrtb2app, true, false, true, false); + metrics.updateAuctionTcfMetrics(INVALID_BIDDER, MetricName.openrtb2web, false, true, true, false); + metrics.updateAuctionTcfMetrics(INVALID_BIDDER, MetricName.openrtb2app, true, false, false, true); // then - assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.userid_removed").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.geo_masked").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.request_blocked").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.analytics_blocked").getCount()) - .isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-web.tcf.geo_masked").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-web.tcf.analytics_blocked").getCount()) - .isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-app.tcf.userid_removed").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-app.tcf.request_blocked").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.userid_removed").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.geo_masked").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.analytics_blocked").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.request_blocked").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-web.tcf.geo_masked").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-web.tcf.analytics_blocked").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-app.tcf.userid_removed").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-app.tcf.request_blocked").getCount()).isOne(); } @Test @@ -699,7 +696,7 @@ public void updatePrivacyCoppaMetricShouldIncrementMetric() { metrics.updatePrivacyCoppaMetric(); // then - assertThat(metricRegistry.counter("privacy.coppa").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.coppa").getCount()).isOne(); } @Test @@ -708,7 +705,7 @@ public void updatePrivacyLmtMetricShouldIncrementMetric() { metrics.updatePrivacyLmtMetric(); // then - assertThat(metricRegistry.counter("privacy.lmt").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.lmt").getCount()).isOne(); } @Test @@ -717,8 +714,8 @@ public void updatePrivacyCcpaMetricsShouldIncrementMetrics() { metrics.updatePrivacyCcpaMetrics(true, true); // then - assertThat(metricRegistry.counter("privacy.usp.specified").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("privacy.usp.opt-out").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.usp.specified").getCount()).isOne(); + assertThat(metricRegistry.counter("privacy.usp.opt-out").getCount()).isOne(); } @Test @@ -727,7 +724,7 @@ public void updatePrivacyTcfMissingMetricShouldIncrementMetric() { metrics.updatePrivacyTcfMissingMetric(); // then - assertThat(metricRegistry.counter("privacy.tcf.missing").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.missing").getCount()).isOne(); } @Test @@ -736,7 +733,16 @@ public void updatePrivacyTcfInvalidMetricShouldIncrementMetric() { metrics.updatePrivacyTcfInvalidMetric(); // then - assertThat(metricRegistry.counter("privacy.tcf.invalid").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.invalid").getCount()).isOne(); + } + + @Test + public void updatePrivacyTcfRequestsMetricShouldIncrementMetric() { + // when + metrics.updatePrivacyTcfRequestsMetric(1); + + // then + assertThat(metricRegistry.counter("privacy.tcf.v1.requests").getCount()).isOne(); } @Test @@ -747,9 +753,9 @@ public void updatePrivacyTcfGeoMetricShouldIncrementMetrics() { metrics.updatePrivacyTcfGeoMetric(2, false); // then - assertThat(metricRegistry.counter("privacy.tcf.v1.unknown-geo").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("privacy.tcf.v2.in-geo").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("privacy.tcf.v2.out-geo").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.v1.unknown-geo").getCount()).isOne(); + assertThat(metricRegistry.counter("privacy.tcf.v2.in-geo").getCount()).isOne(); + assertThat(metricRegistry.counter("privacy.tcf.v2.out-geo").getCount()).isOne(); } @Test @@ -758,7 +764,7 @@ public void updatePrivacyTcfVendorListMissingMetricShouldIncrementMetric() { metrics.updatePrivacyTcfVendorListMissingMetric(1); // then - assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.missing").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.missing").getCount()).isOne(); } @Test @@ -767,7 +773,7 @@ public void updatePrivacyTcfVendorListOkMetricShouldIncrementMetric() { metrics.updatePrivacyTcfVendorListOkMetric(1); // then - assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.ok").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.ok").getCount()).isOne(); } @Test @@ -776,7 +782,7 @@ public void updatePrivacyTcfVendorListErrorMetricShouldIncrementMetric() { metrics.updatePrivacyTcfVendorListErrorMetric(1); // then - assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.err").getCount()).isOne(); } @Test @@ -801,13 +807,13 @@ public void shouldNotUpdateAccountMetricsIfVerbosityIsNone() { metrics.updateAdapterBidMetrics(RUBICON, ACCOUNT_ID, 1234L, true, "banner"); // then - assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isEqualTo(0); - assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isEqualTo(0); - assertThat(metricRegistry.histogram("account.accountId.rubicon.prices").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isEqualTo(0); + assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isZero(); + assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isZero(); + assertThat(metricRegistry.histogram("account.accountId.rubicon.prices").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isZero(); } @Test @@ -823,13 +829,13 @@ public void shouldUpdateAccountRequestsMetricOnlyIfVerbosityIsBasic() { metrics.updateAdapterBidMetrics(RUBICON, ACCOUNT_ID, 1234L, true, "banner"); // then - assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isEqualTo(0); - assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isEqualTo(0); - assertThat(metricRegistry.histogram("account.accountId.rubicon.prices").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isEqualTo(0); + assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isZero(); + assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isZero(); + assertThat(metricRegistry.histogram("account.accountId.rubicon.prices").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isZero(); } @Test @@ -838,7 +844,7 @@ public void shouldIncrementConnectionAcceptErrorsMetric() { metrics.updateConnectionAcceptErrors(); // then - assertThat(metricRegistry.counter("connection_accept_errors").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("connection_accept_errors").getCount()).isOne(); } @Test @@ -847,7 +853,7 @@ public void shouldUpdateDatabaseQueryTimeMetric() { metrics.updateDatabaseQueryTimeMetric(456L); // then - assertThat(metricRegistry.timer("db_query_time").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("db_query_time").getCount()).isOne(); } @Test @@ -893,8 +899,8 @@ public void shouldIncrementBothGeoLocationRequestsAndSuccessfulMetrics() { metrics.updateGeoLocationMetric(true); // then - assertThat(metricRegistry.counter("geolocation_requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("geolocation_successful").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("geolocation_requests").getCount()).isOne(); + assertThat(metricRegistry.counter("geolocation_successful").getCount()).isOne(); } @Test @@ -903,8 +909,8 @@ public void shouldIncrementBothGeoLocationRequestsAndFailMetrics() { metrics.updateGeoLocationMetric(false); // then - assertThat(metricRegistry.counter("geolocation_requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("geolocation_fail").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("geolocation_requests").getCount()).isOne(); + assertThat(metricRegistry.counter("geolocation_fail").getCount()).isOne(); } @Test @@ -915,9 +921,9 @@ public void shouldAlwaysIncrementGeoLocationRequestsMetricAndEitherSuccessfulOrF metrics.updateGeoLocationMetric(true); // then - assertThat(metricRegistry.counter("geolocation_requests").getCount()).isEqualTo(3); - assertThat(metricRegistry.counter("geolocation_fail").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("geolocation_fail").getCount()).isOne(); assertThat(metricRegistry.counter("geolocation_successful").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("geolocation_requests").getCount()).isEqualTo(3); } @Test @@ -926,7 +932,7 @@ public void shouldIncrementStoredRequestFoundMetric() { metrics.updateStoredRequestMetric(true); // then - assertThat(metricRegistry.counter("stored_requests_found").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("stored_requests_found").getCount()).isOne(); } @Test @@ -935,7 +941,7 @@ public void shouldIncrementStoredRequestMissingMetric() { metrics.updateStoredRequestMetric(false); // then - assertThat(metricRegistry.counter("stored_requests_missing").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("stored_requests_missing").getCount()).isOne(); } @Test @@ -944,7 +950,7 @@ public void shouldIncrementStoredImpFoundMetric() { metrics.updateStoredImpsMetric(true); // then - assertThat(metricRegistry.counter("stored_imps_found").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("stored_imps_found").getCount()).isOne(); } @Test @@ -953,7 +959,7 @@ public void shouldIncrementStoredImpMissingMetric() { metrics.updateStoredImpsMetric(false); // then - assertThat(metricRegistry.counter("stored_imps_missing").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("stored_imps_missing").getCount()).isOne(); } @Test @@ -963,7 +969,7 @@ public void shouldIncrementPrebidCacheRequestSuccessTimer() { // then assertThat(metricRegistry.timer("prebid_cache.requests.ok").getCount()).isEqualTo(1); - assertThat(metricRegistry.timer("account.accountId.prebid_cache.requests.ok").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("account.accountId.prebid_cache.requests.ok").getCount()).isOne(); } @Test @@ -973,7 +979,7 @@ public void shouldIncrementPrebidCacheRequestFailedTimer() { // then assertThat(metricRegistry.timer("prebid_cache.requests.err").getCount()).isEqualTo(1); - assertThat(metricRegistry.timer("account.accountId.prebid_cache.requests.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("account.accountId.prebid_cache.requests.err").getCount()).isOne(); } @Test diff --git a/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java b/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java index 5b3721655c2..5a01db2a4d5 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java @@ -38,11 +38,13 @@ import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.prebid.server.assertion.FutureAssertion.assertThat; @@ -237,6 +239,8 @@ public void resolveTcfContextShouldConsiderPresenceOfConsentStringAsInScope() { assertThat(result.result().getConsent()).isNotNull(); verifyZeroInteractions(geoLocationService); + verify(metrics).updatePrivacyTcfRequestsMetric(1); + verify(metrics).updatePrivacyTcfGeoMetric(1, null); } @Test @@ -382,6 +386,23 @@ public void resolveTcfContextShouldIncrementInvalidConsentStringMetric() { verify(metrics).updatePrivacyTcfInvalidMetric(); } + @Test + public void resultForVendorIdsShouldNotSetTcfRequestsAndTcfGeoMetricsWhenConsentIsNotValid() { + // given + given(tcf2Service.permissionsFor(any(), any())).willReturn(Future.succeededFuture()); + + // when + tcfDefinerService.resultForVendorIds(singleton(1), TcfContext.builder() + .gdpr("1") + .consent(TCStringEmpty.create()) + .ipAddress("ip") + .build()); + + // then + verify(metrics, never()).updatePrivacyTcfRequestsMetric(anyInt()); + verify(metrics, never()).updatePrivacyTcfGeoMetric(anyInt(), any()); + } + @Test public void resultForVendorIdsShouldAllowAllWhenGdprIsZero() { // when From 4d42c2c29b860a2c2f89fb5a5960deb57a3866c8 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Fri, 11 Dec 2020 13:35:11 +0200 Subject: [PATCH 056/129] Remove DigiTrust support (#911) --- docs/application-settings.md | 2 +- docs/endpoints/openrtb2/auction.md | 7 +-- .../server/auction/AuctionRequestFactory.java | 33 +------------- .../auction/PrivacyEnforcementService.java | 6 +-- .../server/bidder/adform/AdformAdapter.java | 10 ++--- .../server/bidder/adform/AdformBidder.java | 7 ++- .../server/bidder/adform/AdformHttpUtil.java | 24 +--------- .../bidder/adform/AdformRequestUtil.java | 19 -------- .../bidder/adform/model/AdformDigitrust.java | 17 ------- .../adform/model/AdformDigitrustPrivacy.java | 11 ----- .../prebid/server/bidder/dmx/DmxBidder.java | 5 +-- .../server/bidder/rubicon/RubiconAdapter.java | 7 +-- .../server/bidder/rubicon/RubiconBidder.java | 2 +- .../gdpr/model/PrivacyEnforcementAction.java | 2 +- .../proto/openrtb/ext/request/ExtUser.java | 8 ---- .../openrtb/ext/request/ExtUserDigiTrust.java | 28 ------------ .../server/validation/RequestValidator.java | 6 --- .../auction/AuctionRequestFactoryTest.java | 23 ---------- .../server/auction/ExchangeServiceTest.java | 11 ++--- .../PrivacyEnforcementServiceTest.java | 4 -- .../bidder/adform/AdformAdapterTest.java | 6 +-- .../bidder/adform/AdformBidderTest.java | 13 ++---- .../bidder/adform/AdformHttpUtilTest.java | 44 ++++--------------- .../bidder/adform/AdformRequestUtilTest.java | 43 ------------------ .../bidder/rubicon/RubiconBidderTest.java | 29 +----------- .../java/org/prebid/server/it/AdformTest.java | 7 +-- .../org/prebid/server/it/TelariaTest.java | 2 +- .../validation/RequestValidatorTest.java | 24 ---------- .../adform/test-auction-adform-request.json | 7 +-- .../test-auction-conversant-request.json | 7 +-- .../test-auction-conversant-response.json | 2 +- .../test-conversant-bid-request-1.json | 7 +-- .../test-auction-districtm-request.json | 7 +-- .../test-auction-districtm-response.json | 2 +- .../test-districtm-bid-request-1.json | 7 +-- .../auction/ix/test-auction-ix-request.json | 7 +-- .../auction/ix/test-auction-ix-response.json | 2 +- .../it/auction/ix/test-ix-bid-request-1.json | 7 +-- .../test-auction-lifestreet-request.json | 7 +-- .../test-auction-lifestreet-response.json | 2 +- .../test-lifestreet-bid-request-1.json | 7 +-- .../test-auction-pubmatic-request.json | 7 +-- .../test-auction-pubmatic-response.json | 2 +- .../pubmatic/test-pubmatic-bid-request-1.json | 7 +-- .../test-auction-pulsepoint-request.json | 7 +-- .../test-auction-pulsepoint-response.json | 2 +- .../test-pulsepoint-bid-request-1.json | 7 +-- .../test-appnexus-bid-request-1.json | 7 +-- ...test-auction-rubicon-appnexus-request.json | 7 +-- ...est-auction-rubicon-appnexus-response.json | 2 +- .../sovrn/test-auction-sovrn-request.json | 7 +-- .../sovrn/test-auction-sovrn-response.json | 2 +- .../sovrn/test-sovrn-bid-request-1.json | 7 +-- .../adform/test-auction-adform-request.json | 5 --- .../test-auction-adgeneration-request.json | 7 +-- .../adhese/test-auction-adhese-request.json | 7 +-- .../adkernel/test-adkernel-bid-request.json | 7 +-- .../test-auction-adkernel-request.json | 7 +-- .../test-adkerneladn-bid-request-1.json | 7 +-- .../test-adkerneladn-bid-request-2.json | 7 +-- .../test-auction-adkerneladn-request.json | 7 +-- .../admixer/test-admixer-bid-request.json | 7 +-- .../admixer/test-auction-admixer-request.json | 7 +-- .../adoppler/test-adoppler-bid-request-1.json | 7 +-- .../test-auction-adoppler-request.json | 7 +-- .../adpone/test-adpone-bid-request.json | 7 +-- .../adpone/test-auction-adpone-request.json | 7 +-- .../test-adtelligent-bid-request-1.json | 7 +-- .../test-auction-adtelligent-request.json | 7 +-- .../test-advangelists-bid-request.json | 7 +-- .../test-auction-advangelists-request.json | 7 +-- .../applogy/test-applogy-bid-request-1.json | 7 +-- .../applogy/test-applogy-bid-request-2.json | 7 +-- .../applogy/test-auction-applogy-request.json | 7 +-- .../test-auction-beachfront-request.json | 7 +-- .../test-beachfront-bid-request-1.json | 7 +-- .../test-beachfront-bid-request-2.json | 7 +-- .../test-auction-brightroll-request.json | 7 +-- .../test-brightroll-bid-request-1.json | 5 --- .../test-auction-consumable-request.json | 7 +-- .../test-auction-conversant-request.json | 7 +-- .../alias/test-conversant-bid-request.json | 7 +-- .../test-auction-conversant-request.json | 7 +-- .../test-conversant-bid-request.json | 5 --- .../cpmstar/test-auction-cpmstar-request.json | 7 +-- .../cpmstar/test-cpmstar-bid-request-1.json | 7 +-- .../test-auction-datablocks-request.json | 7 +-- .../test-auction-datablocks-response.json | 11 ++--- .../test-datablocks-bid-request-1.json | 7 +-- .../test-datablocks-bid-request-2.json | 7 +-- .../dmx/test-auction-dmx-request.json | 18 +++++--- .../it/openrtb2/dmx/test-dmx-bid-request.json | 17 ++++--- .../test-auction-emxdigital-request.json | 5 --- .../test-auction-emxdigital-response.json | 7 +-- .../test-auction-engagebdr-request.json | 7 +-- .../test-engagebdr-bid-request-1.json | 7 +-- .../test-engagebdr-bid-request-2.json | 7 +-- .../test-auction-eplanning-request.json | 7 +-- .../test-auction-facebook-request.json | 7 +-- .../facebook/test-facebook-bid-request-1.json | 7 +-- .../facebook/test-facebook-bid-request-2.json | 7 +-- .../facebook/test-facebook-bid-request-3.json | 5 --- .../gamma/test-auction-gamma-request.json | 5 --- .../gamoshi/test-auction-gamoshi-request.json | 7 +-- .../gamoshi/test-gamoshi-bid-request-1.json | 5 --- .../grid/test-auction-grid-request.json | 7 +-- .../grid/test-grid-bid-request-1.json | 5 --- .../gumgum/test-auction-gumgum-request.json | 7 +-- .../gumgum/test-gumgum-bid-request-1.json | 7 +-- .../test-auction-improvedigital-request.json | 7 +-- .../test-improvedigital-bid-request-1.json | 5 --- .../openrtb2/ix/test-auction-ix-request.json | 7 +-- .../it/openrtb2/ix/test-ix-bid-request-1.json | 5 --- .../it/openrtb2/ix/test-ix-bid-request-2.json | 5 --- .../kidoz/test-auction-kidoz-request.json | 7 +-- .../kidoz/test-kidoz-bid-request-1.json | 7 +-- .../kidoz/test-kidoz-bid-request-2.json | 7 +-- .../kubient/test-auction-kubient-request.json | 7 +-- .../test-auction-kubient-response.json | 7 +-- .../kubient/test-kubient-bid-request-1.json | 5 --- .../test-auction-lifestreet-request.json | 7 +-- .../test-lifestreet-bid-request-1.json | 5 --- .../test-lifestreet-bid-request-2.json | 5 --- .../test-auction-lockerdome-request.json | 7 +-- .../test-lockerdome-bid-request.json | 5 --- .../test-auction-marsmedia-request.json | 7 +-- .../test-marsmedia-bid-request-1.json | 5 --- .../mgid/test-auction-mgid-request.json | 7 +-- .../openrtb2/mgid/test-mgid-bid-request.json | 7 +-- .../test-auction-nanointeractive-request.json | 7 +-- .../test-nanointeractive-bid-request-1.json | 7 +-- .../openx/test-auction-openx-request.json | 7 +-- .../openx/test-openx-bid-request-1.json | 5 --- .../openx/test-openx-bid-request-2.json | 5 --- .../openx/test-openx-bid-request-3.json | 5 --- .../test-auction-pubmatic-request.json | 7 +-- .../pubmatic/test-pubmatic-bid-request-1.json | 5 --- .../test-auction-pubnative-request.json | 7 +-- .../test-pubnative-bid-request-1.json | 7 +-- .../test-pubnative-bid-request-2.json | 7 +-- .../test-pubnative-bid-request-3.json | 7 +-- .../test-auction-pulsepoint-request.json | 7 +-- .../test-pulsepoint-bid-request-1.json | 5 --- .../test-auction-rhythmone-request.json | 7 +-- .../test-rhythmone-bid-request-1.json | 5 --- .../test-auction-rtbhouse-request.json | 7 +-- .../rtbhouse/test-rtbhouse-bid-request-1.json | 7 +-- ...test-auction-rubicon-appnexus-request.json | 4 -- ...est-auction-rubicon-appnexus-response.json | 5 --- .../test-auction-sharethrough-request.json | 5 --- .../test-auction-sharethrough-response.json | 5 --- .../test-auction-smartrtb-request.json | 7 +-- .../smartrtb/test-smartrtb-bid-request.json | 7 +-- .../test-auction-somoaudience-request.json | 7 +-- .../test-somoaudience-bid-request-1.json | 5 --- .../test-somoaudience-bid-request-2.json | 5 --- .../test-somoaudience-bid-request-3.json | 5 --- .../sonobi/test-auction-sonobi-request.json | 7 +-- .../sonobi/test-sonobi-bid-request-1.json | 7 +-- .../sonobi/test-sonobi-bid-request-2.json | 7 +-- .../sovrn/test-auction-sovrn-request.json | 7 +-- .../sovrn/test-sovrn-bid-request-1.json | 7 +-- .../test-auction-synacormedia-request.json | 7 +-- .../test-synacormedia-bid-request.json | 7 +-- .../tappx/test-auction-tappx-request.json | 5 --- .../tappx/test-tappx-bid-request.json | 5 --- .../telaria/test-auction-telaria-request.json | 7 +-- .../telaria/test-telaria-bid-request-1.json | 7 +-- .../test-auction-triplelift-request.json | 5 --- .../test-triplelift-bid-request.json | 5 --- ...est-auction-triplelift-native-request.json | 5 --- ...st-auction-triplelift-native-response.json | 7 +-- .../test-triplelift-native-bid-request.json | 5 --- .../ttx/test-auction-ttx-request.json | 7 +-- .../openrtb2/ttx/test-ttx-bid-request-1.json | 7 +-- .../test-auction-ucfunnel-request.json | 7 +-- .../ucfunnel/test-ucfunnel-bid-request.json | 7 +-- .../unruly/test-auction-unruly-request.json | 7 +-- .../unruly/test-unruly-bid-request-1.json | 7 +-- .../unruly/test-unruly-bid-request-2.json | 7 +-- .../test-auction-valueimpression-request.json | 7 +-- .../test-valueimpression-bid-request-1.json | 7 +-- .../test-auction-verizonmedia-request.json | 7 +-- .../test-verizonmedia-bid-request-1.json | 7 +-- .../visx/test-auction-visx-request.json | 7 +-- .../openrtb2/visx/test-visx-bid-request.json | 7 +-- .../vrtcal/test-auction-vrtcal-request.json | 7 +-- .../vrtcal/test-vrtcal-bid-request-1.json | 7 +-- .../yieldmo/test-auction-yieldmo-request.json | 7 +-- .../yieldmo/test-yieldmo-bid-request-1.json | 7 +-- .../test-auction-yieldone-request.json | 7 +-- .../yieldone/test-yieldone-bid-request.json | 7 +-- .../test-auction-zeroclickfraud-request.json | 7 +-- .../test-auction-zeroclickfraud-response.json | 11 ++--- .../test-zeroclickfraud-bid-request-1.json | 7 +-- .../test-zeroclickfraud-bid-request-2.json | 7 +-- 196 files changed, 199 insertions(+), 1295 deletions(-) delete mode 100644 src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrust.java delete mode 100644 src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrustPrivacy.java delete mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserDigiTrust.java diff --git a/docs/application-settings.md b/docs/application-settings.md index 178ae5c9cc2..b6e5b7bc8b6 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -30,7 +30,7 @@ Purpose | Purpose goal | Purpose meaning for PBS (n\a - not p1 | Access device | Stops usersync for given vendor and stops settings cookie on `/seuid` p2 | Select basic ads | Verify consent for each vendor as appropriate for the enforcement method before calling a bid adapter. If consent is not granted, log a metric and skip it. p3 | Personalized ads profile | n\a -p4 | Select personalized ads | Verify consent for each vendor that passed the Purpose 2. If consent is not granted, remove the bidrequest.userId, user.ext.eids, user.ext.digitrust, device.if attributes and call the adapter. +p4 | Select personalized ads | Verify consent for each vendor that passed the Purpose 2. If consent is not granted, remove the bidrequest.userId, user.ext.eids, device.if attributes and call the adapter. p5 | Personalized content profile | n\a p6 | Select personalized content | n\a p7 | Measure ad performance | Verify consent for each analytics module. If consent is not grantet skip it. diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index ec690ae104a..4c1356a1ba5 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -116,7 +116,6 @@ The only exception here is the top-level `BidResponse`, because it's bidder-inde Exceptions are made for extensions with "standard" recommendations: -- `request.user.ext.digitrust` -- To support Digitrust support - `request.regs.ext.gdpr` and `request.user.ext.consent` -- To support GDPR - `request.site.ext.amp` -- To identify AMP as the request source - `request.app.ext.source` and `request.app.ext.version` -- To support identifying the displaymanager/SDK in mobile apps. If given, we expect these to be strings. @@ -683,11 +682,7 @@ Prebid Server adapters can support the [Prebid.js User ID modules](http://prebid "source": "pubcommon", "id":"11111111" } - ], - "digitrust": { - "id": "11111111111", - "keyv": 4 - } + ] } } } diff --git a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java index 78a1bc9d835..4564a844a34 100644 --- a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java @@ -49,8 +49,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; @@ -278,7 +276,6 @@ BidRequest fillImplicitParameters(BidRequest bidRequest, RoutingContext context, final Site populatedSite = bidRequest.getApp() != null ? null : populateSite(site, request); final User user = bidRequest.getUser(); - final User populatedUser = populateUser(user); final Source source = bidRequest.getSource(); final Source populatedSource = populateSource(source); @@ -299,14 +296,13 @@ BidRequest fillImplicitParameters(BidRequest bidRequest, RoutingContext context, final ExtRequest populatedExt = populateRequestExt( ext, bidRequest, ObjectUtils.defaultIfNull(populatedImps, imps)); - if (populatedDevice != null || populatedSite != null || populatedUser != null || populatedSource != null + if (populatedDevice != null || populatedSite != null || populatedSource != null || populatedImps != null || resolvedAt != null || resolvedCurrencies != null || resolvedTmax != null || populatedExt != null) { result = bidRequest.toBuilder() .device(populatedDevice != null ? populatedDevice : device) .site(populatedSite != null ? populatedSite : site) - .user(populatedUser != null ? populatedUser : user) .source(populatedSource != null ? populatedSource : source) .imp(populatedImps != null ? populatedImps : imps) .at(resolvedAt != null ? resolvedAt : at) @@ -450,33 +446,6 @@ private Site populateSite(Site site, HttpServerRequest request) { return result; } - /** - * Populates the request body's 'user' section from the incoming http request if the original is partially filled. - */ - private User populateUser(User user) { - final ExtUser ext = userExtOrNull(user); - - if (ext != null) { - return user.toBuilder().ext(ext).build(); - } - return null; - } - - /** - * Returns updated {@link ExtUser} or null if no updates needed. - */ - private ExtUser userExtOrNull(User user) { - final ExtUser extUser = user != null ? user.getExt() : null; - - final ExtUserDigiTrust digitrust = extUser != null ? extUser.getDigitrust() : null; - if (digitrust != null && digitrust.getPref() == null) { - return extUser.toBuilder() - .digitrust(ExtUserDigiTrust.of(digitrust.getId(), digitrust.getKeyv(), 0)) - .build(); - } - return null; - } - /** * Returns {@link Source} with updated source.tid or null if nothing changed. */ diff --git a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java index d8136ad81d7..ab09449e78f 100644 --- a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java +++ b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java @@ -410,7 +410,7 @@ private static boolean shouldMaskUser(User user) { return true; } final ExtUser extUser = user.getExt(); - return extUser != null && (CollectionUtils.isNotEmpty(extUser.getEids()) || extUser.getDigitrust() != null); + return extUser != null && (CollectionUtils.isNotEmpty(extUser.getEids())); } /** @@ -550,11 +550,11 @@ private static Float maskGeoCoordinate(Float coordinate) { } /** - * Returns masked digitrust and eids of user ext. + * Returns masked eids of user ext. */ private static ExtUser maskUserExt(ExtUser userExt) { return userExt != null - ? nullIfEmpty(userExt.toBuilder().eids(null).digitrust(null).build()) + ? nullIfEmpty(userExt.toBuilder().eids(null).build()) : null; } diff --git a/src/main/java/org/prebid/server/bidder/adform/AdformAdapter.java b/src/main/java/org/prebid/server/bidder/adform/AdformAdapter.java index 18b6ca38d8d..a12902ecdbd 100644 --- a/src/main/java/org/prebid/server/bidder/adform/AdformAdapter.java +++ b/src/main/java/org/prebid/server/bidder/adform/AdformAdapter.java @@ -14,7 +14,6 @@ import org.prebid.server.auction.model.PreBidRequestContext; import org.prebid.server.bidder.Adapter; import org.prebid.server.bidder.adform.model.AdformBid; -import org.prebid.server.bidder.adform.model.AdformDigitrust; import org.prebid.server.bidder.adform.model.AdformParams; import org.prebid.server.bidder.adform.model.UrlParameters; import org.prebid.server.bidder.model.AdapterHttpRequest; @@ -53,7 +52,7 @@ public AdformAdapter(String cookieFamilyName, String endpointUrl, JacksonMapper this.mapper = Objects.requireNonNull(mapper); this.requestUtil = new AdformRequestUtil(); - this.httpUtil = new AdformHttpUtil(mapper); + this.httpUtil = new AdformHttpUtil(); } /** @@ -73,7 +72,7 @@ public List> makeHttpRequests(AdapterRequest adapterReq HttpMethod.GET, getUrl(preBidRequestContext, adformParams, extUser), null, - headers(preBidRequestContext, requestUtil.getAdformDigitrust(extUser)))); + headers(preBidRequestContext))); } @Override @@ -198,14 +197,13 @@ private String getUrlFromParams(List adformParams) { /** * Creates adform headers, which stores adform request parameters */ - private MultiMap headers(PreBidRequestContext preBidRequestContext, AdformDigitrust adformDigitrust) { + private MultiMap headers(PreBidRequestContext preBidRequestContext) { return httpUtil.buildAdformHeaders( VERSION, ObjectUtils.defaultIfNull(preBidRequestContext.getUa(), ""), ObjectUtils.defaultIfNull(preBidRequestContext.getIp(), ""), preBidRequestContext.getReferer(), - preBidRequestContext.getUidsCookie().uidFrom(cookieFamilyName), - adformDigitrust); + preBidRequestContext.getUidsCookie().uidFrom(cookieFamilyName)); } /** diff --git a/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java b/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java index 479fcfe764c..548234336af 100644 --- a/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java +++ b/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java @@ -60,7 +60,7 @@ public AdformBidder(String endpointUrl, JacksonMapper mapper) { this.mapper = Objects.requireNonNull(mapper); this.requestUtil = new AdformRequestUtil(); - this.httpUtil = new AdformHttpUtil(mapper); + this.httpUtil = new AdformHttpUtil(); } /** @@ -98,8 +98,8 @@ public Result>> makeHttpRequests(BidRequest request) { .secure(getSecure(imps)) .gdprApplies(requestUtil.getGdprApplies(request.getRegs())) .consent(requestUtil.getConsent(extUser)) - .currency(currency) .eids(requestUtil.getEids(extUser, mapper)) + .currency(currency) .url(getUrl(extImpAdforms)) .build()); @@ -108,8 +108,7 @@ public Result>> makeHttpRequests(BidRequest request) { getUserAgent(device), getIp(device), getReferer(request.getSite()), - getUserId(user), - requestUtil.getAdformDigitrust(extUser)); + getUserId(user)); return Result.of(Collections.singletonList( HttpRequest.builder() diff --git a/src/main/java/org/prebid/server/bidder/adform/AdformHttpUtil.java b/src/main/java/org/prebid/server/bidder/adform/AdformHttpUtil.java index e57f0b28dbd..0824215ffae 100644 --- a/src/main/java/org/prebid/server/bidder/adform/AdformHttpUtil.java +++ b/src/main/java/org/prebid/server/bidder/adform/AdformHttpUtil.java @@ -4,17 +4,13 @@ import io.vertx.core.MultiMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.bidder.adform.model.AdformDigitrust; import org.prebid.server.bidder.adform.model.UrlParameters; -import org.prebid.server.json.EncodeException; -import org.prebid.server.json.JacksonMapper; import org.prebid.server.util.HttpUtil; import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.stream.Collectors; /** @@ -30,10 +26,7 @@ class AdformHttpUtil { private static final Locale LOCALE = Locale.US; - private final JacksonMapper mapper; - - AdformHttpUtil(JacksonMapper mapper) { - this.mapper = Objects.requireNonNull(mapper); + AdformHttpUtil() { } /** @@ -43,8 +36,7 @@ MultiMap buildAdformHeaders(String version, String userAgent, String ip, String referer, - String userId, - AdformDigitrust adformDigitrust) { + String userId) { final MultiMap headers = MultiMap.caseInsensitiveMultiMap() .add(HttpUtil.CONTENT_TYPE_HEADER, HttpUtil.APPLICATION_JSON_CONTENT_TYPE) @@ -61,18 +53,6 @@ MultiMap buildAdformHeaders(String version, cookieValues.add(String.format("uid=%s", userId)); } - if (adformDigitrust != null) { - try { - final String adformDigitrustEncoded = Base64.getUrlEncoder().withoutPadding() - .encodeToString(mapper.encode(adformDigitrust).getBytes()); - // Cookie name and structure are described here: - // https://github.com/digi-trust/dt-cdn/wiki/Cookies-for-Platforms - cookieValues.add(String.format("DigiTrust.v1.identity=%s", adformDigitrustEncoded)); - } catch (EncodeException e) { - // do not add digitrust to cookie header and just ignore this exception - } - } - if (CollectionUtils.isNotEmpty(cookieValues)) { headers.add(HttpUtil.COOKIE_HEADER, String.join(";", cookieValues)); } diff --git a/src/main/java/org/prebid/server/bidder/adform/AdformRequestUtil.java b/src/main/java/org/prebid/server/bidder/adform/AdformRequestUtil.java index c4a588da015..12b93922912 100644 --- a/src/main/java/org/prebid/server/bidder/adform/AdformRequestUtil.java +++ b/src/main/java/org/prebid/server/bidder/adform/AdformRequestUtil.java @@ -3,12 +3,9 @@ import com.iab.openrtb.request.Regs; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; -import org.prebid.server.bidder.adform.model.AdformDigitrust; -import org.prebid.server.bidder.adform.model.AdformDigitrustPrivacy; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid; @@ -24,8 +21,6 @@ */ class AdformRequestUtil { - private static final int DIGITRUST_VERSION = 1; - /** * Retrieves gdpr from regs.ext.gdpr and in case of any exception or invalid values returns empty string. */ @@ -44,20 +39,6 @@ String getConsent(ExtUser extUser) { return ObjectUtils.defaultIfNull(gdprConsent, ""); } - /** - * Creates {@link AdformDigitrust} from user.extUser.digitrust, if something wrong, returns null. - */ - AdformDigitrust getAdformDigitrust(ExtUser extUser) { - final ExtUserDigiTrust extUserDigiTrust = extUser != null ? extUser.getDigitrust() : null; - return extUserDigiTrust != null - ? AdformDigitrust.of( - extUserDigiTrust.getId(), - DIGITRUST_VERSION, - extUserDigiTrust.getKeyv(), - AdformDigitrustPrivacy.of(extUserDigiTrust.getPref() != 0)) - : null; - } - /** * Retrieves eids from user.ext.eids and in case of any exception or invalid values return empty collection. */ diff --git a/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrust.java b/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrust.java deleted file mode 100644 index d6dbe48a2a8..00000000000 --- a/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrust.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.bidder.adform.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class AdformDigitrust { - - String id; - - Integer version; - - Integer keyv; - - AdformDigitrustPrivacy privacy; -} diff --git a/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrustPrivacy.java b/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrustPrivacy.java deleted file mode 100644 index e654da1e065..00000000000 --- a/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrustPrivacy.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.adform.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class AdformDigitrustPrivacy { - - Boolean optout; -} diff --git a/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java b/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java index 28b345c5069..92d7c8b7a44 100644 --- a/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java +++ b/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java @@ -26,7 +26,6 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.request.dmx.ExtImpDmx; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -198,9 +197,7 @@ private static void checkIfHasId(App app, User user) { } final ExtUser userExt = user.getExt(); if (userExt != null) { - final ExtUserDigiTrust digitrust = userExt.getDigitrust(); - if (CollectionUtils.isNotEmpty(userExt.getEids()) || (digitrust != null - && StringUtils.isNotBlank(digitrust.getId()))) { + if (CollectionUtils.isNotEmpty(userExt.getEids())) { anyHasId = true; } } diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconAdapter.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconAdapter.java index fc33a44f472..fb35f696966 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconAdapter.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconAdapter.java @@ -55,7 +55,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.request.rubicon.RubiconVideoParams; import org.prebid.server.proto.request.PreBidRequest; import org.prebid.server.proto.request.Sdk; @@ -344,19 +343,17 @@ private User makeUser(RubiconParams rubiconParams, PreBidRequestContext preBidRe } private ExtUser makeUserExt(RubiconParams rubiconParams, ExtUser extUser) { - final ExtUserDigiTrust digiTrust = extUser != null ? extUser.getDigitrust() : null; // will be removed final JsonNode visitorNode = rubiconParams.getVisitor(); final JsonNode visitor = visitorNode != null && !visitorNode.isNull() && visitorNode.size() != 0 ? visitorNode : null; - final boolean makeRp = visitor != null; - if (digiTrust != null || visitor != null) { + if (visitor != null) { final ExtUser userExt = extUser != null ? ExtUser.builder().consent(extUser.getConsent()).eids(extUser.getEids()).build() : ExtUser.builder().build(); final RubiconUserExt rubiconUserExt = RubiconUserExt.builder() - .rp(makeRp ? RubiconUserExtRp.of(visitor) : null) + .rp(RubiconUserExtRp.of(visitor)) .build(); return mapper.fillExtension(userExt, rubiconUserExt); } 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 85eca8aa4bf..fcd3d1cbaa8 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -716,7 +716,7 @@ private User makeUser(User user, ExtImpRubicon rubiconImpExt) { final ExtUser userExt = extUser != null ? ExtUser.builder() .consent(extUser.getConsent()) - .digitrust(extUser.getDigitrust()) + .eids(extUser.getEids()) .eids(resolvedExtUserEids) .build() : ExtUser.builder().build(); diff --git a/src/main/java/org/prebid/server/privacy/gdpr/model/PrivacyEnforcementAction.java b/src/main/java/org/prebid/server/privacy/gdpr/model/PrivacyEnforcementAction.java index 4da89cc9a48..a5af3d00715 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/model/PrivacyEnforcementAction.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/model/PrivacyEnforcementAction.java @@ -7,7 +7,7 @@ @Data public class PrivacyEnforcementAction { - boolean removeUserIds; // user.buyeruid, user.id, user.ext.eids, user.ext.digitrust + boolean removeUserIds; // user.buyeruid, user.id, user.ext.eids boolean maskGeo; // user.geo, device.geo diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java index abb3da58692..c0666dcb14b 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java @@ -30,14 +30,6 @@ public class ExtUser extends FlexibleExtension { */ String consent; - /** - * DigiTrust breaks the typical Prebid Server convention of namespacing "global" options inside "ext.prebid.*" - * to match the recommendation from the broader digitrust community. - *

- * For more info, see: https://github.com/digi-trust/dt-cdn/wiki/OpenRTB-extension#openrtb-2x - */ - ExtUserDigiTrust digitrust; - /** * Standardized User IDs. */ diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserDigiTrust.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserDigiTrust.java deleted file mode 100644 index 1a1c07cc3b7..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserDigiTrust.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request; - -import lombok.AllArgsConstructor; -import lombok.Value; - -/** - * Defines the contract for bidrequest.user.ext.digitrust - * More info on DigiTrust can be found here: https://github.com/digi-trust/dt-cdn/wiki/Integration-Guide - */ -@AllArgsConstructor(staticName = "of") -@Value -public class ExtUserDigiTrust { - - /** - * Unique device identifier - */ - String id; - - /** - * Key version used to encrypt id - */ - Integer keyv; - - /** - * User optout preference - */ - Integer pref; -} diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java index ab4a1f3d8d7..28d93c9f807 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -48,7 +48,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; @@ -392,11 +391,6 @@ private void validateUser(User user, Map aliases) throws Validat } } - final ExtUserDigiTrust digitrust = extUser.getDigitrust(); - if (digitrust != null && digitrust.getPref() != null && digitrust.getPref() != 0) { - throw new ValidationException("request.user contains a digitrust object that is not valid"); - } - final List eids = extUser.getEids(); if (eids != null) { if (eids.isEmpty()) { diff --git a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java index a9bbdcd00b6..c01a38e70f0 100644 --- a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java @@ -61,8 +61,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; import org.prebid.server.validation.RequestValidator; @@ -694,27 +692,6 @@ public void shouldSetSiteExtAmpIfNoReferer() { .ext(ExtSite.of(0, null)).build()); } - @Test - public void shouldSetUserExtDigitrustPerfIfNotDefined() { - // given - givenBidRequest(BidRequest.builder() - .user(User.builder() - .ext(ExtUser.builder() - .digitrust(ExtUserDigiTrust.of("id", 123, null)) - .build()) - .build()) - .build()); - - // when - final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest(); - - // then - assertThat(request.getUser().getExt()) - .isEqualTo(ExtUser.builder() - .digitrust(ExtUserDigiTrust.of("id", 123, 0)) - .build()); - } - @Test public void shouldSetSourceTidIfNotDefined() { // given diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 47aa5684387..639155fbcde 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -73,7 +73,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtSource; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; import org.prebid.server.proto.openrtb.ext.response.BidType; @@ -1317,9 +1316,8 @@ public void shouldPassUserExtDataOnlyForAllowedBidder() { final ObjectNode dataNode = mapper.createObjectNode().put("data", "value"); final Map bidderToGdpr = doubleMap("someBidder", 1, "missingBidder", 0); - final ExtUserDigiTrust extUserDigiTrust = ExtUserDigiTrust.of("dId", 23, 222); final List eids = singletonList(ExtUserEid.of("eId", "id", emptyList(), null)); - final ExtUser extUser = ExtUser.builder().data(dataNode).digitrust(extUserDigiTrust).eids(eids).build(); + final ExtUser extUser = ExtUser.builder().data(dataNode).eids(eids).build(); final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr), builder -> builder @@ -1343,7 +1341,7 @@ public void shouldPassUserExtDataOnlyForAllowedBidder() { verify(httpBidderRequester, times(2)).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean()); final List capturedBidRequests = bidRequestCaptor.getAllValues(); - final ExtUser maskedExtUser = ExtUser.builder().digitrust(extUserDigiTrust).eids(eids).build(); + final ExtUser maskedExtUser = ExtUser.builder().eids(eids).build(); assertThat(capturedBidRequests) .extracting(BidRequest::getUser) .extracting(User::getKeywords, User::getGender, User::getYob, User::getGeo, User::getExt) @@ -1394,9 +1392,8 @@ public void shouldMaskUserExtIfDataBiddersListIsEmpty() { final ObjectNode dataNode = mapper.createObjectNode().put("data", "value"); final Map bidderToGdpr = doubleMap("someBidder", 1, "missingBidder", 0); - final ExtUserDigiTrust extUserDigiTrust = ExtUserDigiTrust.of("dId", 23, 222); final List eids = singletonList(ExtUserEid.of("eId", "id", emptyList(), null)); - final ExtUser extUser = ExtUser.builder().data(dataNode).digitrust(extUserDigiTrust).eids(eids).build(); + final ExtUser extUser = ExtUser.builder().data(dataNode).eids(eids).build(); final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr), builder -> builder @@ -1418,7 +1415,7 @@ public void shouldMaskUserExtIfDataBiddersListIsEmpty() { verify(httpBidderRequester, times(2)).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean()); final List capturedBidRequests = bidRequestCaptor.getAllValues(); - final ExtUser expectedExtUser = ExtUser.builder().digitrust(extUserDigiTrust).eids(eids).build(); + final ExtUser expectedExtUser = ExtUser.builder().eids(eids).build(); assertThat(capturedBidRequests) .extracting(BidRequest::getUser) .extracting(User::getKeywords, User::getGender, User::getYob, User::getGeo, User::getExt) diff --git a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java index 9307cafef01..10bd0b29045 100644 --- a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java +++ b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java @@ -42,7 +42,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; import org.prebid.server.proto.request.CookieSyncRequest; @@ -665,7 +664,6 @@ public void shouldMaskUserIdsWhenTcfDefinerServiceRestrictUserIdsAndReturnNullWh final ExtUser extUser = ExtUser.builder() .eids(singletonList(ExtUserEid.of("Test", "id", emptyList(), null))) - .digitrust(ExtUserDigiTrust.of("idDigit", 12, 23)) .build(); final User user = User.builder() .buyeruid(BUYER_UID) @@ -1063,7 +1061,6 @@ public void shouldNotReturnUserIfMaskingAppliedAndUserBecameEmptyObject() { // given final ExtUser extUser = ExtUser.builder() .eids(singletonList(ExtUserEid.of("Test", "id", emptyList(), null))) - .digitrust(ExtUserDigiTrust.of("idDigit", 12, 23)) .build(); final User user = User.builder() .buyeruid("buyeruid") @@ -1431,7 +1428,6 @@ private static User notMaskedUser(ExtUser extUser) { private static ExtUser notMaskedExtUser() { return ExtUser.builder() .eids(singletonList(ExtUserEid.of("Test", "id", emptyList(), null))) - .digitrust(ExtUserDigiTrust.of("idDigit", 12, 23)) .prebid(ExtUserPrebid.of(singletonMap("key", "value"))) .build(); } diff --git a/src/test/java/org/prebid/server/bidder/adform/AdformAdapterTest.java b/src/test/java/org/prebid/server/bidder/adform/AdformAdapterTest.java index 79b359c98ba..2de96f35a32 100644 --- a/src/test/java/org/prebid/server/bidder/adform/AdformAdapterTest.java +++ b/src/test/java/org/prebid/server/bidder/adform/AdformAdapterTest.java @@ -23,7 +23,6 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.request.PreBidRequest; import org.prebid.server.proto.response.Bid; import org.prebid.server.proto.response.MediaType; @@ -76,7 +75,6 @@ public void makeHttpRequestsShouldReturnAdapterHttpRequestWithCorrectUrlAndHeade .user(User.builder() .ext(ExtUser.builder() .consent("consent") - .digitrust(ExtUserDigiTrust.of("id", 123, 1)) .build()) .build()) .build()) @@ -114,9 +112,7 @@ public void makeHttpRequestsShouldReturnAdapterHttpRequestWithCorrectUrlAndHeade tuple(HttpUtil.X_REQUEST_AGENT_HEADER.toString(), "PrebidAdapter 0.1.3"), tuple(HttpUtil.REFERER_HEADER.toString(), "www.example.com"), // Base64 encoded {"id":"id","version":1,"keyv":123,"privacy":{"optout":true}} - tuple(HttpUtil.COOKIE_HEADER.toString(), - "uid=buyeruid;DigiTrust.v1.identity=eyJpZCI6ImlkIiwidmVyc2lvbiI6MSwia2V5diI6MTIzLCJw" - + "cml2YWN5Ijp7Im9wdG91dCI6dHJ1ZX19")); + tuple(HttpUtil.COOKIE_HEADER.toString(), "uid=buyeruid")); } @Test diff --git a/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java b/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java index 0c2ca714f6b..ed8909ec01f 100644 --- a/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java @@ -25,7 +25,6 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid; import org.prebid.server.proto.openrtb.ext.request.adform.ExtImpAdform; @@ -68,7 +67,6 @@ public void makeHttpRequestsShouldReturnHttpRequestWithoutErrors() { .buyeruid("buyeruid") .ext(ExtUser.builder() .consent("consent") - .digitrust(ExtUserDigiTrust.of("id", 123, 1)) .eids(singletonList(ExtUserEid.of("test.com", "some_user_id", singletonList(ExtUserEidUid.of("uId", 1, null)), null))) .build()) @@ -103,9 +101,7 @@ public void makeHttpRequestsShouldReturnHttpRequestWithoutErrors() { tuple(HttpUtil.X_REQUEST_AGENT_HEADER.toString(), "PrebidAdapter 0.1.3"), tuple(HttpUtil.REFERER_HEADER.toString(), "www.example.com"), // Base64 encoded {"id":"id","version":1,"keyv":123,"privacy":{"optout":true}} - tuple(HttpUtil.COOKIE_HEADER.toString(), - "uid=buyeruid;DigiTrust.v1.identity=eyJpZCI6ImlkIiwidmVyc2lvbiI6MSwia2V5diI6MTIzLCJwcml" - + "2YWN5Ijp7Im9wdG91dCI6dHJ1ZX19")); + tuple(HttpUtil.COOKIE_HEADER.toString(), "uid=buyeruid")); } @Test @@ -184,7 +180,6 @@ public void makeHttpRequestsShouldReturnHttpsUrlIfAtLeastOneImpIsSecured() { .buyeruid("buyeruid") .ext(ExtUser.builder() .consent("consent") - .digitrust(ExtUserDigiTrust.of("id", 123, 1)) .eids(singletonList(ExtUserEid.of("test.com", "some_user_id", singletonList(ExtUserEidUid.of("uId", 1, null)), null))) .build()) @@ -333,7 +328,6 @@ public void makeHttpRequestsShouldPassMultipleUserIds() { .buyeruid("buyeruid") .ext(ExtUser.builder() .consent("consent") - .digitrust(ExtUserDigiTrust.of("id", 123, 1)) .eids(asList(ExtUserEid.of("test.com", "some_user_id", singletonList(ExtUserEidUid.of("uId", 1, null)), null), ExtUserEid.of("test.com", "some_user_id", @@ -350,8 +344,9 @@ public void makeHttpRequestsShouldPassMultipleUserIds() { // then assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getUri) - .containsExactly("https://adform.com/openrtb2d?CC=1&eids=eyJ0ZXN0LmNvbSI6eyJ1SWQiOlsxLDJdfSwidGVzdC5uZXQiOnsiaWRfc29tZV91c2VyIjpbM119fQ&fd=1&gdpr=" - + "&gdpr_consent=consent&ip=&rp=4&stid=tid&bWlkPTE1JnJjdXI9VVNE"); + .containsExactly( + "https://adform.com/openrtb2d?CC=1&eids=eyJ0ZXN0LmNvbSI6eyJ1SWQiOlsxLDJdfSwidGVzdC5uZXQiOnsiaWRfc29tZV91c2VyIjpbM119fQ&fd=1&gdpr=" + + "&gdpr_consent=consent&ip=&rp=4&stid=tid&bWlkPTE1JnJjdXI9VVNE"); } private static HttpCall givenHttpCall(String body) { diff --git a/src/test/java/org/prebid/server/bidder/adform/AdformHttpUtilTest.java b/src/test/java/org/prebid/server/bidder/adform/AdformHttpUtilTest.java index bda3754eec2..3b5c320b764 100644 --- a/src/test/java/org/prebid/server/bidder/adform/AdformHttpUtilTest.java +++ b/src/test/java/org/prebid/server/bidder/adform/AdformHttpUtilTest.java @@ -5,8 +5,6 @@ import org.junit.Before; import org.junit.Test; import org.prebid.server.VertxTest; -import org.prebid.server.bidder.adform.model.AdformDigitrust; -import org.prebid.server.bidder.adform.model.AdformDigitrustPrivacy; import org.prebid.server.bidder.adform.model.UrlParameters; import org.prebid.server.util.HttpUtil; @@ -27,7 +25,7 @@ public class AdformHttpUtilTest extends VertxTest { @Before public void setUp() { - httpUtil = new AdformHttpUtil(jacksonMapper); + httpUtil = new AdformHttpUtil(); } @Test @@ -38,8 +36,7 @@ public void buildAdformHeadersShouldReturnAllHeaders() { "userAgent", "ip", "www.example.com", - "buyeruid", - AdformDigitrust.of("id", 1, 123, AdformDigitrustPrivacy.of(true))); + "buyeruid"); // then assertThat(headers).hasSize(7) @@ -50,10 +47,7 @@ public void buildAdformHeadersShouldReturnAllHeaders() { tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "ip"), tuple(HttpUtil.X_REQUEST_AGENT_HEADER.toString(), "PrebidAdapter 0.1.0"), tuple(HttpUtil.REFERER_HEADER.toString(), "www.example.com"), - tuple(HttpUtil.COOKIE_HEADER.toString(), - // Base64 encoded {"id":"id","version":1,"keyv":123,"privacy":{"optout":true}} - "uid=buyeruid;DigiTrust.v1.identity=eyJpZCI6ImlkIiwidmVyc2lvbiI6MSwia2V5diI6MTIzLC" - + "Jwcml2YWN5Ijp7Im9wdG91dCI6dHJ1ZX19")); + tuple(HttpUtil.COOKIE_HEADER.toString(), "uid=buyeruid")); } @Test @@ -64,63 +58,41 @@ public void buildAdformHeadersShouldNotContainRefererHeaderIfRefererIsEmpty() { "userAgent", "ip", "", - "buyeruid", - AdformDigitrust.of("id", 1, 123, AdformDigitrustPrivacy.of(true))); + "buyeruid"); // then assertThat(headers).extracting(Map.Entry::getKey).doesNotContain(HttpUtil.REFERER_HEADER.toString()); } @Test - public void buildAdformHeadersShouldNotContainCookieHeaderIfUserIdAndDigiTrustAreEmpty() { + public void buildAdformHeadersShouldNotContainCookieHeaderIfUserIdIsEmpty() { // when final MultiMap headers = httpUtil.buildAdformHeaders( "0.1.0", "userAgent", "ip", "referer", - "", - null); + ""); // then assertThat(headers).extracting(Map.Entry::getKey).doesNotContain(HttpUtil.COOKIE_HEADER.toString()); } @Test - public void buildAdformHeaderShouldContainCookieHeaderOnlyWithUserIdIfUserIdPresentAndDigitrustAbsent() { + public void buildAdformHeaderShouldContainCookieHeaderOnlyWithUserIdIfUserIdPresent() { // when final MultiMap headers = httpUtil.buildAdformHeaders( "0.1.0", "userAgent", "ip", "referer", - "buyeruid", - null); + "buyeruid"); // then assertThat(headers).extracting(Map.Entry::getKey, Map.Entry::getValue) .contains(tuple(HttpUtil.COOKIE_HEADER.toString(), "uid=buyeruid")); } - @Test - public void buildAdformHeaderShouldContainCookieHeaderOnlyWithDigitrustIfUserIsAbsentAndDigitrustPresent() { - // when - final MultiMap headers = httpUtil.buildAdformHeaders( - "0.1.0", - "userAgent", - "ip", - "referer", - "", - AdformDigitrust.of("id", 1, 123, AdformDigitrustPrivacy.of(true))); - - // then - assertThat(headers).extracting(Map.Entry::getKey, Map.Entry::getValue) - .contains(tuple(HttpUtil.COOKIE_HEADER.toString(), - // Base64 encoded {"id":"id","version":1,"keyv":123,"privacy":{"optout":true}} - "DigiTrust.v1.identity=eyJpZCI6ImlkIiwidmVyc2lvbiI6MSwia2V5diI6MTIzLCJwcml2YWN5Ijp7Im9wdG91dC" - + "I6dHJ1ZX19")); - } - @Test public void buildAdformUrlShouldReturnCorrectUrl() { // when diff --git a/src/test/java/org/prebid/server/bidder/adform/AdformRequestUtilTest.java b/src/test/java/org/prebid/server/bidder/adform/AdformRequestUtilTest.java index 4cf50a36201..46afb44585d 100644 --- a/src/test/java/org/prebid/server/bidder/adform/AdformRequestUtilTest.java +++ b/src/test/java/org/prebid/server/bidder/adform/AdformRequestUtilTest.java @@ -4,11 +4,8 @@ import org.junit.Before; import org.junit.Test; import org.prebid.server.VertxTest; -import org.prebid.server.bidder.adform.model.AdformDigitrust; -import org.prebid.server.bidder.adform.model.AdformDigitrustPrivacy; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import static org.assertj.core.api.Assertions.assertThat; @@ -92,44 +89,4 @@ public void getConsentShouldReturnConsent() { // then assertThat(consent).isEqualTo("consent"); } - - @Test - public void getAdformDigiTrustShouldReturnNullIfUserExtIsNull() { - // given and when - final AdformDigitrust adformDigitrust = requestUtil.getAdformDigitrust(null); - - // then - assertThat(adformDigitrust).isNull(); - } - - @Test - public void getAdformDigiTrustShouldReturnNullIfUserExtDigitrustIsNull() { - // given and when - final AdformDigitrust adformDigitrust = requestUtil.getAdformDigitrust(ExtUser.builder().build()); - - // then - assertThat(adformDigitrust).isNull(); - } - - @Test - public void getAdformDigiTrustShouldReturnAdformDigitrustWithOptOutFalseIfPrefIsZero() { - // given and when - final AdformDigitrust adformDigitrust = requestUtil.getAdformDigitrust(ExtUser.builder() - .digitrust(ExtUserDigiTrust.of("id", 123, 0)) - .build()); - - // then - assertThat(adformDigitrust).isEqualTo(AdformDigitrust.of("id", 1, 123, AdformDigitrustPrivacy.of(false))); - } - - @Test - public void getAdformDigiTrustShouldReturnAdformDigitrustWithOptOutTrueIfPrefIsNotZero() { - // given and when - final AdformDigitrust adformDigitrust = requestUtil.getAdformDigitrust(ExtUser.builder() - .digitrust(ExtUserDigiTrust.of("id", 123, 1)) - .build()); - - // then - assertThat(adformDigitrust).isEqualTo(AdformDigitrust.of("id", 1, 123, AdformDigitrustPrivacy.of(true))); - } } 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 595f2742b47..466256bc8ae 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -65,7 +65,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid; import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUidExt; @@ -575,32 +574,6 @@ public void makeHttpRequestsShouldNotFillUserExtRpWhenVisitorAndInventoryIsEmpty .containsOnly(User.builder().id("id").build()); } - @Test - public void makeHttpRequestsShouldFillUserExtIfUserAndDigiTrustPresent() { - // given - final BidRequest bidRequest = givenBidRequest( - builder -> builder.user(User.builder() - .ext(ExtUser.builder() - .digitrust(ExtUserDigiTrust.of("id", 123, 0)) - .build()) - .build()), - builder -> builder.video(Video.builder().build()), - identity()); - // when - final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).doesNotContainNull() - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .extracting(BidRequest::getUser).doesNotContainNull() - .containsOnly(User.builder() - .ext(ExtUser.builder() - .digitrust(ExtUserDigiTrust.of("id", 123, 0)) - .build()) - .build()); - } - @Test public void makeHttpRequestsShouldFillUserIfUserAndConsentArePresent() { // given @@ -786,7 +759,7 @@ public void makeHttpRequestsShouldNormalizeAndCopyUserExtDataFieldsToUserExtRp() } @Test - public void makeHttpRequestsShouldNotCreateUserIfVisitorAndDigiTrustAndConsentNotPresent() { + public void makeHttpRequestsShouldNotCreateUserIfVisitorAndConsentNotPresent() { // given final BidRequest bidRequest = givenBidRequest( identity(), diff --git a/src/test/java/org/prebid/server/it/AdformTest.java b/src/test/java/org/prebid/server/it/AdformTest.java index 7cec9b56659..c8181a6d14d 100644 --- a/src/test/java/org/prebid/server/it/AdformTest.java +++ b/src/test/java/org/prebid/server/it/AdformTest.java @@ -49,8 +49,7 @@ public void openrtb2AuctionShouldRespondWithBidsFromAdform() throws IOException, .withHeader("User-Agent", equalTo("userAgent")) .withHeader("X-Request-Agent", equalTo("PrebidAdapter 0.1.3")) .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Cookie", equalTo("uid=AF-UID;DigiTrust.v1.identity=eyJpZCI6ImlkIiwidmVyc2l" - + "vbiI6MSwia2V5diI6MTIzLCJwcml2YWN5Ijp7Im9wdG91dCI6ZmFsc2V9fQ")) + .withHeader("Cookie", equalTo("uid=AF-UID")) .withRequestBody(absent()) .willReturn(aResponse().withBody(jsonFrom("openrtb2/adform/test-adform-bid-response-1.json")))); @@ -99,9 +98,7 @@ public void auctionShouldRespondWithBidsFromAdform() throws IOException { .withHeader("User-Agent", equalTo("userAgent")) .withHeader("X-Request-Agent", equalTo("PrebidAdapter 0.1.3")) .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Cookie", equalTo("uid=AF-UID;DigiTrust.v1.identity" - //{"id":"id","version":1,"keyv":123,"privacy":{"optout":true}} - + "=eyJpZCI6ImlkIiwidmVyc2lvbiI6MSwia2V5diI6MTIzLCJwcml2YWN5Ijp7Im9wdG91dCI6dHJ1ZX19")) + .withHeader("Cookie", equalTo("uid=AF-UID")) .withRequestBody(absent()) .willReturn(aResponse().withBody(jsonFrom("auction/adform/test-adform-bid-response-1.json")))); diff --git a/src/test/java/org/prebid/server/it/TelariaTest.java b/src/test/java/org/prebid/server/it/TelariaTest.java index 2662333773f..0243d2815c1 100644 --- a/src/test/java/org/prebid/server/it/TelariaTest.java +++ b/src/test/java/org/prebid/server/it/TelariaTest.java @@ -34,7 +34,7 @@ public void openrtb2AuctionShouldRespondWithBidsFromTelaria() throws IOException .withHeader("x-openrtb-version", equalTo("2.5")) .withHeader("Accept-Encoding", equalTo("gzip")) .withHeader("Accept-Language", equalTo("en")) - .withHeader("Content-Length", equalTo("677")) + .withHeader("Content-Length", equalTo("633")) .withHeader("DNT", equalTo("2")) .withHeader("Host", equalTo("localhost:8090")) .withRequestBody(equalToJson(jsonFrom("openrtb2/telaria/test-telaria-bid-request-1.json"))) diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index 5f2331643c7..b51c59e96d5 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -48,7 +48,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; @@ -1245,7 +1244,6 @@ public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsNoVal .user(User.builder() .ext(ExtUser.builder() .prebid(ExtUserPrebid.of(emptyMap())) - .digitrust(ExtUserDigiTrust.of(null, null, 0)) .build()) .build()) .build(); @@ -1506,7 +1504,6 @@ public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsUnkno .user(User.builder() .ext(ExtUser.builder() .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) - .digitrust(ExtUserDigiTrust.of(null, null, 0)) .build()) .build()) .build(); @@ -1530,7 +1527,6 @@ public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIs .user(User.builder() .ext(ExtUser.builder() .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) - .digitrust(ExtUserDigiTrust.of(null, null, 0)) .build()) .build()) .build(); @@ -1549,7 +1545,6 @@ public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIs .user(User.builder() .ext(ExtUser.builder() .prebid(ExtUserPrebid.of(singletonMap("rubicon", "42"))) - .digitrust(ExtUserDigiTrust.of(null, null, 0)) .build()) .build()) .build(); @@ -1561,25 +1556,6 @@ public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIs assertThat(result.getErrors()).isEmpty(); } - @Test - public void validateShouldReturnValidationMessageWhenDigiTrustPrefNotEqualZero() { - // given; - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .ext(ExtUser.builder() - .digitrust(ExtUserDigiTrust.of(null, null, 1)) - .build()) - .build()) - .build(); - - // when - final ValidationResult result = requestValidator.validate(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user contains a digitrust object that is not valid"); - } - @Test public void validateShouldReturnValidationMessageWhenEidsIsEmpty() { // given diff --git a/src/test/resources/org/prebid/server/it/auction/adform/test-auction-adform-request.json b/src/test/resources/org/prebid/server/it/auction/adform/test-auction-adform-request.json index 0413c04ffe4..0bfb9328530 100644 --- a/src/test/resources/org/prebid/server/it/auction/adform/test-auction-adform-request.json +++ b/src/test/resources/org/prebid/server/it/auction/adform/test-auction-adform-request.json @@ -36,12 +36,7 @@ }, "user": { "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-request.json b/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-request.json index 2e66d02422d..23578e05f89 100644 --- a/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-request.json +++ b/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-request.json @@ -50,12 +50,7 @@ }, "user": { "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-response.json b/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-response.json index fec15683456..5fd988bf1ac 100644 --- a/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-response.json +++ b/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-response.json @@ -9,7 +9,7 @@ "debug": [ { "request_uri": "{{ conversant.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode10\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250,\"pos\":28},\"displaymanager\":\"prebid-s2s\",\"displaymanagerver\":\"1.0.1\",\"tagid\":\"tagId1\",\"bidfloor\":7.32,\"secure\":42}],\"site\":{\"id\":\"siteId1\",\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"mobile\":64},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"CV-UID\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", + "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode10\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250,\"pos\":28},\"displaymanager\":\"prebid-s2s\",\"displaymanagerver\":\"1.0.1\",\"tagid\":\"tagId1\",\"bidfloor\":7.32,\"secure\":42}],\"site\":{\"id\":\"siteId1\",\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"mobile\":64},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"CV-UID\",\"ext\":{\"consent\":\"consent1\"}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", "response_body": "{\"id\":\"bidResponseId10\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode10\",\"price\":5.78,\"adm\":\"adm10\",\"crid\":\"crid10\",\"dealid\":\"dealId10\",\"w\":300,\"h\":250}],\"seat\":\"seatId10\",\"group\":0}]}", "status_code": 200 } diff --git a/src/test/resources/org/prebid/server/it/auction/conversant/test-conversant-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/conversant/test-conversant-bid-request-1.json index a7bedd264ce..685c6e840e7 100644 --- a/src/test/resources/org/prebid/server/it/auction/conversant/test-conversant-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/auction/conversant/test-conversant-bid-request-1.json @@ -38,12 +38,7 @@ "user": { "buyeruid": "CV-UID", "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-request.json b/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-request.json index 23849c265da..c87e2cd5420 100644 --- a/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-request.json +++ b/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-request.json @@ -61,12 +61,7 @@ }, "user": { "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-response.json b/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-response.json index 33df9613410..64127f4e822 100644 --- a/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-response.json +++ b/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-response.json @@ -9,7 +9,7 @@ "debug": [ { "request_uri": "{{ appnexus.exchange_uri }}?member_id=member1", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode4\",\"video\":{\"mimes\":[\"mimes\"],\"minduration\":20,\"maxduration\":60,\"protocols\":[1],\"w\":300,\"h\":250,\"startdelay\":5,\"playbackmethod\":[1]},\"tagid\":\"invCode1\",\"bidfloor\":1.0,\"ext\":{\"appnexus\":{\"placement_id\":9848285,\"keywords\":\"k1=v1,k1=v2\",\"traffic_source_code\":\"trafficSourceCode1\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"id\":\"12345\",\"buyeruid\":\"12345\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", + "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode4\",\"video\":{\"mimes\":[\"mimes\"],\"minduration\":20,\"maxduration\":60,\"protocols\":[1],\"w\":300,\"h\":250,\"startdelay\":5,\"playbackmethod\":[1]},\"tagid\":\"invCode1\",\"bidfloor\":1.0,\"ext\":{\"appnexus\":{\"placement_id\":9848285,\"keywords\":\"k1=v1,k1=v2\",\"traffic_source_code\":\"trafficSourceCode1\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"id\":\"12345\",\"buyeruid\":\"12345\",\"ext\":{\"consent\":\"consent1\"}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", "response_body": "{\"id\":\"bidResponseId4\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode4\",\"price\":5.78,\"adm\":\"adm4\",\"crid\":\"crid4\",\"dealid\":\"dealId4\",\"w\":300,\"h\":250,\"ext\":{\"appnexus\":{\"bid_ad_type\":1}}}],\"seat\":\"seatId4\",\"group\":0}]}", "status_code": 200 } diff --git a/src/test/resources/org/prebid/server/it/auction/districtm/test-districtm-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/districtm/test-districtm-bid-request-1.json index 5acca6e19ea..18f60cc35b3 100644 --- a/src/test/resources/org/prebid/server/it/auction/districtm/test-districtm-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/auction/districtm/test-districtm-bid-request-1.json @@ -46,12 +46,7 @@ "id": "12345", "buyeruid": "12345", "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-request.json b/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-request.json index 1df884d2720..c3ba263fd57 100644 --- a/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-request.json +++ b/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-request.json @@ -35,12 +35,7 @@ }, "user": { "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-response.json b/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-response.json index 398840b9c3e..474aaa7c4cb 100644 --- a/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-response.json +++ b/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-response.json @@ -9,7 +9,7 @@ "debug": [ { "request_uri": "{{ ix.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode7\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250},\"tagid\":\"adUnitCode7\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"486\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"IE-UID\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", + "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode7\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250},\"tagid\":\"adUnitCode7\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"486\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"IE-UID\",\"ext\":{\"consent\":\"consent1\"}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", "response_body": "{\"id\":\"bidResponseId7\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode7\",\"price\":5.78,\"adm\":\"adm7\",\"crid\":\"crid7\",\"dealid\":\"dealId7\",\"w\":300,\"h\":250}],\"seat\":\"seatId7\",\"group\":0}]}", "status_code": 200 } diff --git a/src/test/resources/org/prebid/server/it/auction/ix/test-ix-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/ix/test-ix-bid-request-1.json index 311b3bd37fd..5e6667648fe 100644 --- a/src/test/resources/org/prebid/server/it/auction/ix/test-ix-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/auction/ix/test-ix-bid-request-1.json @@ -34,12 +34,7 @@ "user": { "buyeruid": "IE-UID", "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-request.json b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-request.json index c05eadda14c..eb81d8988b4 100644 --- a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-request.json +++ b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-request.json @@ -35,12 +35,7 @@ }, "user": { "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-response.json b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-response.json index 2ba37a52718..563979c2a74 100644 --- a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-response.json +++ b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-response.json @@ -9,7 +9,7 @@ "debug": [ { "request_uri": "{{ lifestreet.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode8\",\"banner\":{\"w\":300,\"h\":250},\"tagid\":\"slot.tag1\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"LS-UID\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", + "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode8\",\"banner\":{\"w\":300,\"h\":250},\"tagid\":\"slot.tag1\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"LS-UID\",\"ext\":{\"consent\":\"consent1\"}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", "response_body": "{\"id\":\"bidResponseId8\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode8\",\"price\":5.78,\"adm\":\"adm8\",\"crid\":\"crid8\",\"dealid\":\"dealId8\",\"w\":300,\"h\":250}],\"seat\":\"seatId8\",\"group\":0}]}", "status_code": 200 } diff --git a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-lifestreet-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-lifestreet-bid-request-1.json index b6351131bbe..fd784a16554 100644 --- a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-lifestreet-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-lifestreet-bid-request-1.json @@ -25,12 +25,7 @@ "user": { "buyeruid": "LS-UID", "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-request.json b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-request.json index 2befcd7a7e4..9f66e808813 100644 --- a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-request.json +++ b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-request.json @@ -36,12 +36,7 @@ }, "user": { "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-response.json b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-response.json index e8c3ab7e747..b092e81019c 100644 --- a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-response.json +++ b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-response.json @@ -9,7 +9,7 @@ "debug": [ { "request_uri": "{{ pubmatic.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode9\",\"banner\":{\"format\":[{\"w\":200,\"h\":150}],\"w\":300,\"h\":250},\"tagid\":\"slot9\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId9\",\"domain\":\"example.com\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"PM-UID\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", + "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode9\",\"banner\":{\"format\":[{\"w\":200,\"h\":150}],\"w\":300,\"h\":250},\"tagid\":\"slot9\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId9\",\"domain\":\"example.com\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"PM-UID\",\"ext\":{\"consent\":\"consent1\"}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", "response_body": "{\"id\":\"bidResponseId9\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode9\",\"price\":5.78,\"adm\":\"adm9\",\"crid\":\"crid9\",\"dealid\":\"dealId9\",\"w\":300,\"h\":250}],\"seat\":\"seatId9\",\"group\":0}]}", "status_code": 200 } diff --git a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-pubmatic-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-pubmatic-bid-request-1.json index f42d82b05d8..dfe1ffb7858 100644 --- a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-pubmatic-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-pubmatic-bid-request-1.json @@ -35,12 +35,7 @@ "user": { "buyeruid": "PM-UID", "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-request.json b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-request.json index ef099a0f325..d06c351a753 100644 --- a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-request.json +++ b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-request.json @@ -37,12 +37,7 @@ }, "user": { "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-response.json b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-response.json index 1cc5f52f719..08ec48a4ed4 100644 --- a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-response.json +++ b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-response.json @@ -9,7 +9,7 @@ "debug": [ { "request_uri": "{{ pulsepoint.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode6\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250},\"tagid\":\"456\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"123\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"PP-UID\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", + "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode6\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250},\"tagid\":\"456\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"123\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"PP-UID\",\"ext\":{\"consent\":\"consent1\"}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", "response_body": "{\"id\":\"bidResponseId6\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode6\",\"price\":5.78,\"adm\":\"adm6\",\"crid\":\"crid6\",\"dealid\":\"dealId6\",\"w\":300,\"h\":250}],\"seat\":\"seatId6\",\"group\":0}]}", "status_code": 200 } diff --git a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-pulsepoint-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-pulsepoint-bid-request-1.json index 04973cac104..4157690bb13 100644 --- a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-pulsepoint-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-pulsepoint-bid-request-1.json @@ -34,12 +34,7 @@ "user": { "buyeruid": "PP-UID", "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-appnexus-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-appnexus-bid-request-1.json index 5acca6e19ea..18f60cc35b3 100644 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-appnexus-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-appnexus-bid-request-1.json @@ -46,12 +46,7 @@ "id": "12345", "buyeruid": "12345", "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-request.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-request.json index 0ec5c77e558..74c8b18ab48 100644 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-request.json +++ b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-request.json @@ -158,12 +158,7 @@ }, "user": { "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-response.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-response.json index 0d599bcb5e4..0fe317b0ee1 100644 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-response.json +++ b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-response.json @@ -9,7 +9,7 @@ "debug": [ { "request_uri": "{{ appnexus.exchange_uri }}?member_id=member1", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode4\",\"video\":{\"mimes\":[\"mimes\"],\"minduration\":20,\"maxduration\":60,\"protocols\":[1],\"w\":300,\"h\":250,\"startdelay\":5,\"playbackmethod\":[1]},\"tagid\":\"invCode1\",\"bidfloor\":1.0,\"ext\":{\"appnexus\":{\"placement_id\":9848285,\"keywords\":\"k1=v1,k1=v2\",\"traffic_source_code\":\"trafficSourceCode1\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"id\":\"12345\",\"buyeruid\":\"12345\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", + "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode4\",\"video\":{\"mimes\":[\"mimes\"],\"minduration\":20,\"maxduration\":60,\"protocols\":[1],\"w\":300,\"h\":250,\"startdelay\":5,\"playbackmethod\":[1]},\"tagid\":\"invCode1\",\"bidfloor\":1.0,\"ext\":{\"appnexus\":{\"placement_id\":9848285,\"keywords\":\"k1=v1,k1=v2\",\"traffic_source_code\":\"trafficSourceCode1\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"id\":\"12345\",\"buyeruid\":\"12345\",\"ext\":{\"consent\":\"consent1\"}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", "response_body": "{\"id\":\"bidResponseId4\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode4\",\"price\":5.78,\"adm\":\"adm4\",\"crid\":\"crid4\",\"dealid\":\"dealId4\",\"w\":300,\"h\":250,\"ext\":{\"appnexus\":{\"bid_ad_type\":1}}}],\"seat\":\"seatId4\",\"group\":0}]}", "status_code": 200 } diff --git a/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-request.json b/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-request.json index 930ae4cceb0..2a7a016a8f4 100644 --- a/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-request.json +++ b/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-request.json @@ -50,12 +50,7 @@ }, "user": { "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-response.json b/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-response.json index b08299832c8..18e143da014 100644 --- a/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-response.json +++ b/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-response.json @@ -9,7 +9,7 @@ "debug": [ { "request_uri": "{{ sovrn.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode11\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250},\"tagid\":\"tagId1\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"990011\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", + "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode11\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250},\"tagid\":\"tagId1\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"990011\",\"ext\":{\"consent\":\"consent1\"}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", "response_body": "{\"id\":\"bidResponseId11\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode11\",\"price\":5.78,\"adm\":\"adm11\",\"crid\":\"crid11\",\"dealid\":\"dealId11\",\"w\":300,\"h\":250}],\"seat\":\"seatId11\",\"group\":0}]}", "status_code": 200 } diff --git a/src/test/resources/org/prebid/server/it/auction/sovrn/test-sovrn-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/sovrn/test-sovrn-bid-request-1.json index 67edaac40d7..1ebe7176ae6 100644 --- a/src/test/resources/org/prebid/server/it/auction/sovrn/test-sovrn-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/auction/sovrn/test-sovrn-bid-request-1.json @@ -31,12 +31,7 @@ "user": { "buyeruid": "990011", "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } + "consent": "consent1" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-request.json index e88886add30..a067a8cfdf9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-request.json @@ -68,11 +68,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-request.json index f64a5a78722..77280d99817 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-request.json @@ -76,12 +76,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-request.json index 3073a20f66b..b6654b2c812 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-request.json @@ -86,12 +86,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-request.json index 9a0d61c74cb..13eac22e83d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-request.json @@ -31,12 +31,7 @@ "user": { "buyeruid": "AK-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-request.json index 181833b7898..78d11b91cb3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-request.json @@ -70,12 +70,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-1.json index cd339c41a1a..c72ef604770 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-1.json @@ -33,12 +33,7 @@ "user": { "buyeruid": "AK-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-2.json index 961361f1c38..c7438410403 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-2.json @@ -30,12 +30,7 @@ "user": { "buyeruid": "AK-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-request.json index b583dabf956..218846f482b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-request.json @@ -81,12 +81,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-request.json index 2eca793ae38..d913ab94f75 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-request.json @@ -64,12 +64,7 @@ "user": { "buyeruid": "AD-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-request.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-request.json index ea1a4d3eb51..55d16a5aecb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-request.json @@ -78,12 +78,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json index 8ee1468f868..516c7b029d8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json @@ -36,12 +36,7 @@ "user": { "buyeruid": "AP-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json index 2ab1c7860f2..798268fa198 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json @@ -69,12 +69,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-request.json index 8d461e8bb05..0bd175eb819 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-request.json @@ -39,12 +39,7 @@ "user": { "buyeruid": "AP-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-request.json index f2ab294b189..7323cac2769 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-request.json @@ -62,12 +62,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json index 59a7c4cba4e..c5624a8178b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json @@ -43,12 +43,7 @@ "user": { "buyeruid": "AT-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-request.json index 8f8ac568361..4cbb5cedbe7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-request.json @@ -65,12 +65,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-request.json index e1f0195bbb3..917caf35825 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-request.json @@ -38,12 +38,7 @@ "user": { "buyeruid": "AV-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-request.json b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-request.json index 00c7c183f0f..ae7d1bebaae 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-request.json @@ -70,12 +70,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-1.json index 7764d9a5dca..8b3218be6fc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-1.json @@ -40,12 +40,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-2.json index 1908c31cef2..4e5fc9dfbc8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-2.json @@ -41,12 +41,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-request.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-request.json index 641d7062bae..8579656425c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-request.json @@ -81,12 +81,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-request.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-request.json index 623d78352da..ba9e0eef62a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-request.json @@ -93,12 +93,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-1.json index 98ece0b09ac..3e10aa922ed 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-1.json @@ -40,12 +40,7 @@ "user": { "buyeruid": "BF-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-2.json index f5443122e4e..c412f204683 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-2.json @@ -39,12 +39,7 @@ "user": { "buyeruid": "BF-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/brightroll/test-auction-brightroll-request.json b/src/test/resources/org/prebid/server/it/openrtb2/brightroll/test-auction-brightroll-request.json index 04f41fb4842..f9c81a3f876 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/brightroll/test-auction-brightroll-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/brightroll/test-auction-brightroll-request.json @@ -64,12 +64,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/brightroll/test-brightroll-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/brightroll/test-brightroll-bid-request-1.json index ebdeef8734e..71c0a0389fe 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/brightroll/test-brightroll-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/brightroll/test-brightroll-bid-request-1.json @@ -52,11 +52,6 @@ "user": { "buyeruid": "BR-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-request.json b/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-request.json index 88fb155b0ab..3d4151a6b86 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-request.json @@ -65,12 +65,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-auction-conversant-request.json b/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-auction-conversant-request.json index 62ae5f8e6d5..efe8225764f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-auction-conversant-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-auction-conversant-request.json @@ -67,12 +67,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-conversant-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-conversant-bid-request.json index b172612ec7d..653a4b9d51e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-conversant-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/conversant/alias/test-conversant-bid-request.json @@ -44,12 +44,7 @@ "user": { "buyeruid": "CV-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-auction-conversant-request.json b/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-auction-conversant-request.json index 22bfe67895f..3ceceeb9a61 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-auction-conversant-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-auction-conversant-request.json @@ -79,12 +79,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-conversant-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-conversant-bid-request.json index 11a68f857ad..bf1337d38fc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-conversant-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/conversant/test-conversant-bid-request.json @@ -61,11 +61,6 @@ "user": { "buyeruid": "CV-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-request.json b/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-request.json index 47bc7bde079..68f0b7c91d3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-request.json @@ -83,12 +83,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-cpmstar-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-cpmstar-bid-request-1.json index b3d811045e6..a07e42f638d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-cpmstar-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-cpmstar-bid-request-1.json @@ -56,12 +56,7 @@ "user": { "buyeruid": "CS-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-request.json b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-request.json index ea69921948e..a3edebf4a84 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-request.json @@ -81,12 +81,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json index 2f25dda7869..66ddb8803ef 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json @@ -103,13 +103,13 @@ "datablocks": [ { "uri": "{{ datablocks.exchange_uri }}?sid=2", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId002\",\"video\":{\"mimes\":[\"video/mp4\"],\"w\":300,\"h\":250,\"pos\":1},\"ext\":{\"bidder\":{\"host\":\"localhost:8090\",\"sourceId\":2}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"DB-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId002\",\"video\":{\"mimes\":[\"video/mp4\"],\"w\":300,\"h\":250,\"pos\":1},\"ext\":{\"bidder\":{\"host\":\"localhost:8090\",\"sourceId\":2}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"DB-UID\",\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"bid002\",\"impid\":\"impId002\",\"price\":9.99,\"crid\":\"crid002\",\"cid\":\"cid002\",\"adomain\":[\"psacentral.org\"],\"h\":250,\"w\":300}],\"seat\":\"datablocks\"}]}", "status": 200 }, { "uri": "{{ datablocks.exchange_uri }}?sid=1", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId001\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}]},\"ext\":{\"bidder\":{\"host\":\"localhost:8090\",\"sourceId\":1}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"DB-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId001\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}]},\"ext\":{\"bidder\":{\"host\":\"localhost:8090\",\"sourceId\":1}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"DB-UID\",\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"bid001\",\"impid\":\"impId001\",\"price\":7.77,\"adid\":\"adid001\",\"crid\":\"crid001\",\"cid\":\"cid001\",\"adm\":\"adm001\",\"h\":250,\"w\":300}],\"seat\":\"datablocks\"}]}", "status": 200 } @@ -173,12 +173,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-datablocks-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-datablocks-bid-request-1.json index fda6ba6af81..d83e947f165 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-datablocks-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-datablocks-bid-request-1.json @@ -40,12 +40,7 @@ "user": { "buyeruid": "DB-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-datablocks-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-datablocks-bid-request-2.json index a6ed8c70eeb..0352ce9e783 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-datablocks-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-datablocks-bid-request-2.json @@ -40,12 +40,7 @@ "user": { "buyeruid": "DB-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-request.json index 01774409106..356d83d4fa6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-request.json @@ -74,14 +74,20 @@ "fd": 1, "tid": "tid" }, + "user": { "ext": { "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "eids": [ + { + "source": "adserver.org", + "uids": [ + { + "id": "id" + } + ] + } + ] } }, "regs": { @@ -123,4 +129,4 @@ "auctiontimestamp": 1000 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-dmx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-dmx-bid-request.json index df6f7d0fe44..444460c60f3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-dmx-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-dmx-bid-request.json @@ -71,11 +71,16 @@ "buyeruid": "DM-UID", "ext": { "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "eids": [ + { + "source": "adserver.org", + "uids": [ + { + "id": "id" + } + ] + } + ] } }, "at": 1, @@ -129,4 +134,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json index 7c9afda64d7..2b1bb045fc8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json @@ -73,11 +73,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json index 6c9e0d2158f..59cecf06f22 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json @@ -50,7 +50,7 @@ "emx_digital": [ { "uri": "{{ emx_digital.exchange_uri }}?t=1000&ts=2060541160", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0,\"ext\":{\"bidder\":{\"tagid\":\"25251\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0,\"ext\":{\"bidder\":{\"tagid\":\"25251\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"some_test_auction\",\"seatbid\":[{\"seat\":\"12356\",\"bid\":[{\"id\":\"uuid\",\"adm\":\"

\",\"impid\":\"uuid\",\"ttl\":300,\"crid\":\"94395500\",\"w\":300,\"price\":2.942808,\"adid\":\"94395500\",\"h\":250}]}],\"cur\":\"USD\"}", "status": 200 } @@ -104,11 +104,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-auction-engagebdr-request.json b/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-auction-engagebdr-request.json index 22d183ef720..ea9428f01b7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-auction-engagebdr-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-auction-engagebdr-request.json @@ -74,12 +74,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-engagebdr-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-engagebdr-bid-request-1.json index ea0cb1d65d6..1126f469c70 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-engagebdr-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-engagebdr-bid-request-1.json @@ -35,12 +35,7 @@ "user": { "buyeruid": "EG-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-engagebdr-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-engagebdr-bid-request-2.json index 1317bdc2578..6ffe58b473f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-engagebdr-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/engagebdr/test-engagebdr-bid-request-2.json @@ -39,12 +39,7 @@ "user": { "buyeruid": "EG-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-request.json b/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-request.json index aae85c34bbc..f9217cad610 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-request.json @@ -63,12 +63,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-auction-facebook-request.json b/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-auction-facebook-request.json index 3af581f91a6..c0133c4cab9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-auction-facebook-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-auction-facebook-request.json @@ -91,12 +91,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-1.json index 92524585708..b65befa9ae0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-1.json @@ -26,12 +26,7 @@ "user": { "buyeruid": "FB-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-2.json index 9a182f31169..91e8b766567 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-2.json @@ -30,12 +30,7 @@ "user": { "buyeruid": "FB-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-3.json b/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-3.json index 1a2952a99d4..6ebeb687006 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-3.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/facebook/test-facebook-bid-request-3.json @@ -26,11 +26,6 @@ "user": { "buyeruid": "FB-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-request.json b/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-request.json index ed18110b4b9..79472812715 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-request.json @@ -69,11 +69,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-request.json b/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-request.json index cbb9986d15f..ddcdd5b3e2c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-request.json @@ -81,12 +81,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-gamoshi-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-gamoshi-bid-request-1.json index a7d7e0f525b..e1af7fe1880 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-gamoshi-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-gamoshi-bid-request-1.json @@ -60,11 +60,6 @@ "user": { "buyeruid": "GM-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-request.json index e5dca974590..6ea2e260623 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-request.json @@ -62,12 +62,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/grid/test-grid-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/grid/test-grid-bid-request-1.json index ed007d4c297..59837eb72e4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/grid/test-grid-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/grid/test-grid-bid-request-1.json @@ -39,11 +39,6 @@ "user": { "buyeruid": "GRID-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json index f7a9dbc5f0a..380e5d38f4b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json @@ -105,12 +105,7 @@ "user": { "buyeruid" : "GUM-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request-1.json index 0a29b8e6729..7ff815c64da 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request-1.json @@ -84,12 +84,7 @@ "user": { "buyeruid" : "GUM-UID", "ext": { - "consent" : "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-request.json b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-request.json index 203fcea22a9..d511bb16b6f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-request.json @@ -66,12 +66,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request-1.json index a10b790fcad..09055b884ef 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request-1.json @@ -43,11 +43,6 @@ "user": { "buyeruid": "ID-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json index 0a033505885..649ff43cfc1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json @@ -68,12 +68,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-1.json index 8cb6f275e4f..01cfbd3b728 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-1.json @@ -42,11 +42,6 @@ "user": { "buyeruid": "IE-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-2.json index f0c314f2f37..f75ca1d4437 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-2.json @@ -42,11 +42,6 @@ "user": { "buyeruid": "IE-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-request.json b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-request.json index 5f14035ebc2..c23f9b8cbab 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-request.json @@ -93,12 +93,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-kidoz-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-kidoz-bid-request-1.json index 18ea5c6ace0..4642e4ee4c5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-kidoz-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-kidoz-bid-request-1.json @@ -40,12 +40,7 @@ "user": { "buyeruid": "KZ-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-kidoz-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-kidoz-bid-request-2.json index 163ea170416..1001ba1ae91 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-kidoz-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-kidoz-bid-request-2.json @@ -43,12 +43,7 @@ "user": { "buyeruid": "KZ-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json index 2120605003c..ce593722cb1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json @@ -69,12 +69,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json index e0660e9e4e3..06c3ccd4116 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json @@ -62,7 +62,7 @@ "kubient": [ { "uri": "{{ kubient.exchange_uri }}", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-banner-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":500,\"h\":400},\"ext\":{\"bidder\":{\"zoneid\":\"9042\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-banner-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":500,\"h\":400},\"ext\":{\"bidder\":{\"zoneid\":\"9042\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-banner-id\",\"price\":0.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"advertsite.com\"],\"cid\":\"772\",\"crid\":\"29681110\",\"h\":576,\"w\":1024}]}]}", "status": 200 } @@ -110,11 +110,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-kubient-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-kubient-bid-request-1.json index 22bb47d2aec..8776048d2f6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-kubient-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-kubient-bid-request-1.json @@ -40,11 +40,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-auction-lifestreet-request.json b/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-auction-lifestreet-request.json index ce8c289d1b2..6088fe8c279 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-auction-lifestreet-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-auction-lifestreet-request.json @@ -79,12 +79,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-lifestreet-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-lifestreet-bid-request-1.json index d09c7d601b3..4a2a5c7ad2f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-lifestreet-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-lifestreet-bid-request-1.json @@ -36,11 +36,6 @@ "user": { "buyeruid": "LS-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-lifestreet-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-lifestreet-bid-request-2.json index 2be68d0431f..ba3bfcd9393 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-lifestreet-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lifestreet/test-lifestreet-bid-request-2.json @@ -39,11 +39,6 @@ "user": { "buyeruid": "LS-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-request.json b/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-request.json index 7a914dac341..873ee9b8631 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-request.json @@ -62,12 +62,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-lockerdome-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-lockerdome-bid-request.json index dff9878bef9..76ec286f60b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-lockerdome-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-lockerdome-bid-request.json @@ -39,11 +39,6 @@ "user": { "buyeruid": "LD-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-request.json b/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-request.json index 2db8cc2a6df..fb335dd6c69 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-request.json @@ -77,12 +77,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-marsmedia-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-marsmedia-bid-request-1.json index 1e2bf2a5aac..4a0c0913910 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-marsmedia-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-marsmedia-bid-request-1.json @@ -56,11 +56,6 @@ "user": { "buyeruid": "MM-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-request.json index 7691fce4060..8b93a756fe1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-request.json @@ -69,12 +69,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-mgid-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-mgid-bid-request.json index be9a8a5654e..f301e145230 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-mgid-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-mgid-bid-request.json @@ -49,12 +49,7 @@ "user": { "buyeruid": "MGID-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nanointeractive/test-auction-nanointeractive-request.json b/src/test/resources/org/prebid/server/it/openrtb2/nanointeractive/test-auction-nanointeractive-request.json index 4399365e153..4a55209e63a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/nanointeractive/test-auction-nanointeractive-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/nanointeractive/test-auction-nanointeractive-request.json @@ -100,12 +100,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nanointeractive/test-nanointeractive-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/nanointeractive/test-nanointeractive-bid-request-1.json index 2cbb8090222..2934f223725 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/nanointeractive/test-nanointeractive-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/nanointeractive/test-nanointeractive-bid-request-1.json @@ -72,12 +72,7 @@ "user": { "buyeruid": "NI-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-request.json index da5a20c5084..0c573d26446 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-request.json @@ -135,12 +135,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-1.json index c765d75a28f..bd909861224 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-1.json @@ -63,11 +63,6 @@ "user": { "buyeruid": "OX-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-2.json index c12f7c9fb77..de95eb016a6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-2.json @@ -41,11 +41,6 @@ "user": { "buyeruid": "OX-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-3.json b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-3.json index 4b19cfe27a3..c5a2e86a49e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-3.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request-3.json @@ -41,11 +41,6 @@ "user": { "buyeruid":"OX-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json index 2799068aef5..3917bd648d5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json @@ -103,12 +103,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request-1.json index 4c77a806ed2..d4a5120aaf7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request-1.json @@ -54,11 +54,6 @@ "user": { "buyeruid": "PM-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-request.json index ce019eb761f..25141633587 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-request.json @@ -93,12 +93,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-1.json index c737950f3f4..4a4f9d894fe 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-1.json @@ -43,12 +43,7 @@ "user": { "buyeruid": "PN-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-2.json index 400020985b1..62e1d9d9061 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-2.json @@ -40,12 +40,7 @@ "user": { "buyeruid": "PN-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-3.json b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-3.json index c39bbdcc5c9..6a9591a6ce6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-3.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-pubnative-bid-request-3.json @@ -37,12 +37,7 @@ "user": { "buyeruid": "PN-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-request.json index dffa0f808f3..254d3589220 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-request.json @@ -86,12 +86,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-1.json index ec4ef146bd1..270c71276cb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-1.json @@ -65,11 +65,6 @@ "user": { "buyeruid": "PP-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rhythmone/test-auction-rhythmone-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rhythmone/test-auction-rhythmone-request.json index 8725408d37d..7efe918e2bb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rhythmone/test-auction-rhythmone-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rhythmone/test-auction-rhythmone-request.json @@ -81,12 +81,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rhythmone/test-rhythmone-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rhythmone/test-rhythmone-bid-request-1.json index be9655aa778..299b60feba6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rhythmone/test-rhythmone-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rhythmone/test-rhythmone-bid-request-1.json @@ -60,11 +60,6 @@ "user": { "buyeruid": "RO-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-request.json index 388106a0b1f..82b15a0bac1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-request.json @@ -63,12 +63,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-request-1.json index a7e8ecc0fda..c6f6e47a07c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-request-1.json @@ -37,12 +37,7 @@ "user": { "buyeruid": "RTBH-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json index b65c278477a..2484264b8a9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json @@ -278,10 +278,6 @@ "user": { "ext": { "consent": "BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA", - "digitrust": { - "id": "id", - "keyv": 123 - }, "eids": [ { "source": "adserver.org", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json index a7cea58a85a..1683448972d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json @@ -628,11 +628,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA", "eids": [ { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json index f85ecebe767..c91351042e8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json @@ -93,11 +93,6 @@ ] } ], - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json index 0237cfc3df6..c2c3eb80270 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json @@ -121,11 +121,6 @@ ] } ], - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-request.json index f6caca91bf3..2c6355f4429 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-request.json @@ -98,12 +98,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-request.json index 841e7745379..e52187cc0b5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-request.json @@ -107,12 +107,7 @@ "tmax": 5000, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-request.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-request.json index a7cdb7df279..7234ec3354a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-request.json @@ -116,12 +116,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-1.json index 077c83ddce5..acc41e62f2b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-1.json @@ -47,11 +47,6 @@ "user": { "buyeruid": "SM-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-2.json index d23c70e4ebf..78dae5aebaf 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-2.json @@ -38,11 +38,6 @@ "user": { "buyeruid": "SM-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-3.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-3.json index bca7fa68205..1abac39190a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-3.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-3.json @@ -31,11 +31,6 @@ "user": { "buyeruid": "SM-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-request.json index 4f8dddb44df..652df4c2d79 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-request.json @@ -77,12 +77,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-1.json index 51ed94815c8..51b5e0c2b3a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-1.json @@ -40,12 +40,7 @@ "user": { "buyeruid": "SB-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-2.json index bbb3df7405e..a4ac76b8a32 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-2.json @@ -39,12 +39,7 @@ "user": { "buyeruid": "SB-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-request.json index 77b496bb41e..3c9e555f284 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-request.json @@ -64,12 +64,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-request-1.json index 3e333fb2f90..ae826e0b486 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-request-1.json @@ -42,12 +42,7 @@ "user": { "buyeruid": "990011", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-request.json b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-request.json index 839add5a448..635962ae61b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-request.json @@ -80,12 +80,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-request.json index ddb69425960..5468f52bc99 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-request.json @@ -59,12 +59,7 @@ "user": { "buyeruid": "SCM-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json index 47492a49d71..a0dc82ec832 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json @@ -65,11 +65,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json index 89ee69285d3..df98570852c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json @@ -43,11 +43,6 @@ "user": { "buyeruid": "TX-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-request.json b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-request.json index f98e514064f..2699198f3e3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-request.json @@ -91,12 +91,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-request-1.json index 3fb7054b871..f55efa396a0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-request-1.json @@ -44,12 +44,7 @@ "user": { "buyeruid": "TL-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json index 8a2deca46c7..8a0ce522449 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json @@ -62,11 +62,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json index 5eb728e4cd9..493d9e71ecc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json @@ -40,11 +40,6 @@ "user": { "buyeruid": "TL-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json index 7653b419c44..96fac3d6d7d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json @@ -62,11 +62,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json index 00a6d4ec1c0..03c2a674e26 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json @@ -63,7 +63,7 @@ "triplelift_native": [ { "uri": "{{ triplelift_native.exchange_uri }}", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-id\",\"native\":{\"request\":\"{\\\"ver\\\":\\\"1.1\\\",\\\"context\\\":1,\\\"contextsubtype\\\":11,\\\"plcmttype\\\":4,\\\"plcmtcnt\\\":1,\\\"assets\\\":[{\\\"id\\\":0,\\\"required\\\":1,\\\"title\\\":{\\\"len\\\":500}}]}\"},\"tagid\":\"foo\",\"ext\":{\"bidder\":{\"inventoryCode\":\"foo\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"test\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"T\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-id\",\"native\":{\"request\":\"{\\\"ver\\\":\\\"1.1\\\",\\\"context\\\":1,\\\"contextsubtype\\\":11,\\\"plcmttype\\\":4,\\\"plcmtcnt\\\":1,\\\"assets\\\":[{\\\"id\\\":0,\\\"required\\\":1,\\\"title\\\":{\\\"len\\\":500}}]}\"},\"tagid\":\"foo\",\"ext\":{\"bidder\":{\"inventoryCode\":\"foo\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"test\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"T\",\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"test-request-id\",\"seatbid\":[{\"seat\":\"958\",\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-id\",\"price\":0.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"triplelift.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"h\":250,\"w\":300}]}],\"bidid\":\"5778926625248726496\",\"cur\":\"USD\"}", "status": 200 } @@ -104,11 +104,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json index 439b6474237..eeeb34ec3fb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json @@ -35,11 +35,6 @@ "user": { "buyeruid": "T", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json index 0e6d21179aa..2a3a461727d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json @@ -64,12 +64,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request-1.json index 95ee04bedbb..ff0d24bf988 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request-1.json @@ -41,12 +41,7 @@ "user": { "buyeruid": "TTX-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-request.json index 8317dba4939..331e6857f42 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-request.json @@ -73,12 +73,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-request.json index 6eb2dec557c..62747e3cc7c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-request.json @@ -40,12 +40,7 @@ "user": { "buyeruid": "UF-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json index 0805dd611c1..085bca4c46f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json @@ -78,12 +78,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-1.json index f7a7377760f..b3fcaf9705e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-1.json @@ -39,12 +39,7 @@ "user": { "buyeruid": "UR-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-2.json index 974bf822a1e..96889f14e2b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-2.json @@ -39,12 +39,7 @@ "user": { "buyeruid": "UR-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-request.json b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-request.json index c8226ede61c..55afe5c957c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-request.json @@ -92,12 +92,7 @@ "user": { "buyeruid": "VI-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-request-1.json index ec42d77106c..a59a418916d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-request-1.json @@ -58,12 +58,7 @@ "user": { "buyeruid": "VI-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-request.json b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-request.json index af571db17df..a7ea628e575 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-request.json @@ -63,12 +63,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-request-1.json index ce7df815bbc..50ae26dd237 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-request-1.json @@ -43,12 +43,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-request.json index a39a7602e77..a31f4b23855 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-request.json @@ -70,12 +70,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-request.json index fcee8a54f73..acd4d80c313 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-request.json @@ -47,12 +47,7 @@ "user": { "buyeruid": "VISX-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-request.json b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-request.json index c29f63cbb11..6e65e13fb4a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-request.json @@ -59,12 +59,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-request-1.json index 4437d138db1..7a9dede97f5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-request-1.json @@ -30,12 +30,7 @@ "user": { "buyeruid": "VR-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-request.json index 25cfb2c200d..5dd12d1e5b5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-request.json @@ -62,12 +62,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request-1.json index 2592271d1c0..456dec57676 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request-1.json @@ -37,12 +37,7 @@ "user": { "buyeruid": "YM-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-request.json index ce0f887b9df..dae78d61fbe 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-request.json @@ -62,12 +62,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-request.json index 97ac27767a4..2dd1d05c05b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-request.json @@ -41,12 +41,7 @@ "user": { "buyeruid": "YD-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json index 0f1f2f26a0c..be89aba4f07 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json @@ -91,12 +91,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json index 36ff7f81ffa..6b9f939a0b9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json @@ -103,13 +103,13 @@ "zeroclickfraud": [ { "uri": "{{ zeroclickfraud.exchange_uri }}?sid=2", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId002\",\"video\":{\"mimes\":[\"video/mp4\"],\"w\":300,\"h\":250,\"pos\":1},\"ext\":{\"bidder\":{\"host\":\"localhost:8090\",\"sourceId\":2}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"ZF-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"currency\":{\"rates\":{\"EUR\":{\"USD\":1.2406},\"USD\":{\"EUR\":0.811}}},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId002\",\"video\":{\"mimes\":[\"video/mp4\"],\"w\":300,\"h\":250,\"pos\":1},\"ext\":{\"bidder\":{\"host\":\"localhost:8090\",\"sourceId\":2}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"ZF-UID\",\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"currency\":{\"rates\":{\"EUR\":{\"USD\":1.2406},\"USD\":{\"EUR\":0.811}}},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"bid002\",\"impid\":\"impId002\",\"price\":9.99,\"crid\":\"crid002\",\"cid\":\"cid002\",\"adomain\":[\"psacentral.org\"],\"h\":250,\"w\":300}],\"seat\":\"zeroclickfraud\"}]}", "status": 200 }, { "uri": "{{ zeroclickfraud.exchange_uri }}?sid=1", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId001\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}]},\"ext\":{\"bidder\":{\"host\":\"localhost:8090\",\"sourceId\":1}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"ZF-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"currency\":{\"rates\":{\"EUR\":{\"USD\":1.2406},\"USD\":{\"EUR\":0.811}}},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId001\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}]},\"ext\":{\"bidder\":{\"host\":\"localhost:8090\",\"sourceId\":1}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"ZF-UID\",\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"currency\":{\"rates\":{\"EUR\":{\"USD\":1.2406},\"USD\":{\"EUR\":0.811}}},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"bid001\",\"impid\":\"impId001\",\"price\":7.77,\"adid\":\"adid001\",\"crid\":\"crid001\",\"cid\":\"cid001\",\"adm\":\"adm001\",\"h\":250,\"w\":300}],\"seat\":\"zeroclickfraud\"}]}", "status": 200 } @@ -173,12 +173,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-1.json index e60f0063394..d578b675833 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-1.json @@ -40,12 +40,7 @@ "user": { "buyeruid": "ZF-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-2.json index 3129406631f..871c2f75cd1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-2.json @@ -40,12 +40,7 @@ "user": { "buyeruid": "ZF-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, From 60bd9ca0c5d1b97b7e633de20330179675098cb7 Mon Sep 17 00:00:00 2001 From: Braslavskyi Andrii Date: Fri, 11 Dec 2020 14:20:01 +0200 Subject: [PATCH 057/129] Make glv id optional (#839) * Skip host vendor check for cookie sync and setuid --- .../server/handler/CookieSyncHandler.java | 26 +++++++++-- .../prebid/server/handler/SetuidHandler.java | 14 +++++- .../server/handler/CookieSyncHandlerTest.java | 44 ++++++++++++++++++- .../server/handler/SetuidHandlerTest.java | 40 ++++++++++++++++- 4 files changed, 115 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java index 30cdec79030..487f6e44d9a 100644 --- a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java +++ b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java @@ -29,6 +29,7 @@ import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; +import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.gdpr.model.TcfResponse; import org.prebid.server.privacy.model.Privacy; import org.prebid.server.privacy.model.PrivacyContext; @@ -96,7 +97,7 @@ public CookieSyncHandler(String externalUrl, this.activeBidders = activeBidders(bidderCatalog); this.tcfDefinerService = Objects.requireNonNull(tcfDefinerService); this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); - this.gdprHostVendorId = gdprHostVendorId; + this.gdprHostVendorId = validateHostVendorId(gdprHostVendorId); this.defaultCoopSync = defaultCoopSync; this.listOfCoopSyncBidders = CollectionUtils.isNotEmpty(listOfCoopSyncBidders) ? listOfCoopSyncBidders @@ -111,6 +112,13 @@ private static Set activeBidders(BidderCatalog bidderCatalog) { return bidderCatalog.names().stream().filter(bidderCatalog::isActive).collect(Collectors.toSet()); } + private static Integer validateHostVendorId(Integer gdprHostVendorId) { + if (gdprHostVendorId == null) { + logger.warn("gdpr.host-vendor-id not specified. Will skip host company GDPR checks"); + } + return gdprHostVendorId; + } + @Override public void handle(RoutingContext context) { metrics.updateCookieSyncRequestMetric(); @@ -165,9 +173,8 @@ public void handle(RoutingContext context) { .compose(account -> privacyEnforcementService.contextFromCookieSyncRequest( cookieSyncRequest, context.request(), account, timeout) .map(privacyContext -> Tuple2.of(account, privacyContext))) - .map((Tuple2 accountAndPrivacy) -> tcfDefinerService.resultForVendorIds( - vendorIds, accountAndPrivacy.getRight().getTcfContext()) - .compose(this::handleVendorIdResult) + .map((Tuple2 accountAndPrivacy) -> allowedForVendorId(vendorIds, + accountAndPrivacy.getRight().getTcfContext()) .compose(ignored -> tcfDefinerService.resultForBidderNames( biddersToSync, accountAndPrivacy.getRight().getTcfContext(), @@ -208,6 +215,17 @@ private Set biddersToSync(List requestBidders, Boolean requestCo return new HashSet<>(requestBidders); } + /** + * Returns failed future if vendor is not allowed for cookie sync. + * If host vendor id is null, host allowed to sync cookies. + */ + private Future allowedForVendorId(Set vendorIds, TcfContext tcfContext) { + return gdprHostVendorId != null + ? tcfDefinerService.resultForVendorIds(vendorIds, tcfContext) + .compose(this::handleVendorIdResult) + : Future.succeededFuture(); + } + private Set addAllCoopSyncBidders(List bidders) { final Set updatedBidders = listOfCoopSyncBidders.stream() .flatMap(Collection::stream) diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index 1419f2a6aa1..57ae761cb5c 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -72,7 +72,7 @@ public SetuidHandler(long defaultTimeout, this.applicationSettings = Objects.requireNonNull(applicationSettings); this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); this.tcfDefinerService = Objects.requireNonNull(tcfDefinerService); - this.gdprHostVendorId = gdprHostVendorId; + this.gdprHostVendorId = validateHostVendorId(gdprHostVendorId); this.analyticsReporter = Objects.requireNonNull(analyticsReporter); this.metrics = Objects.requireNonNull(metrics); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); @@ -84,6 +84,13 @@ public SetuidHandler(long defaultTimeout, .collect(Collectors.toSet()); } + private static Integer validateHostVendorId(Integer gdprHostVendorId) { + if (gdprHostVendorId == null) { + logger.warn("gdpr.host-vendor-id not specified. Will skip host company GDPR checks"); + } + return gdprHostVendorId; + } + @Override public void handle(RoutingContext context) { final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(context); @@ -106,6 +113,11 @@ public void handle(RoutingContext context) { return; } + if (gdprHostVendorId == null) { + respondWithCookie(context, cookieName, uidsCookie); + return; + } + final Set vendorIds = Collections.singleton(gdprHostVendorId); final String requestAccount = context.request().getParam(ACCOUNT_PARAM); final Timeout timeout = timeoutFactory.create(defaultTimeout); diff --git a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java index 17911e2959f..86a771f508c 100644 --- a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java @@ -625,6 +625,46 @@ public void shouldTolerateBiddersWithoutUsersyncUrl() throws IOException { assertThat(cookieSyncResponse).isEqualTo(CookieSyncResponse.of("ok", emptyList())); } + @Test + public void shouldSkipVendorHostCheckAndContinueWithBiddersCheckWhenHostVendorIdIsMissing() throws IOException { + // given + cookieSyncHandler = new CookieSyncHandler("http://external-url", 2000, uidsCookieService, applicationSettings, + bidderCatalog, tcfDefinerService, privacyEnforcementService, null, false, emptyList(), + analyticsReporter, metrics, timeoutFactory, jacksonMapper); + given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + Uids.builder().uids(singletonMap(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"))).build(), + jacksonMapper)); + + given(routingContext.getBody()) + .willReturn(givenRequestBody( + CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).build())); + + rubiconUsersyncer = new Usersyncer(RUBICON, "", null, null, null, false); + appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "", null, null, null, false); + givenUsersyncersReturningFamilyName(); + + given(bidderCatalog.isActive(RUBICON)).willReturn(true); + given(bidderCatalog.isActive(APPNEXUS)).willReturn(true); + + given(bidderCatalog.bidderInfoByName(APPNEXUS)) + .willReturn(BidderInfo.create(true, null, null, + null, null, 2, true, true, false)); + + givenTcfServiceReturningVendorIdResult(singleton(1)); + givenTcfServiceReturningBidderNamesResult(singleton(RUBICON)); + + // when + cookieSyncHandler.handle(routingContext); + + // then + verify(tcfDefinerService, never()).resultForVendorIds(anySet(), any()); + final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); + assertThat(cookieSyncResponse.getStatus()).isEqualTo("ok"); + assertThat(cookieSyncResponse.getBidderStatus()).hasSize(1) + .extracting(BidderUsersyncStatus::getBidder, BidderUsersyncStatus::getError) + .containsOnly(tuple(APPNEXUS, "Rejected by TCF")); + } + @Test public void shouldUpdateCookieSyncSetAndRejectByTcfMetricForEachRejectedAndSyncedBidder() { // given @@ -685,7 +725,7 @@ public void shouldUpdateCookieSyncMatchesMetricForEachAlreadySyncedBidder() { public void shouldRespondWithNoCookieStatusIfHostVendorRejectedByTcf() throws IOException { // given cookieSyncHandler = new CookieSyncHandler("http://external-url", 2000, uidsCookieService, applicationSettings, - bidderCatalog, tcfDefinerService, privacyEnforcementService, null, false, emptyList(), + bidderCatalog, tcfDefinerService, privacyEnforcementService, 1, false, emptyList(), analyticsReporter, metrics, timeoutFactory, jacksonMapper); given(uidsCookieService.parseFromRequest(any())) @@ -701,7 +741,7 @@ bidderCatalog, tcfDefinerService, privacyEnforcementService, null, false, emptyL given(bidderCatalog.isActive(RUBICON)).willReturn(true); given(bidderCatalog.isActive(APPNEXUS)).willReturn(true); - givenTcfServiceReturningVendorIdResult(singleton(1)); + givenTcfServiceReturningVendorIdResult(emptySet()); givenTcfServiceReturningBidderNamesResult(set(RUBICON, APPNEXUS)); // when diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index 9aff7e95951..5cfae4e917d 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -90,7 +90,7 @@ public class SetuidHandlerTest extends VertxTest { @Before public void setUp() { - final Map vendorIdToGdpr = singletonMap(null, + final Map vendorIdToGdpr = singletonMap(1, PrivacyEnforcementAction.allowAll()); given(privacyEnforcementService.contextFromSetuidRequest(any(), any(), any())) @@ -118,7 +118,7 @@ public void setUp() { bidderCatalog, privacyEnforcementService, tcfDefinerService, - null, + 1, analyticsReporter, metrics, timeoutFactory); @@ -452,6 +452,42 @@ public void shouldRespondWithCookieIfUserIsNotInGdprScope() throws IOException { assertThat(decodedUids.getUids().get(RUBICON).getUid()).isEqualTo("J5VLCWQP-26-CWFT"); } + @Test + public void shouldSkipTcfChecksAndRespondWithCookieIfHostVendorIdNotDefined() throws IOException { + // given + final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + setuidHandler = new SetuidHandler(2000, uidsCookieService, applicationSettings, + bidderCatalog, privacyEnforcementService, tcfDefinerService, null, analyticsReporter, metrics, + new TimeoutFactory(clock)); + given(tcfDefinerService.resultForVendorIds(anySet(), any())) + .willReturn(Future.succeededFuture(TcfResponse.of(false, emptyMap(), null))); + + given(uidsCookieService.parseFromRequest(any())) + .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); + + // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} + given(uidsCookieService.toCookie(any())).willReturn(Cookie + .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + + given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); + + given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + + // when + setuidHandler.handle(routingContext); + + // then + verify(tcfDefinerService, never()).resultForVendorIds(anySet(), any()); + verify(routingContext, never()).addCookie(any(Cookie.class)); + verify(httpResponse).end(); + + final String uidsCookie = getUidsCookie(); + final Uids decodedUids = decodeUids(uidsCookie); + assertThat(decodedUids.getUids()).hasSize(1); + assertThat(decodedUids.getUids().get(RUBICON).getUid()).isEqualTo("J5VLCWQP-26-CWFT"); + } + @Test public void shouldUpdateOptOutsMetricIfOptedOut() { // given From cbeaeead6dc389288b16e5e9a6be2f6a16336ca9 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Fri, 11 Dec 2020 16:15:47 +0200 Subject: [PATCH 058/129] Clean bidder integration tests JSONs (#1068) --- .../adman/test-adman-bid-request-1.json | 7 +- .../adman/test-adman-bid-request-2.json | 9 +- .../adman/test-auction-adman-request.json | 9 +- .../adocean/test-auction-adocean-request.json | 7 +- .../test-auction-adocean-response.json | 7 +- .../adtarget/test-adtarget-bid-request-1.json | 9 +- .../test-auction-adtarget-request.json | 9 +- .../openrtb2/aja/test-aja-bid-request-1.json | 120 +++++++------- .../openrtb2/aja/test-aja-bid-request-2.json | 122 +++++++------- .../aja/test-auction-aja-request.json | 9 +- .../aja/test-auction-aja-response.json | 150 +++++++++--------- .../openrtb2/aja/test-cache-aja-request.json | 50 +++--- .../avocet/test-auction-avocet-request.json | 11 +- .../avocet/test-auction-avocet-response.json | 11 +- .../avocet/test-avocet-bid-request-1.json | 13 +- .../beintoo/test-auction-beintoo-request.json | 11 +- .../test-auction-beintoo-response.json | 11 +- .../beintoo/test-beintoo-bid-request.json | 17 +- .../test-auction-emxdigital-request.json | 4 - .../test-auction-emxdigital-response.json | 6 +- .../test-emxdigital-bid-request.json | 4 - .../invibes/test-auction-invibes-request.json | 7 +- .../kubient/test-auction-kubient-request.json | 4 - .../test-auction-kubient-response.json | 6 +- .../kubient/test-kubient-bid-request-1.json | 4 - .../logicad/test-auction-logicad-request.json | 7 +- .../logicad/test-logicad-bid-request.json | 9 +- .../test-auction-lunamedia-request.json | 9 +- .../lunamedia/test-lunamedia-bid-request.json | 11 +- .../test-auction-mobilefuse-request.json | 7 +- .../test-mobilefuse-bid-request.json | 9 +- .../test-auction-ninthdecimal-request.json | 7 +- .../test-ninthdecimal-bid-request.json | 9 +- .../test-auction-orbidder-request.json | 9 +- .../orbidder/test-orbidder-bid-request.json | 9 +- .../test-auction-sharethrough-request.json | 4 - .../test-auction-sharethrough-response.json | 4 - .../test-auction-smartadserver-request.json | 11 +- .../test-auction-smartadserver-response.json | 11 +- .../test-smartadserver-bid-request-1.json | 11 +- ...est-auction-triplelift-native-request.json | 4 - ...st-auction-triplelift-native-response.json | 6 +- .../test-triplelift-native-bid-request.json | 4 - .../test-auction-yeahmobi-request.json | 9 +- .../yeahmobi/test-yeahmobi-bid-request.json | 7 +- .../test-auction-yieldlab-request.json | 7 +- .../test-auction-yieldlab-response.json | 7 +- 47 files changed, 268 insertions(+), 520 deletions(-) diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-1.json index df169930104..fffdc0d462a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-1.json @@ -40,12 +40,7 @@ "user": { "buyeruid": "AD-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-2.json index b65b6c6d8ff..46343b73e4a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-2.json @@ -39,12 +39,7 @@ "user": { "buyeruid": "AD-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, @@ -88,4 +83,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json index aac9638c266..4478ab51de1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json @@ -55,12 +55,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { @@ -90,4 +85,4 @@ "auctiontimestamp": 1000 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json index eef8d0cef8e..fd5e23cb665 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json @@ -79,11 +79,6 @@ "user": { "buyeruid": "AO-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, @@ -92,4 +87,4 @@ "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json index 567301ea3a0..bb50cf1fdd9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json @@ -105,11 +105,6 @@ "user": { "buyeruid": "AO-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, @@ -176,4 +171,4 @@ "auctiontimestamp": 1000 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-request-1.json index 5feb88fc600..dd5c18952c8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-request-1.json @@ -43,12 +43,7 @@ "user": { "buyeruid": "AD-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, @@ -92,4 +87,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-request.json index b3f96954db2..e541d4818b3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-request.json @@ -65,12 +65,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { @@ -78,4 +73,4 @@ "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-1.json index a22c9678a27..a8d33c18829 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-1.json @@ -1,99 +1,93 @@ { - "id":"tid", - "imp":[ + "id": "tid", + "imp": [ { - "id":"impId001", - "banner":{ - "format":[ + "id": "impId001", + "banner": { + "format": [ { - "w":300, - "h":250 + "w": 300, + "h": 250 } ] }, - "tagid":"test" + "tagid": "test" } ], - "site":{ - "domain":"example.com", - "page":"http://www.example.com", - "publisher":{ - "id":"publisherId" + "site": { + "domain": "example.com", + "page": "http://www.example.com", + "publisher": { + "id": "publisherId" }, - "ext":{ - "amp":0 + "ext": { + "amp": 0 } }, - "device":{ - "ua":"userAgent", - "dnt":2, - "ip":"193.168.244.1", - "pxratio":4.2, - "language":"en", - "ifa":"ifaId" + "device": { + "ua": "userAgent", + "dnt": 2, + "ip": "193.168.244.1", + "pxratio": 4.2, + "language": "en", + "ifa": "ifaId" }, - "user":{ - "buyeruid":"AJA-UID", - "ext":{ - "consent":"consentValue", - "digitrust":{ - "id":"id", - "keyv":123, - "pref":0 - } + "user": { + "buyeruid": "AJA-UID", + "ext": { + "consent": "consentValue" } }, - "at":1, - "tmax":5000, - "cur":[ + "at": 1, + "tmax": 5000, + "cur": [ "USD" ], - "source":{ - "fd":1, - "tid":"tid" + "source": { + "fd": 1, + "tid": "tid" }, - "regs":{ - "ext":{ - "gdpr":0 + "regs": { + "ext": { + "gdpr": 0 } }, - "ext":{ - "prebid":{ - "currency":{ - "rates":{ - "EUR":{ - "USD":1.2406 + "ext": { + "prebid": { + "currency": { + "rates": { + "EUR": { + "USD": 1.2406 }, - "USD":{ - "EUR":0.811 + "USD": { + "EUR": 0.811 } } }, - "targeting":{ - "pricegranularity":{ - "precision":2, - "ranges":[ + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ { - "max":20, - "increment":0.1 + "max": 20, + "increment": 0.1 } ] }, - "includewinners":true, - "includebidderkeys":true + "includewinners": true, + "includebidderkeys": true }, - "cache":{ - "bids":{ - + "cache": { + "bids": { }, - "vastxml":{ - "ttlseconds":120 + "vastxml": { + "ttlseconds": 120 } }, - "auctiontimestamp":1000, + "auctiontimestamp": 1000, "channel": { "name": "web" } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-2.json index 8dffa5581ca..7dd8fe7d4ef 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-2.json @@ -1,102 +1,96 @@ { - "id":"tid", - "imp":[ + "id": "tid", + "imp": [ { - "id":"impId002", - "video":{ - "mimes":[ + "id": "impId002", + "video": { + "mimes": [ "video/mp4" ], - "protocols":[ + "protocols": [ 2, 5 ], - "w":1024, - "h":576 + "w": 1024, + "h": 576 }, - "tagid":"aja-test" + "tagid": "aja-test" } ], - "site":{ - "domain":"example.com", - "page":"http://www.example.com", - "publisher":{ - "id":"publisherId" + "site": { + "domain": "example.com", + "page": "http://www.example.com", + "publisher": { + "id": "publisherId" }, - "ext":{ - "amp":0 + "ext": { + "amp": 0 } }, - "device":{ - "ua":"userAgent", - "dnt":2, - "ip":"193.168.244.1", - "pxratio":4.2, - "language":"en", - "ifa":"ifaId" + "device": { + "ua": "userAgent", + "dnt": 2, + "ip": "193.168.244.1", + "pxratio": 4.2, + "language": "en", + "ifa": "ifaId" }, - "user":{ - "buyeruid":"AJA-UID", - "ext":{ - "consent":"consentValue", - "digitrust":{ - "id":"id", - "keyv":123, - "pref":0 - } + "user": { + "buyeruid": "AJA-UID", + "ext": { + "consent": "consentValue" } }, - "at":1, - "tmax":5000, - "cur":[ + "at": 1, + "tmax": 5000, + "cur": [ "USD" ], - "source":{ - "fd":1, - "tid":"tid" + "source": { + "fd": 1, + "tid": "tid" }, - "regs":{ - "ext":{ - "gdpr":0 + "regs": { + "ext": { + "gdpr": 0 } }, - "ext":{ - "prebid":{ - "currency":{ - "rates":{ - "EUR":{ - "USD":1.2406 + "ext": { + "prebid": { + "currency": { + "rates": { + "EUR": { + "USD": 1.2406 }, - "USD":{ - "EUR":0.811 + "USD": { + "EUR": 0.811 } } }, - "targeting":{ - "pricegranularity":{ - "precision":2, - "ranges":[ + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ { - "max":20, - "increment":0.1 + "max": 20, + "increment": 0.1 } ] }, - "includewinners":true, - "includebidderkeys":true + "includewinners": true, + "includebidderkeys": true }, - "cache":{ - "bids":{ - + "cache": { + "bids": { }, - "vastxml":{ - "ttlseconds":120 + "vastxml": { + "ttlseconds": 120 } }, - "auctiontimestamp":1000, + "auctiontimestamp": 1000, "channel": { "name": "web" } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-request.json index 8461aa2d80e..bc0813c5e24 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-request.json @@ -91,12 +91,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { @@ -104,4 +99,4 @@ "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json index b53a2378636..e1bb13841f5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json @@ -1,102 +1,102 @@ { - "id":"tid", - "seatbid":[ + "id": "tid", + "seatbid": [ { - "bid":[ + "bid": [ { - "id":"bid002", - "impid":"impId002", - "price":9.99, - "adomain":[ + "id": "bid002", + "impid": "impId002", + "price": 9.99, + "adomain": [ "psacentral.org" ], - "cid":"cid002", - "crid":"crid002", - "w":300, - "h":250, + "cid": "cid002", + "crid": "crid002", + "w": 300, + "h": 250, "exp": 120, - "ext":{ - "prebid":{ - "type":"video", - "targeting":{ - "hb_cache_path_aja":"{{ cache.path }}", - "hb_cache_host_aja":"{{ cache.host }}", - "hb_bidder_aja":"aja", - "hb_cache_id":"e7965b2e-0aa3-4252-a22c-580ed010e619", - "hb_cache_id_aja":"e7965b2e-0aa3-4252-a22c-580ed010e619", - "hb_pb_aja":"9.90", - "hb_pb":"9.90", - "hb_uuid_aja":"44a52b06-b29f-4819-a05f-db36b9e7b8fc", - "hb_cache_path":"{{ cache.path }}", - "hb_size_aja":"300x250", - "hb_uuid":"44a52b06-b29f-4819-a05f-db36b9e7b8fc", - "hb_size":"300x250", - "hb_bidder":"aja", - "hb_cache_host":"{{ cache.host }}" + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_cache_path_aja": "{{ cache.path }}", + "hb_cache_host_aja": "{{ cache.host }}", + "hb_bidder_aja": "aja", + "hb_cache_id": "e7965b2e-0aa3-4252-a22c-580ed010e619", + "hb_cache_id_aja": "e7965b2e-0aa3-4252-a22c-580ed010e619", + "hb_pb_aja": "9.90", + "hb_pb": "9.90", + "hb_uuid_aja": "44a52b06-b29f-4819-a05f-db36b9e7b8fc", + "hb_cache_path": "{{ cache.path }}", + "hb_size_aja": "300x250", + "hb_uuid": "44a52b06-b29f-4819-a05f-db36b9e7b8fc", + "hb_size": "300x250", + "hb_bidder": "aja", + "hb_cache_host": "{{ cache.host }}" }, - "cache":{ - "bids":{ - "url":"{{ cache.resource_url }}e7965b2e-0aa3-4252-a22c-580ed010e619", - "cacheId":"e7965b2e-0aa3-4252-a22c-580ed010e619" + "cache": { + "bids": { + "url": "{{ cache.resource_url }}e7965b2e-0aa3-4252-a22c-580ed010e619", + "cacheId": "e7965b2e-0aa3-4252-a22c-580ed010e619" }, - "vastXml":{ - "url":"{{ cache.resource_url }}44a52b06-b29f-4819-a05f-db36b9e7b8fc", - "cacheId":"44a52b06-b29f-4819-a05f-db36b9e7b8fc" + "vastXml": { + "url": "{{ cache.resource_url }}44a52b06-b29f-4819-a05f-db36b9e7b8fc", + "cacheId": "44a52b06-b29f-4819-a05f-db36b9e7b8fc" } } } } }, { - "id":"bid001", - "impid":"impId001", - "price":3.33, - "adm":"adm001", - "adid":"adid001", - "cid":"cid001", - "crid":"crid001", - "w":300, - "h":250, - "ext":{ - "prebid":{ - "type":"banner", - "targeting":{ - "hb_pb_aja":"3.30", - "hb_pb":"3.30", - "hb_cache_path_aja":"{{ cache.path }}", - "hb_cache_path":"{{ cache.path }}", - "hb_size_aja":"300x250", - "hb_size":"300x250", - "hb_cache_host_aja":"{{ cache.host }}", - "hb_bidder_aja":"aja", - "hb_bidder":"aja", - "hb_cache_id":"f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_host":"{{ cache.host }}", - "hb_cache_id_aja":"f0ab9105-cb21-4e59-b433-70f5ad6671cb" + "id": "bid001", + "impid": "impId001", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_pb_aja": "3.30", + "hb_pb": "3.30", + "hb_cache_path_aja": "{{ cache.path }}", + "hb_cache_path": "{{ cache.path }}", + "hb_size_aja": "300x250", + "hb_size": "300x250", + "hb_cache_host_aja": "{{ cache.host }}", + "hb_bidder_aja": "aja", + "hb_bidder": "aja", + "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", + "hb_cache_host": "{{ cache.host }}", + "hb_cache_id_aja": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" }, - "cache":{ - "bids":{ - "url":"{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "cacheId":"f0ab9105-cb21-4e59-b433-70f5ad6671cb" + "cache": { + "bids": { + "url": "{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb", + "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" } } } } } ], - "seat":"aja", - "group":0 + "seat": "aja", + "group": 0 } ], - "cur":"USD", - "ext":{ - "responsetimemillis":{ - "cache":"{{ cache.response_time_ms }}", - "aja":"{{ aja.response_time_ms }}" + "cur": "USD", + "ext": { + "responsetimemillis": { + "cache": "{{ cache.response_time_ms }}", + "aja": "{{ aja.response_time_ms }}" }, - "tmaxrequest":5000, - "prebid":{ - "auctiontimestamp":1000 + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 1000 } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-cache-aja-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-cache-aja-request.json index 5bb0050c74f..9d4855a6222 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-cache-aja-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-cache-aja-request.json @@ -1,38 +1,38 @@ { - "puts":[ + "puts": [ { - "type":"json", - "value":{ - "id":"bid001", - "impid":"impId001", - "price":3.33, - "adm":"adm001", - "adid":"adid001", - "cid":"cid001", - "crid":"crid001", - "w":300, - "h":250 + "type": "json", + "value": { + "id": "bid001", + "impid": "impId001", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250 } }, { - "type":"json", - "value":{ - "id":"bid002", - "impid":"impId002", - "price":9.99, - "adomain":[ + "type": "json", + "value": { + "id": "bid002", + "impid": "impId002", + "price": 9.99, + "adomain": [ "psacentral.org" ], - "cid":"cid002", - "crid":"crid002", - "w":300, - "h":250 + "cid": "cid002", + "crid": "crid002", + "w": 300, + "h": 250 } }, { - "type":"xml", - "value":"prebid.org wrapper", - "expiry":120 + "type": "xml", + "value": "prebid.org wrapper", + "expiry": 120 } ] } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json index c5033bf45c5..7f817cd6eb4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json @@ -43,10 +43,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, @@ -69,12 +65,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json index b3514e3d759..c758d0c5d95 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json @@ -68,7 +68,7 @@ "avocet": [ { "uri": "{{ avocet.exchange_uri }}", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-banner-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":500,\"h\":400},\"ext\":{\"bidder\":{\"placement\":\"5ea9601ac865f911007f1b6a\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"AV-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-banner-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":500,\"h\":400},\"ext\":{\"bidder\":{\"placement\":\"5ea9601ac865f911007f1b6a\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"AV-UID\",\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-banner-id\",\"price\":0.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"advertsite.com\"],\"cid\":\"772\",\"crid\":\"29681110\",\"h\":576,\"w\":1024,\"api\":1,\"ext\":{\"avocet\":{\"duration\":30}}}]}]}", "status": 200 } @@ -116,11 +116,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, @@ -141,10 +136,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-request-1.json index 9b30034e435..db3cb3b5396 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-request-1.json @@ -39,13 +39,8 @@ "ifa": "ifaId" }, "user": { - "buyeruid" : "AV-UID", + "buyeruid": "AV-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, @@ -66,10 +61,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, @@ -95,4 +86,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json index 279bf8e18fd..4cf9ae64ba2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json @@ -24,7 +24,7 @@ "language": "en", "ifa": "ifaId", "ua": "Android Chrome/60", - "ip" : "127.0.0.1" + "ip": "127.0.0.1" }, "site": { "page": "http://www.example.com", @@ -47,10 +47,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, @@ -73,11 +69,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json index 8e4cb308a7e..0e159e53c0e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json @@ -50,7 +50,7 @@ "beintoo": [ { "uri": "{{ beintoo.exchange_uri }}", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"BT-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"BT-UID\",\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"some_test_auction\",\"seatbid\":[{\"seat\":\"12356\",\"bid\":[{\"id\":\"uuid\",\"adm\":\"
\",\"impid\":\"uuid\",\"ttl\":300,\"crid\":\"94395500\",\"w\":300,\"price\":2.942808,\"adid\":\"94395500\",\"h\":250}]}],\"cur\":\"USD\"}", "status": 200 } @@ -104,11 +104,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, @@ -129,10 +124,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-request.json index 23914b4b674..7948350ea6e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-request.json @@ -31,14 +31,9 @@ "ifa": "ifaId" }, "user": { - "buyeruid" : "BT-UID", - "ext" : { - "consent" : "consentValue", - "digitrust" : { - "id" : "id", - "keyv" : 123, - "pref" : 0 - } + "buyeruid": "BT-UID", + "ext": { + "consent": "consentValue" } }, "at": 1, @@ -58,10 +53,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, @@ -87,4 +78,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json index 2b1bb045fc8..9f0db45cbbc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json @@ -47,10 +47,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json index 59cecf06f22..1eb31ec7cf8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json @@ -50,7 +50,7 @@ "emx_digital": [ { "uri": "{{ emx_digital.exchange_uri }}?t=1000&ts=2060541160", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0,\"ext\":{\"bidder\":{\"tagid\":\"25251\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0,\"ext\":{\"bidder\":{\"tagid\":\"25251\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"some_test_auction\",\"seatbid\":[{\"seat\":\"12356\",\"bid\":[{\"id\":\"uuid\",\"adm\":\"
\",\"impid\":\"uuid\",\"ttl\":300,\"crid\":\"94395500\",\"w\":300,\"price\":2.942808,\"adid\":\"94395500\",\"h\":250}]}],\"cur\":\"USD\"}", "status": 200 } @@ -124,10 +124,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-emxdigital-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-emxdigital-bid-request.json index cb4802104a4..8b819c1bb3e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-emxdigital-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-emxdigital-bid-request.json @@ -46,10 +46,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-request.json b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-request.json index 77da3269f4f..ac19fc93b16 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-request.json @@ -53,12 +53,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json index ce593722cb1..a2d90139b66 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json @@ -43,10 +43,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json index 06c3ccd4116..22af329541c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json @@ -62,7 +62,7 @@ "kubient": [ { "uri": "{{ kubient.exchange_uri }}", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-banner-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":500,\"h\":400},\"ext\":{\"bidder\":{\"zoneid\":\"9042\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-banner-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":500,\"h\":400},\"ext\":{\"bidder\":{\"zoneid\":\"9042\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-banner-id\",\"price\":0.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"advertsite.com\"],\"cid\":\"772\",\"crid\":\"29681110\",\"h\":576,\"w\":1024}]}]}", "status": 200 } @@ -130,10 +130,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-kubient-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-kubient-bid-request-1.json index 8776048d2f6..30fc51dafc9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-kubient-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-kubient-bid-request-1.json @@ -60,10 +60,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-request.json b/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-request.json index af7b053eeae..19c76cc4529 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-request.json @@ -69,12 +69,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-logicad-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-logicad-bid-request.json index 1d60d5783ab..9e7a534c4f9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-logicad-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-logicad-bid-request.json @@ -41,12 +41,7 @@ "user": { "buyeruid": "LC-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, @@ -90,4 +85,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-request.json b/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-request.json index 38d001f51c7..f3322195185 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-request.json @@ -70,12 +70,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { @@ -83,4 +78,4 @@ "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-lunamedia-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-lunamedia-bid-request.json index d49a9b375c4..4547b4f6393 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-lunamedia-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-lunamedia-bid-request.json @@ -36,14 +36,9 @@ "ifa": "ifaId" }, "user": { - "buyeruid" : "LM-UID", + "buyeruid": "LM-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, @@ -87,4 +82,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-request.json index 6b0cae4bf6f..326188434f0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-request.json @@ -50,12 +50,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json index 225a0e57171..31b5c38798c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json @@ -35,12 +35,7 @@ "user": { "buyeruid": "MF-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, @@ -94,4 +89,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ninthdecimal/test-auction-ninthdecimal-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ninthdecimal/test-auction-ninthdecimal-request.json index dae0737f694..d83bea50931 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ninthdecimal/test-auction-ninthdecimal-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ninthdecimal/test-auction-ninthdecimal-request.json @@ -70,12 +70,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ninthdecimal/test-ninthdecimal-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ninthdecimal/test-ninthdecimal-bid-request.json index c262def4797..bc2e18078f7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ninthdecimal/test-ninthdecimal-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ninthdecimal/test-ninthdecimal-bid-request.json @@ -38,12 +38,7 @@ "user": { "buyeruid": "ND-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, @@ -87,4 +82,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-request.json b/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-request.json index ae6c601f277..583a41dedde 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-request.json @@ -50,12 +50,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { @@ -87,4 +82,4 @@ "auctiontimestamp": 1000 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-orbidder-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-orbidder-bid-request.json index ec68a1df402..78558170715 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-orbidder-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-orbidder-bid-request.json @@ -42,12 +42,7 @@ "user": { "buyeruid": "OB-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, @@ -91,4 +86,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json index c91351042e8..073016adb28 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json @@ -57,10 +57,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json index c2c3eb80270..e86ac798d08 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json @@ -146,10 +146,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json index 7de3994683c..34abff9aca8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json @@ -46,10 +46,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, @@ -72,12 +68,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json index 62365b0bbe3..f53c1f4dcc4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json @@ -62,7 +62,7 @@ "smartadserver": [ { "uri": "{{ smartadserver.exchange_uri }}/api/bid?callerId=5", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-banner-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":500,\"h\":400},\"ext\":{\"bidder\":{\"siteId\":1,\"pageId\":2,\"formatId\":3,\"networkId\":73}}}],\"site\":{\"publisher\":{\"id\":\"73\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"SA-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-banner-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":500,\"h\":400},\"ext\":{\"bidder\":{\"siteId\":1,\"pageId\":2,\"formatId\":3,\"networkId\":73}}}],\"site\":{\"publisher\":{\"id\":\"73\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"SA-UID\",\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-banner-id\",\"price\":0.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"advertsite.com\"],\"cid\":\"772\",\"crid\":\"29681110\",\"h\":576,\"w\":1024}]}]}", "status": 200 } @@ -113,11 +113,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, @@ -138,10 +133,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request-1.json index 115e58fa4b0..d44e20c3b68 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request-1.json @@ -39,11 +39,6 @@ "user": { "buyeruid" : "SA-UID", "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, @@ -64,10 +59,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, @@ -93,4 +84,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json index 96fac3d6d7d..c3c161d62aa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json @@ -36,10 +36,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json index 03c2a674e26..570aefa9feb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json @@ -63,7 +63,7 @@ "triplelift_native": [ { "uri": "{{ triplelift_native.exchange_uri }}", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-id\",\"native\":{\"request\":\"{\\\"ver\\\":\\\"1.1\\\",\\\"context\\\":1,\\\"contextsubtype\\\":11,\\\"plcmttype\\\":4,\\\"plcmtcnt\\\":1,\\\"assets\\\":[{\\\"id\\\":0,\\\"required\\\":1,\\\"title\\\":{\\\"len\\\":500}}]}\"},\"tagid\":\"foo\",\"ext\":{\"bidder\":{\"inventoryCode\":\"foo\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"test\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"T\",\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-id\",\"native\":{\"request\":\"{\\\"ver\\\":\\\"1.1\\\",\\\"context\\\":1,\\\"contextsubtype\\\":11,\\\"plcmttype\\\":4,\\\"plcmtcnt\\\":1,\\\"assets\\\":[{\\\"id\\\":0,\\\"required\\\":1,\\\"title\\\":{\\\"len\\\":500}}]}\"},\"tagid\":\"foo\",\"ext\":{\"bidder\":{\"inventoryCode\":\"foo\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"test\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"T\",\"ext\":{\"consent\":\"consentValue\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"test-request-id\",\"seatbid\":[{\"seat\":\"958\",\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-id\",\"price\":0.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"triplelift.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"h\":250,\"w\":300}]}],\"bidid\":\"5778926625248726496\",\"cur\":\"USD\"}", "status": 200 } @@ -124,10 +124,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json index eeeb34ec3fb..d4a22534d32 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json @@ -55,10 +55,6 @@ "ext": { "prebid": { "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, "targeting": { "pricegranularity": { "precision": 2, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-request.json index ee088e56217..354d2be8365 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-request.json @@ -53,12 +53,7 @@ }, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "regs": { @@ -100,4 +95,4 @@ "auctiontimestamp": 1000 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-request.json index 4ff2144a2bc..b94c172d60b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-request.json @@ -44,12 +44,7 @@ "user": { "buyeruid": "YM-UID", "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } }, "at": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json index 1b1f1059fed..c53a1e56cb3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json @@ -82,11 +82,6 @@ }, "user": { "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, "consent": "consentValue" } }, @@ -95,4 +90,4 @@ "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json index 4b56af12471..28b402fc0f6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json @@ -164,12 +164,7 @@ "tmax": 5000, "user": { "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } + "consent": "consentValue" } } } From 147fb2a358415ec8d482bf53aade4876e7488c7b Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Fri, 11 Dec 2020 16:27:38 +0200 Subject: [PATCH 059/129] Add removing of all duplicated bids (#821) * Add removing of all duplicated bids * Minor test refactoring * Minor test refactoring * No reason for removing if only one bid was responded --- .../server/auction/BidResponseCreator.java | 11 ++- .../auction/BidResponseCreatorTest.java | 75 ++++++++++++------- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index faef570fc6f..aff02669f58 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -280,14 +280,19 @@ private ExtBidResponse toExtBidResponse(List bidderResponses, private static void removeRedundantBids(BidderResponse bidderResponse) { final List responseBidderBids = bidderResponse.getSeatBid().getBids(); + if (responseBidderBids.size() < 2) { // no reason for removing if only one bid was responded + return; + } + final Map> impIdToBidderBid = responseBidderBids.stream() .collect(Collectors.groupingBy(bidderBid -> bidderBid.getBid().getImpid())); - final List mostValuableBids = impIdToBidderBid.values().stream() + final Set mostValuableBids = impIdToBidderBid.values().stream() .map(BidResponseCreator::mostValuableBid) - .collect(Collectors.toList()); + .collect(Collectors.toSet()); - responseBidderBids.retainAll(mostValuableBids); + responseBidderBids.clear(); + responseBidderBids.addAll(mostValuableBids); } private static BidderBid mostValuableBid(List bidderBids) { diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 2bb19adfca4..424a3a8e578 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -73,7 +73,6 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -81,6 +80,7 @@ import java.util.Map; import java.util.UUID; import java.util.function.UnaryOperator; +import java.util.stream.Collectors; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; @@ -212,25 +212,29 @@ public void shouldRequestCacheServiceWithExpectedArguments() { bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false); // then - Map> biddersToCacheBidIds = new HashMap<>(); - biddersToCacheBidIds.put("bidder1", Arrays.asList("bidId1", "bidId2")); - biddersToCacheBidIds.put("bidder2", Arrays.asList("bidId3", "bidId4")); + ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); verify(cacheService).cacheBidsOpenrtb( argThat(t -> t.containsAll(asList(bid1, bid4, bid3, bid2))), same(auctionContext), - eq(CacheContext.builder() - .shouldCacheBids(true) - .shouldCacheVideoBids(true) - .cacheBidsTtl(99) - .cacheVideoBidsTtl(101) - .bidderToVideoBidIdsToModify(emptyMap()) - .bidderToBidIds(biddersToCacheBidIds) - .build()), + contextArgumentCaptor.capture(), eq(EventsContext.builder() .enabledForAccount(true) .enabledForRequest(true) .auctionTimestamp(1000L) .build())); + + Map> biddersToCacheBidIds = new HashMap<>(); + biddersToCacheBidIds.put("bidder1", asList("bidId1", "bidId2")); + biddersToCacheBidIds.put("bidder2", asList("bidId3", "bidId4")); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + assertThat(context.getCacheBidsTtl()).isEqualTo(99); + assertThat(context.getCacheVideoBidsTtl()).isEqualTo(101); + assertMapWithUnorderedList(context.getBidderToBidIds(), biddersToCacheBidIds); + assertThat(context.getBidderToVideoBidIdsToModify()).isEmpty(); + }); } @Test @@ -260,17 +264,21 @@ public void shouldRequestCacheServiceWithWinningBidsOnlyWhenWinningonlyIsTrue() bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false); // then - Map> biddersToCacheBidIds = new HashMap<>(); - biddersToCacheBidIds.put("bidder1", Arrays.asList("bidId1", "bidId2")); - biddersToCacheBidIds.put("bidder2", Arrays.asList("bidId3", "bidId4")); + ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); verify(cacheService).cacheBidsOpenrtb( - argThat(t -> t.containsAll(asList(bid1, bid2)) && t.size() == 2), + eq(asList(bid1, bid2)), same(auctionContext), - eq(CacheContext.builder() - .bidderToVideoBidIdsToModify(emptyMap()) - .bidderToBidIds(biddersToCacheBidIds) - .build()), + contextArgumentCaptor.capture(), eq(EventsContext.builder().auctionTimestamp(1000L).build())); + + Map> biddersToCacheBidIds = new HashMap<>(); + biddersToCacheBidIds.put("bidder1", asList("bidId1", "bidId2")); + biddersToCacheBidIds.put("bidder2", asList("bidId3", "bidId4")); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertMapWithUnorderedList(context.getBidderToBidIds(), biddersToCacheBidIds); + assertThat(context.getBidderToVideoBidIdsToModify()).isEmpty(); + }); } @Test @@ -541,6 +549,7 @@ public void shouldFilterByDealsAndPriceBidsWhenImpIdsAreEqual() { BidderBid.of(simpleBidImp1, banner, null), BidderBid.of(simpleBid1Imp2, banner, null), BidderBid.of(simpleBid2Imp2, banner, null), // will stay (top price) + BidderBid.of(simpleBid2Imp2, banner, null), // duplicate should be removed BidderBid.of(dealBid2Imp1, banner, null), // will stay (deal + topPrice) BidderBid.of(dealBid1Imp1, banner, null)); @@ -1688,7 +1697,7 @@ public void impToStoredVideoJsonShouldInjectStoredVideoWhenExtOptionsIsTrueAndVi final Bid bid1 = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build(); final Bid bid2 = Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.valueOf(2)).build(); final Bid bid3 = Bid.builder().id("bidId3").impid("impId3").price(BigDecimal.valueOf(3)).build(); - final List bidderBids = Arrays.asList( + final List bidderBids = mutableList( BidderBid.of(bid1, banner, "USD"), BidderBid.of(bid2, banner, "USD"), BidderBid.of(bid3, banner, "USD")); @@ -1705,7 +1714,7 @@ public void impToStoredVideoJsonShouldInjectStoredVideoWhenExtOptionsIsTrueAndVi bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false); // then - verify(storedRequestProcessor).videoStoredDataResult(any(), eq(Arrays.asList(imp1, imp3)), anyList(), + verify(storedRequestProcessor).videoStoredDataResult(any(), eq(asList(imp1, imp3)), anyList(), eq(timeout)); assertThat(result.result().getSeatbid()) @@ -1729,8 +1738,7 @@ public void impToStoredVideoJsonShouldAddErrorsWithPrebidBidderWhenStoredVideoRe final AuctionContext auctionContext = givenAuctionContext(bidRequest); final Bid bid1 = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build(); - final List bidderBids = singletonList( - BidderBid.of(bid1, banner, "USD")); + final List bidderBids = singletonList(BidderBid.of(bid1, banner, "USD")); final List bidderResponses = singletonList( BidderResponse.of("bidder1", BidderSeatBid.of(bidderBids, emptyList(), emptyList()), 100)); @@ -1789,8 +1797,7 @@ public void shouldProcessRequestAndAddErrorFromAuctionContext() { contextBuilder -> contextBuilder.prebidErrors(singletonList("privacy error"))); final Bid bid1 = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build(); - final List bidderBids = singletonList( - BidderBid.of(bid1, banner, "USD")); + final List bidderBids = singletonList(BidderBid.of(bid1, banner, "USD")); final List bidderResponses = singletonList( BidderResponse.of("bidder1", BidderSeatBid.of(bidderBids, emptyList(), emptyList()), 100)); @@ -1939,7 +1946,7 @@ private static BidRequest givenBidRequest(Imp... imps) { } private static BidderSeatBid givenSeatBid(BidderBid... bids) { - return BidderSeatBid.of(new ArrayList<>(asList(bids)), emptyList(), emptyList()); + return BidderSeatBid.of(mutableList(bids), emptyList(), emptyList()); } private static ExtRequestTargeting givenTargeting() { @@ -1965,4 +1972,18 @@ private static String toTargetingByKey(Bid bid, String targetingKey) { final Map targeting = toExtPrebid(bid.getExt()).getPrebid().getTargeting(); return targeting != null ? targeting.get(targetingKey) : null; } + + @SuppressWarnings("unchecked") + private static void assertMapWithUnorderedList(Map> map, Map> expectedMap) { + assertThat(map).hasSize(expectedMap.size()); + for (Map.Entry> keyToValues : expectedMap.entrySet()) { + final V[] values = (V[]) keyToValues.getValue().toArray(); + assertThat(expectedMap.get(keyToValues.getKey())).containsOnly(values); + } + } + + @SafeVarargs + private static List mutableList(T... values) { + return Arrays.stream(values).collect(Collectors.toList()); + } } From 7bf95f18907f6684f385c27d790845d20547f995 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Mon, 14 Dec 2020 14:40:34 +0200 Subject: [PATCH 060/129] Add an option to discard latest external currency rates after configured amount of time (#840) --- docs/config-app.md | 1 + .../currency/CurrencyConversionService.java | 28 +++++++++++++++---- .../spring/config/ServiceConfiguration.java | 3 ++ .../model/ExternalConversionProperties.java | 3 ++ .../CurrencyConversionServiceTest.java | 26 ++++++++--------- 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/docs/config-app.md b/docs/config-app.md index b88d090d892..68d60cd9ab6 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -135,6 +135,7 @@ But feel free to add additional bidder's specific options. - `currency-converter.external-rates.default-timeout-ms` - default operation timeout for fetching currency rates. - `currency-converter.external-rates.refresh-period-ms` - default refresh period for currency rates updates. - `currency-converter.external-rates.stale-after-ms` - how old currency rates should be to become considered stale. +- `currency-converter.external-rates.stale-period-ms` - stale period after which the latest external currency rates get discarded. ## Admin Endpoints - `admin-endpoints.version.enabled` - if equals to `true` the endpoint will be available. diff --git a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java index 43060faf4d0..5027fb6e447 100644 --- a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java +++ b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java @@ -84,7 +84,7 @@ private void populatesLatestCurrencyRates(String currencyServerUrl, Long default httpClient.get(currencyServerUrl, defaultTimeout) .map(this::processResponse) .map(this::updateCurrencyRates) - .recover(CurrencyConversionService::failResponse); + .otherwise(this::handleErrorResponse); } /** @@ -106,21 +106,37 @@ private CurrencyConversionRates processResponse(HttpClientResponse response) { } } - private CurrencyConversionRates updateCurrencyRates(CurrencyConversionRates currencyConversionRates) { + private Void updateCurrencyRates(CurrencyConversionRates currencyConversionRates) { final Map> receivedCurrencyRates = currencyConversionRates.getConversions(); if (receivedCurrencyRates != null) { externalCurrencyRates = receivedCurrencyRates; - lastUpdated = ZonedDateTime.now(externalConversionProperties.getClock()); + lastUpdated = now(); } - return currencyConversionRates; + + return null; } /** * Handles errors occurred while HTTP request or response processing. */ - private static Future failResponse(Throwable exception) { + private Void handleErrorResponse(Throwable exception) { logger.warn("Error occurred while request to currency service", exception); - return Future.failedFuture(exception); + + if (externalRatesAreStale()) { + externalCurrencyRates = null; + } + + return null; + } + + private boolean externalRatesAreStale() { + final Long stalePeriodMs = externalConversionProperties.getStalePeriodMs(); + + return stalePeriodMs != null && Duration.between(lastUpdated, now()).toMillis() > stalePeriodMs; + } + + private ZonedDateTime now() { + return ZonedDateTime.now(externalConversionProperties.getClock()); } public boolean isExternalRatesActive() { diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 1e9df6476d7..637a95eddf4 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -603,6 +603,7 @@ AmpResponsePostProcessor ampResponsePostProcessor() { @Bean CurrencyConversionService currencyConversionService( @Autowired(required = false) ExternalConversionProperties externalConversionProperties) { + return new CurrencyConversionService(externalConversionProperties); } @@ -613,6 +614,7 @@ ExternalConversionProperties externalConversionProperties( @Value("${currency-converter.external-rates.default-timeout-ms}") long defaultTimeoutMs, @Value("${currency-converter.external-rates.refresh-period-ms}") long refreshPeriodMs, @Value("${currency-converter.external-rates.stale-after-ms}") long staleAfterMs, + @Value("${currency-converter.external-rates.stale-period-ms:#{null}}") Long stalePeriodMs, Vertx vertx, HttpClient httpClient, Metrics metrics, @@ -624,6 +626,7 @@ ExternalConversionProperties externalConversionProperties( defaultTimeoutMs, refreshPeriodMs, staleAfterMs, + stalePeriodMs, vertx, httpClient, metrics, diff --git a/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java b/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java index fa97d533d73..b0206149f0d 100644 --- a/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java +++ b/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java @@ -32,6 +32,9 @@ public class ExternalConversionProperties { @NotNull Long staleAfterMs; + @Min(2) + Long stalePeriodMs; + @NotNull Vertx vertx; diff --git a/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java b/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java index 326bfff2149..23f2ea4368b 100644 --- a/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java +++ b/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java @@ -72,13 +72,13 @@ public void setUp() throws JsonProcessingException { givenHttpClientReturnsResponse(httpClient, 200, mapper.writeValueAsString(CurrencyConversionRates.of(null, currencyRates))); - currencyService = createInitializedService(URL, 1L, -3600L, vertx, httpClient, metrics, clock); + currencyService = createInitializedService(URL, 1L, -3600L, httpClient); } @Test public void creationShouldFailOnInvalidCurrencyServerUrl() { assertThatIllegalArgumentException() - .isThrownBy(() -> createInitializedService("invalid-url", 1L, -1L, vertx, httpClient, metrics, clock)) + .isThrownBy(() -> createInitializedService("invalid-url", 1L, -1L, httpClient)) .withMessage("URL supplied is not valid: invalid-url"); } @@ -101,7 +101,7 @@ public void currencyRatesGaugeShouldReportStale() { public void currencyRatesGaugeShouldReportNotStale() { // when metrics = mock(Metrics.class); // original mock is already spoiled by service initialization in setUp - currencyService = createInitializedService(URL, 1L, 3600L, vertx, httpClient, metrics, clock); + currencyService = createInitializedService(URL, 1L, 3600L, httpClient); // then final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(BooleanSupplier.class); @@ -283,7 +283,7 @@ public void convertCurrencyShouldThrowPrebidExceptionIfMultiplierWasNotFoundFrom givenHttpClientReturnsResponse(httpClient, 503, "server unavailable"); // when - currencyService = createInitializedService(URL, 1L, -1L, vertx, httpClient, metrics, clock); + currencyService = createInitializedService(URL, 1L, -1L, httpClient); // then assertThatExceptionOfType(PreBidException.class) @@ -298,7 +298,7 @@ public void convertCurrencyShouldThrowExceptionWhenCurrencyServerResponseStatusN givenHttpClientReturnsResponse(httpClient, 503, "server unavailable"); // when - currencyService = createInitializedService(URL, 1L, -1L, vertx, httpClient, metrics, clock); + currencyService = createInitializedService(URL, 1L, -1L, httpClient); // then assertThatExceptionOfType(PreBidException.class) @@ -312,7 +312,7 @@ public void convertCurrencyShouldThrowExceptionWhenCurrencyServerResponseContain givenHttpClientReturnsResponse(httpClient, 200, "{\"foo\": \"bar\"}"); // when - currencyService = createInitializedService(URL, 1L, -1L, vertx, httpClient, metrics, clock); + currencyService = createInitializedService(URL, 1L, -1L, httpClient); // then assertThatExceptionOfType(PreBidException.class) @@ -329,7 +329,7 @@ public void initializeShouldMakeOneInitialRequestAndTwoScheduled() { givenHttpClientReturnsResponse(httpClient, 200, "{\"foo\": \"bar\"}"); // when and then - currencyService = createInitializedService(URL, 1000, -1L, vertx, httpClient, metrics, clock); + currencyService = createInitializedService(URL, 1000, -1L, httpClient); final ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); verify(vertx).setPeriodic(eq(1000L), handlerCaptor.capture()); @@ -341,13 +341,10 @@ public void initializeShouldMakeOneInitialRequestAndTwoScheduled() { verify(httpClient, times(3)).get(anyString(), anyLong()); } - private static CurrencyConversionService createInitializedService(String url, - long refreshPeriod, - long staleAfter, - Vertx vertx, - HttpClient httpClient, - Metrics metrics, - Clock clock) { + private CurrencyConversionService createInitializedService(String url, + long refreshPeriod, + long staleAfter, + HttpClient httpClient) { final CurrencyConversionService currencyService = new CurrencyConversionService( new ExternalConversionProperties( @@ -355,6 +352,7 @@ private static CurrencyConversionService createInitializedService(String url, 1000L, refreshPeriod, staleAfter, + null, vertx, httpClient, metrics, From 8ba106133f6683174be5656e6772dd6444de0d03 Mon Sep 17 00:00:00 2001 From: Braslavskyi Andrii Date: Tue, 15 Dec 2020 14:46:23 +0200 Subject: [PATCH 061/129] include format targeting flag implemented (#845) * include format targeting flag implemented * Resolve tests after merging conflicts * Merge branch 'master' into kvp-hb-format-support # Conflicts: # src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json * Fixed pull request comments * Do not set default includeformat value to request * Remove test file and remove includeformat null check in Targeting creation * Remove includeformat from defaultTargeting creation condition * Use BooleanUtils.isTrue instead toBooleanDefaultIfNull --- docs/endpoints/openrtb2/auction.md | 3 + .../server/auction/AmpRequestFactory.java | 3 + .../server/auction/AuctionRequestFactory.java | 1 + .../server/auction/BidResponseCreator.java | 4 +- .../auction/TargetingKeywordsCreator.java | 21 ++++ .../prebid/server/handler/AuctionHandler.java | 2 +- .../ext/request/ExtRequestTargeting.java | 5 + .../proto/openrtb/ext/response/BidType.java | 6 +- .../server/auction/AmpRequestFactoryTest.java | 30 ++++- .../auction/BidResponseCreatorTest.java | 13 +- .../auction/TargetingKeywordsCreatorTest.java | 118 ++++++++++++------ 11 files changed, 157 insertions(+), 49 deletions(-) diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index 4c1356a1ba5..a48a9a99bd0 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -164,6 +164,7 @@ to set these params on the response at `response.seatbid[i].bid[j].ext.prebid.ta }, "includewinners": false, // Optional param defaulting to true "includebidderkeys": false // Optional param defaulting to true + "includeformat": false // Optional param defaulting to false } } } @@ -178,6 +179,8 @@ For backwards compatibility the following strings will also be allowed as price One of "includewinners" or "includebidderkeys" must be true (both default to true if unset). If both were false, then no targeting keys would be set, which is better configured by omitting targeting altogether. +The parameter "includeformat" indicates the type of the bid (banner, video, etc) for multiformat requests. It will add the key `hb_format` and/or `hb_format_{bidderName}` as per "includewinners" and "includebidderkeys" above. + MediaType PriceGranularity - when a single OpenRTB request contains multiple impressions with different mediatypes, or a single impression supports multiple formats, the different mediatypes may need different price granularities. If `mediatypepricegranularity` is present, `pricegranularity` would only be used for any mediatypes not specified. ``` diff --git a/src/main/java/org/prebid/server/auction/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/AmpRequestFactory.java index 4c8a45ade5a..4b7a3130c41 100644 --- a/src/main/java/org/prebid/server/auction/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AmpRequestFactory.java @@ -634,11 +634,14 @@ private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) final boolean includeBidderKeys = isTargetingNull || targeting.getIncludebidderkeys() == null || targeting.getIncludebidderkeys(); + final Boolean includeFormat = !isTargetingNull ? targeting.getIncludeformat() : null; + return ExtRequestTargeting.builder() .pricegranularity(outgoingPriceGranularityNode) .mediatypepricegranularity(mediaTypePriceGranularity) .includewinners(includeWinners) .includebidderkeys(includeBidderKeys) + .includeformat(includeFormat) .build(); } } diff --git a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java index 4564a844a34..c2e254738f0 100644 --- a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java @@ -570,6 +570,7 @@ private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, Set makeFor(Bid bid, boolean winningBid) { + final MediaType mediaType = bid.getMediaType(); return truncateKeys(makeFor( bid.getBidder(), winningBid, @@ -178,6 +192,7 @@ public Map makeFor(Bid bid, boolean winningBid) { bid.getHeight(), bid.getCacheId(), null, + mediaType != null ? mediaType.name() : null, bid.getDealId())); } @@ -188,6 +203,7 @@ Map makeFor(com.iab.openrtb.response.Bid bid, String bidder, boolean winningBid, String cacheId, + String format, String vastCacheId) { final Map keywords = makeFor( @@ -199,6 +215,7 @@ Map makeFor(com.iab.openrtb.response.Bid bid, bid.getH(), cacheId, vastCacheId, + format, bid.getDealid()); if (resolver == null) { @@ -222,6 +239,7 @@ private Map makeFor(String bidder, Integer height, String cacheId, String vastCacheId, + String format, String dealId) { final KeywordMap keywordMap = new KeywordMap(bidder, winningBid, includeWinners, includeBidderKeys, @@ -247,6 +265,9 @@ private Map makeFor(String bidder, keywordMap.put(HB_CACHE_HOST_KEY, cacheHost); keywordMap.put(HB_CACHE_PATH_KEY, cachePath); } + if (StringUtils.isNotBlank(format) && includeFormat) { + keywordMap.put(HB_FORMAT_KEY, format); + } if (StringUtils.isNotBlank(dealId)) { keywordMap.put(HB_DEAL_KEY, dealId); } diff --git a/src/main/java/org/prebid/server/handler/AuctionHandler.java b/src/main/java/org/prebid/server/handler/AuctionHandler.java index c79c8d28545..821e3314edc 100644 --- a/src/main/java/org/prebid/server/handler/AuctionHandler.java +++ b/src/main/java/org/prebid/server/handler/AuctionHandler.java @@ -400,7 +400,7 @@ private static PreBidResponse addTargetingKeywords(PreBidRequest preBidRequest, final Integer sortBids = preBidRequest.getSortBids(); if (sortBids != null && sortBids == 1) { final TargetingKeywordsCreator keywordsCreator = - TargetingKeywordsCreator.create(account.getPriceGranularity(), true, true, false, 0); + TargetingKeywordsCreator.create(account.getPriceGranularity(), true, true, false, false, 0); final Map> adUnitCodeToBids = preBidResponse.getBids().stream() .collect(Collectors.groupingBy(Bid::getCode)); diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java index 0099e1d9d94..b2eac627e62 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java @@ -44,6 +44,11 @@ public class ExtRequestTargeting { */ Boolean includebidderkeys; + /** + * Defines the contract for bidrequest.ext.prebid.targeting.includeformat + */ + Boolean includeformat; + /** * Defines the contract for bidrequest.ext.prebid.targeting.truncateattrchars */ diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/BidType.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/BidType.java index 161ec51a73a..f1143141538 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/BidType.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/BidType.java @@ -8,5 +8,9 @@ public enum BidType { video, audio, @JsonProperty("native") - xNative + xNative; + + public String getName() { + return this == xNative ? "native" : this.name(); + } } diff --git a/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java index 8f3e7bb64b6..864a85ce91b 100644 --- a/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java @@ -278,10 +278,8 @@ public void shouldReturnBidRequestWithDefaultIncludeWinnersIfStoredBidRequestExt .extracting(BidRequest::getExt).isNotNull() .extracting(ExtRequest::getPrebid) .extracting(ExtRequestPrebid::getTargeting) - .extracting(ExtRequestTargeting::getIncludewinners, ExtRequestTargeting::getPricegranularity) - // assert that includeWinners was set with default value and priceGranularity remained unchanged - .containsExactly( - tuple(true, mapper.createObjectNode().put("foo", "bar"))); + .extracting(ExtRequestTargeting::getIncludewinners) + .containsExactly(true); } @Test @@ -308,6 +306,30 @@ public void shouldReturnBidRequestWithIncludeWinnersFromStoredBidRequest() { .containsExactly(false); } + @Test + public void shouldReturnBidRequestWithIncludeFormatFromStoredBidRequest() { + // given + givenBidRequest( + builder -> builder + .ext(givenRequestExt( + ExtRequestTargeting.builder() + .pricegranularity(mapper.createObjectNode().put("foo", "bar")) + .includeformat(true) + .build())), + Imp.builder().build()); + + // when + final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest(); + + // then + assertThat(singletonList(request)) + .extracting(BidRequest::getExt).isNotNull() + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getTargeting) + .extracting(ExtRequestTargeting::getIncludeformat) + .containsExactly(true); + } + @Test public void shouldReturnBidRequestWithDefaultIncludeBidderKeysIfStoredRequestExtTargetingHasNoIncludeBidderKeys() { // given diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 424a3a8e578..f9f536ca465 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -980,7 +980,8 @@ public void shouldTruncateTargetingKeywordsByRequestPassedValue() { BigDecimal.valueOf(0.5)))))) .includewinners(true) .includebidderkeys(true) - .truncateattrchars(20) + .includeformat(false) + .truncateattrchars(20) .build())); final AuctionContext auctionContext = givenAuctionContext( bidRequest, @@ -1061,7 +1062,8 @@ public void shouldPopulateTargetingKeywordsFromMediaTypePriceGranularities() { null)) .includewinners(true) .includebidderkeys(true) - .build()))); + .includeformat(false) + .build()))); final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("i1").build(); final List bidderResponses = singletonList(BidderResponse.of("bidder1", @@ -1455,7 +1457,8 @@ public void shouldNotPopulateWinningBidTargetingIfIncludeWinnersFlagIsFalse() { BigDecimal.valueOf(0.5)))))) .includewinners(false) .includebidderkeys(true) - .build()))); + .includeformat(false) + .build()))); final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("i1").build(); final List bidderResponses = singletonList(BidderResponse.of("bidder1", @@ -1496,7 +1499,8 @@ public void shouldNotPopulateBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse() BigDecimal.valueOf(0.5)))))) .includewinners(true) .includebidderkeys(false) - .build()))); + .includeformat(false) + .build()))); final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("i1").build(); final List bidderResponses = singletonList(BidderResponse.of("bidder1", @@ -1956,6 +1960,7 @@ private static ExtRequestTargeting givenTargeting() { BigDecimal.valueOf(0.5)))))) .includewinners(true) .includebidderkeys(true) + .includeformat(false) .build(); } diff --git a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java index 48860242eb9..773006fd3f8 100644 --- a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java @@ -8,6 +8,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.response.Bid; +import org.prebid.server.proto.response.MediaType; import java.math.BigDecimal; import java.util.Map; @@ -29,7 +30,8 @@ public class TargetingKeywordsCreatorTest { @Test public void shouldReturnTargetingKeywordsForOrdinaryBid() { // given - final Bid bid = Bid.builder().bidder("bidder1").price(BigDecimal.ONE).dealId("dealId1").cacheId("cacheId1") + final Bid bid = Bid.builder().bidder("bidder1").price(BigDecimal.ONE).dealId("dealId1") + .mediaType(MediaType.banner).cacheId("cacheId1") .width(50).height(100).build(); // when @@ -40,6 +42,7 @@ public void shouldReturnTargetingKeywordsForOrdinaryBid() { true, true, false, + false, 0, null, null, @@ -69,11 +72,12 @@ public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() { true, true, false, + false, 0, null, null, null) - .makeFor(bid, "bidder1", false, null, null); + .makeFor(bid, "bidder1", false, null, null, null); // then assertThat(keywords).containsOnly( @@ -87,7 +91,7 @@ public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() { public void shouldReturnTargetingKeywordsWithEntireKeys() { // given final Bid bid = Bid.builder().bidder("veryververyverylongbidder1").price(BigDecimal.ONE).dealId("dealId1") - .cacheId("cacheId1").width(50).height(100).build(); + .cacheId("cacheId1").mediaType(MediaType.banner).width(50).height(100).build(); // when final Map keywords = TargetingKeywordsCreator.create( @@ -97,6 +101,7 @@ public void shouldReturnTargetingKeywordsWithEntireKeys() { true, true, false, + false, 0, null, null, @@ -126,11 +131,12 @@ public void shouldReturnTargetingKeywordsWithEntireKeysOpenrtb() { true, true, false, + false, 0, null, null, null) - .makeFor(bid, "veryververyverylongbidder1", false, null, null); + .makeFor(bid, "veryververyverylongbidder1", false, null, null, null); // then assertThat(keywords).containsOnly( @@ -148,7 +154,7 @@ public void shouldReturnTargetingKeywordsForWinningBid() { .price(BigDecimal.ONE) .dealId("dealId1") .cacheId("cacheId1") - .width(50) + .mediaType(MediaType.banner).width(50) .height(100) .build(); @@ -159,6 +165,7 @@ public void shouldReturnTargetingKeywordsForWinningBid() { singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))), true, true, + true, false, 0, null, @@ -177,7 +184,9 @@ public void shouldReturnTargetingKeywordsForWinningBid() { entry("hb_bidder", "bidder1"), entry("hb_cache_id", "cacheId1"), entry("hb_size", "50x100"), - entry("hb_deal", "dealId1")); + entry("hb_deal", "dealId1"), + entry("hb_format", "banner"), + entry("hb_format_bidder1", "banner")); } @Test @@ -197,12 +206,13 @@ public void shouldReturnTargetingKeywordsForWinningBidOpenrtb() { singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))), true, true, + true, false, 0, null, null, null) - .makeFor(bid, "bidder1", true, "cacheId1", "videoCacheId1"); + .makeFor(bid, "bidder1", true, "cacheId1", "banner", "videoCacheId1"); // then assertThat(keywords).containsOnly( @@ -217,30 +227,46 @@ public void shouldReturnTargetingKeywordsForWinningBidOpenrtb() { entry("hb_cache_id", "cacheId1"), entry("hb_cache_id_bidder1", "cacheId1"), entry("hb_uuid", "videoCacheId1"), - entry("hb_uuid_bidder1", "videoCacheId1")); + entry("hb_uuid_bidder1", "videoCacheId1"), + entry("hb_format", "banner"), + entry("hb_format_bidder1", "banner")); } @Test public void shouldFallbackToDefaultPriceIfInvalidPriceGranularity() { // given - final Bid bid = Bid.builder().bidder("").price(BigDecimal.valueOf(3.87)).build(); + final Bid bid = Bid.builder().bidder("").price(BigDecimal.valueOf(3.87)).mediaType(MediaType.banner).build(); // when final Map keywords = TargetingKeywordsCreator.create( - "invalid", true, true, false, 0) + "invalid", true, true, false, false, 0) .makeFor(bid, true); // then assertThat(keywords).contains(entry("hb_pb", StringUtils.EMPTY)); } + @Test + public void shouldIncludeFormat() { + // given + final Bid bid = Bid.builder().bidder("").price(BigDecimal.valueOf(3.87)).mediaType(MediaType.banner).build(); + + // when + final Map keywords = TargetingKeywordsCreator.create( + "invalid", true, true, true, false, 0) + .makeFor(bid, true); + + // then + assertThat(keywords).contains(entry("hb_format", "banner")); + } + @Test public void shouldUseDefaultPriceGranularity() { // given - final Bid bid = Bid.builder().bidder("").price(BigDecimal.valueOf(3.87)).build(); + final Bid bid = Bid.builder().bidder("").price(BigDecimal.valueOf(3.87)).mediaType(MediaType.banner).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, 0) + final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, false, 0) .makeFor(bid, true); // then @@ -254,20 +280,34 @@ public void shouldUseDefaultPriceGranularityOpenrtb() { .price(BigDecimal.valueOf(3.87)).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, 0) - .makeFor(bid, "", true, null, null); + final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, false, 0) + .makeFor(bid, "", true, null, null, null); // then assertThat(keywords).contains(entry("hb_pb", "3.80")); } + @Test + public void shouldIncludeFormatOpenrtb() { + // given + final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder() + .price(BigDecimal.valueOf(3.87)).build(); + + // when + final Map keywords = TargetingKeywordsCreator.create(null, true, true, true, false, 0) + .makeFor(bid, "", true, null, "banner", null); + + // then + assertThat(keywords).contains(entry("hb_format", "banner")); + } + @Test public void shouldNotIncludeCacheIdAndDealIdAndSize() { // given - final Bid bid = Bid.builder().bidder("bidder").price(BigDecimal.ONE).build(); + final Bid bid = Bid.builder().bidder("bidder").mediaType(MediaType.banner).price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, 0) + final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, false, 0) .makeFor(bid, true); // then @@ -281,8 +321,8 @@ public void shouldNotIncludeCacheIdAndDealIdAndSizeOpenrtb() { final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, 0) - .makeFor(bid, "bidder", true, null, null); + final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, false, 0) + .makeFor(bid, "bidder", true, null, null, null); // then assertThat(keywords).doesNotContainKeys("hb_cache_id_bidder", "hb_deal_bidder", "hb_size_bidder", @@ -292,10 +332,10 @@ public void shouldNotIncludeCacheIdAndDealIdAndSizeOpenrtb() { @Test public void shouldReturnEnvKeyForAppRequest() { // given - final Bid bid = Bid.builder().bidder("bidder").price(BigDecimal.ONE).build(); + final Bid bid = Bid.builder().bidder("bidder").mediaType(MediaType.banner).price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, true, true, true, 0) + final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, true, 0) .makeFor(bid, true); // then @@ -310,8 +350,8 @@ public void shouldReturnEnvKeyForAppRequestOpenrtb() { final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, true, true, true, 0) - .makeFor(bid, "bidder", true, null, null); + final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, true, 0) + .makeFor(bid, "bidder", true, null, null, null); // then assertThat(keywords).contains( @@ -325,8 +365,8 @@ public void shouldNotIncludeWinningBidTargetingIfIncludeWinnersFlagIsFalse() { final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, 0) - .makeFor(bid, "bidder1", true, null, null); + final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, false, 0) + .makeFor(bid, "bidder1", true, null, null, null); // then assertThat(keywords).doesNotContainKeys("hb_bidder", "hb_pb"); @@ -338,8 +378,8 @@ public void shouldIncludeWinningBidTargetingIfIncludeWinnersFlagIsTrue() { final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, 0) - .makeFor(bid, "bidder1", true, null, null); + final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, false, 0) + .makeFor(bid, "bidder1", true, null, null, null); // then assertThat(keywords).containsKeys("hb_bidder", "hb_pb"); @@ -351,8 +391,8 @@ public void shouldNotIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse() final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, false, false, false, 0) - .makeFor(bid, "bidder1", true, null, null); + final Map keywords = TargetingKeywordsCreator.create(null, false, false, false, false, 0) + .makeFor(bid, "bidder1", true, null, null, null); // then assertThat(keywords).doesNotContainKeys("hb_bidder_bidder1", "hb_pb_bidder1"); @@ -364,8 +404,8 @@ public void shouldIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsTrue() { final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, 0) - .makeFor(bid, "bidder1", true, null, null); + final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, false, 0) + .makeFor(bid, "bidder1", true, null, null, null); // then assertThat(keywords).containsKeys("hb_bidder_bidder1", "hb_pb_bidder1"); @@ -377,8 +417,8 @@ public void shouldTruncateTargetingBidderKeywordsIfTruncateAttrCharsIsDefined() final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, 20) - .makeFor(bid, "someVeryLongBidderName", true, null, null); + final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, false, 20) + .makeFor(bid, "someVeryLongBidderName", true, null, null, null); // then assertThat(keywords).hasSize(2) @@ -391,8 +431,8 @@ public void shouldTruncateTargetingWithoutBidderSuffixKeywordsIfTruncateAttrChar final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, true, false, false, 7) - .makeFor(bid, "bidder", true, null, null); + final Map keywords = TargetingKeywordsCreator.create(null, true, false, false, false, 7) + .makeFor(bid, "bidder", true, null, null, null); // then assertThat(keywords).hasSize(2) @@ -405,8 +445,8 @@ public void shouldNotTruncateTargetingKeywordsIfTruncateAttrCharsIsNotDefined() final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, 0) - .makeFor(bid, "someVeryLongBidderName", true, null, null); + final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, false, 0) + .makeFor(bid, "someVeryLongBidderName", true, null, null, null); // then assertThat(keywords).hasSize(2) @@ -432,11 +472,12 @@ public void shouldTruncateKeysFromResolver() { true, true, false, + false, 20, null, null, resolver) - .makeFor(bid, "bidder1", true, null, null); + .makeFor(bid, "bidder1", true, null, null, null); // then assertThat(keywords).contains(entry("key_longer_than_twen", "value1")); @@ -461,11 +502,12 @@ public void shouldIncludeKeywordsFromResolver() { true, true, false, + false, 0, null, null, resolver) - .makeFor(bid, "bidder1", true, null, null); + .makeFor(bid, "bidder1", true, null, null, null); // then assertThat(keywords).contains(entry("keyword1", "value1")); From bebb407e6360725b579442d02bc354bd3666bae0 Mon Sep 17 00:00:00 2001 From: Braslavskyi Andrii Date: Tue, 15 Dec 2020 15:38:00 +0200 Subject: [PATCH 062/129] Filter not supported types for Rubicon bidder (#855) * Filter not supported types for Rubicon bidder * Refactor filter logic * Return empty collections instead of null --- .../server/bidder/rubicon/RubiconBidder.java | 46 ++++++++++++++++++- .../bidder/rubicon/RubiconBidderTest.java | 46 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index fcd3d1cbaa8..8f8895f8d82 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -170,8 +170,14 @@ public Result>> makeHttpRequests(BidRequest bidRequ final List> httpRequests = new ArrayList<>(); final List errors = new ArrayList<>(); + final List imps = extractValidImps(bidRequest, errors); + if (CollectionUtils.isEmpty(imps)) { + errors.add(BidderError.of("There are no valid impressions to create bid request to rubicon bidder", + BidderError.Type.bad_input)); + return Result.of(Collections.emptyList(), errors); + } final Map> impToImpExt = - parseRubiconImpExts(bidRequest.getImp(), errors); + parseRubiconImpExts(imps, errors); final String impLanguage = firstImpExtLanguage(impToImpExt.values()); for (Map.Entry> impToExt : impToImpExt.entrySet()) { @@ -226,6 +232,44 @@ public Map extractTargeting(ObjectNode extBidBidder) { : Collections.emptyMap(); } + private List extractValidImps(BidRequest bidRequest, List errors) { + final Map> isValidToImps = bidRequest.getImp().stream() + .collect(Collectors.groupingBy(RubiconBidder::isValidType)); + + isValidToImps.getOrDefault(false, Collections.emptyList()).stream() + .map(this::impTypeErrorMessage) + .forEach(errors::add); + + return isValidToImps.getOrDefault(true, Collections.emptyList()); + } + + private static boolean isValidType(Imp imp) { + return imp.getVideo() != null || imp.getBanner() != null; + } + + private BidderError impTypeErrorMessage(Imp imp) { + final BidType type = resolveExpectedBidType(imp); + return BidderError.of( + String.format("Impression with id %s rejected with invalid type `%s`." + " Allowed types are banner and" + + " video.", imp.getId(), type != null ? type.name() : "unknown"), BidderError.Type.bad_input); + } + + private static BidType resolveExpectedBidType(Imp imp) { + if (imp.getBanner() != null) { + return BidType.banner; + } + if (imp.getVideo() != null) { + return BidType.video; + } + if (imp.getAudio() != null) { + return BidType.audio; + } + if (imp.getXNative() != null) { + return BidType.xNative; + } + return null; + } + private static MultiMap headers(String xapiUsername, String xapiPassword) { return MultiMap.caseInsensitiveMultiMap() .add(HttpUtil.AUTHORIZATION_HEADER, authHeader(xapiUsername, xapiPassword)) 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 466256bc8ae..01e2a6306f6 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.BidRequest.BidRequestBuilder; @@ -13,6 +14,7 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Imp.ImpBuilder; import com.iab.openrtb.request.Metric; +import com.iab.openrtb.request.Native; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; @@ -138,6 +140,49 @@ public void makeHttpRequestsShouldFillMethodAndUrlAndExpectedHeaders() { tuple(HttpUtil.USER_AGENT_HEADER.toString(), "prebid-server/1.0")); } + @Test + public void makeHttpRequestsShouldFilterImpressionsWithInvalidTypes() { + // given + final Imp imp1 = givenImp(builder -> builder.video(Video.builder().build())); + final Imp imp2 = givenImp(builder -> builder.id("2").xNative(Native.builder().build())); + final Imp imp3 = givenImp(builder -> builder.id("3").audio(Audio.builder().build())); + final BidRequest bidRequest = BidRequest.builder().imp(asList(imp1, imp2, imp3)).build(); + + // when + final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(2) + .containsOnly( + BidderError.of("Impression with id 2 rejected with invalid type `xNative`." + + " Allowed types are banner and video.", BidderError.Type.bad_input), + BidderError.of("Impression with id 3 rejected with invalid type `audio`." + + " Allowed types are banner and video.", BidderError.Type.bad_input)); + assertThat(result.getValue()).hasSize(1); + } + + @Test + public void makeHttpRequestsShouldFilterAllImpressionsAndReturnErrorMeessagesWithoutRequests() { + // given + final Imp imp1 = givenImp(builder -> builder.id("1").xNative(Native.builder().build())); + final Imp imp2 = givenImp(builder -> builder.id("2").audio(Audio.builder().build())); + final BidRequest bidRequest = BidRequest.builder().imp(asList(imp1, imp2)).build(); + + // when + final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(3) + .containsOnly( + BidderError.of("Impression with id 1 rejected with invalid type `xNative`." + + " Allowed types are banner and video.", BidderError.Type.bad_input), + BidderError.of("Impression with id 2 rejected with invalid type `audio`." + + " Allowed types are banner and video.", BidderError.Type.bad_input), + BidderError.of("There are no valid impressions to create bid request to rubicon bidder", + BidderError.Type.bad_input)); + assertThat(result.getValue()).isEmpty(); + } + @Test public void makeHttpRequestsShouldReplaceDefaultParametersWithExtPrebidBiddersBidder() { // given @@ -1929,6 +1974,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { .imp(asList( givenImp(builder -> builder.video(Video.builder().build())), Imp.builder() + .banner(Banner.builder().build()) .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) .build())) .build(); From 7f98043a5b36a1d3082f1e9793279f26218b04cc Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Tue, 15 Dec 2020 15:47:53 +0200 Subject: [PATCH 063/129] Add including of targeting for video in case if we had "includeBiddersKey" (#875) --- .../server/auction/VideoResponseFactory.java | 16 ++++++++++++---- .../server/auction/VideoResponseFactoryTest.java | 10 +++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/VideoResponseFactory.java b/src/main/java/org/prebid/server/auction/VideoResponseFactory.java index 2a9083d2385..752b632c2a1 100644 --- a/src/main/java/org/prebid/server/auction/VideoResponseFactory.java +++ b/src/main/java/org/prebid/server/auction/VideoResponseFactory.java @@ -88,7 +88,7 @@ private List adPodsWithTargetingFrom(List bids) { final List adPods = new ArrayList<>(); for (Bid bid : bids) { final Map targeting = targeting(bid); - if (targeting.get("hb_uuid") == null) { + if (findByPrefix(targeting, "hb_uuid") == null) { continue; } final String impId = bid.getImpid(); @@ -99,9 +99,9 @@ private List adPodsWithTargetingFrom(List bids) { final Integer podId = Integer.parseInt(podIdString); final ExtResponseVideoTargeting videoTargeting = ExtResponseVideoTargeting.of( - targeting.get("hb_pb"), - targeting.get("hb_pb_cat_dur"), - targeting.get("hb_uuid")); + findByPrefix(targeting, "hb_pb"), + findByPrefix(targeting, "hb_pb_cat_dur"), + findByPrefix(targeting, "hb_uuid")); ExtAdPod adPod = adPods.stream() .filter(extAdPod -> extAdPod.getPodid().equals(podId)) @@ -130,6 +130,14 @@ private Map targeting(Bid bid) { return targeting != null ? targeting : Collections.emptyMap(); } + private static String findByPrefix(Map keyToValue, String prefix) { + return keyToValue.entrySet().stream() + .filter(keyAndValue -> keyAndValue.getKey().startsWith(prefix)) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + } + private static List adPodsWithErrors(List podErrors) { return podErrors.stream() .map(podError -> ExtAdPod.of(podError.getPodId(), null, podError.getPodErrors())) diff --git a/src/test/java/org/prebid/server/auction/VideoResponseFactoryTest.java b/src/test/java/org/prebid/server/auction/VideoResponseFactoryTest.java index 6916a5b9e10..1b78d21ef6e 100644 --- a/src/test/java/org/prebid/server/auction/VideoResponseFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/VideoResponseFactoryTest.java @@ -35,9 +35,9 @@ public void setUp() { public void shouldReturnExpectedVideoResponse() { // given final Map targeting = new HashMap<>(); - targeting.put("hb_uuid", "value1"); - targeting.put("hb_pb", "hb_pb"); - targeting.put("hb_pb_cat_dur", "hb_pb_cat_dur"); + targeting.put("hb_uuid_appnexus", "hb_uuidVal"); + targeting.put("hb_pb_appnexus", "hb_pbVal"); + targeting.put("hb_pb_cat_dur_appnexus", "hb_pb_cat_durVal"); final Bid bid0 = Bid.builder() .impid("0_0") @@ -73,9 +73,9 @@ public void shouldReturnExpectedVideoResponse() { // then final ExtAdPod expectedExtAdPod0 = ExtAdPod.of(0, - singletonList(ExtResponseVideoTargeting.of("hb_pb", "hb_pb_cat_dur", "value1")), null); + singletonList(ExtResponseVideoTargeting.of("hb_pbVal", "hb_pb_cat_durVal", "hb_uuidVal")), null); final ExtAdPod expectedExtAdPod1 = ExtAdPod.of( - 1, singletonList(ExtResponseVideoTargeting.of("hb_pb", "hb_pb_cat_dur", "value1")), null); + 1, singletonList(ExtResponseVideoTargeting.of("hb_pbVal", "hb_pb_cat_durVal", "hb_uuidVal")), null); final ExtAdPod expectedErrorExtAdPod3 = ExtAdPod.of(3, null, singletonList("Error")); final List expectedAdPodResponse = Arrays.asList(expectedExtAdPod0, expectedExtAdPod1, expectedErrorExtAdPod3); From 3f0ebe2a9259a98ab3f698d74966b35c77d909d6 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Tue, 15 Dec 2020 16:00:59 +0200 Subject: [PATCH 064/129] Make valid request with duplicating eids (#892) --- .../prebid/server/bidder/rubicon/RubiconBidder.java | 13 +++++-------- .../prebid/server/validation/RequestValidator.java | 7 ------- .../server/validation/RequestValidatorTest.java | 5 ++--- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index 8f8895f8d82..1a1ff24424b 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -947,14 +947,11 @@ private void mergeFirstPartyDataFromUser(User user, ObjectNode result) { private static String extractLiverampId(Map> sourceToUserEidExt) { final List liverampEids = MapUtils.emptyIfNull(sourceToUserEidExt).get(LIVERAMP_EID); for (ExtUserEid extUserEid : CollectionUtils.emptyIfNull(liverampEids)) { - final ExtUserEidUid eidUid = extUserEid != null - ? CollectionUtils.emptyIfNull(extUserEid.getUids()).stream().findFirst().orElse(null) - : null; - - final String id = eidUid != null ? eidUid.getId() : null; - if (StringUtils.isNotEmpty(id)) { - return id; - } + return extUserEid.getUids().stream() + .map(ExtUserEidUid::getId) + .filter(StringUtils::isNotEmpty) + .findFirst() + .orElse(null); } return null; } diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java index 28d93c9f807..829d790ad50 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -59,7 +59,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -397,7 +396,6 @@ private void validateUser(User user, Map aliases) throws Validat throw new ValidationException( "request.user.ext.eids must contain at least one element or be undefined"); } - final Set uniqueSources = new HashSet<>(eids.size()); for (int index = 0; index < eids.size(); index++) { final ExtUserEid eid = eids.get(index); if (StringUtils.isBlank(eid.getSource())) { @@ -425,11 +423,6 @@ private void validateUser(User user, Map aliases) throws Validat } } } - uniqueSources.add(eid.getSource()); - } - - if (eids.size() != uniqueSources.size()) { - throw new ValidationException("request.user.ext.eids must contain unique sources"); } } } diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index b51c59e96d5..bb728ff306f 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -1653,7 +1653,7 @@ public void validateShouldReturnValidationMessageWhenEidUidIdIsMissing() { } @Test - public void validateShouldReturnValidationMessageWhenEidSourceIsNotUnique() { + public void validateShouldNotReturnAnyErrorWhenEidSourceIsNotUnique() { // given final BidRequest bidRequest = validBidRequestBuilder() .user(User.builder() @@ -1671,8 +1671,7 @@ public void validateShouldReturnValidationMessageWhenEidSourceIsNotUnique() { final ValidationResult result = requestValidator.validate(bidRequest); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.ext.eids must contain unique sources"); + assertThat(result.getErrors()).isEmpty(); } @Test From f6bc30f8ad3f737d28917eb17f4365ad4381d059 Mon Sep 17 00:00:00 2001 From: Braslavskyi Andrii Date: Tue, 15 Dec 2020 17:05:33 +0200 Subject: [PATCH 065/129] Remove safari metrics (#1074) --- docs/metrics.md | 2 -- .../prebid/server/handler/AuctionHandler.java | 12 +++------- .../server/handler/openrtb2/AmpHandler.java | 9 +++----- .../handler/openrtb2/AuctionHandler.java | 9 +++----- .../server/handler/openrtb2/VideoHandler.java | 2 -- .../org/prebid/server/metric/MetricName.java | 2 -- .../org/prebid/server/metric/Metrics.java | 12 +--------- .../java/org/prebid/server/util/HttpUtil.java | 14 ------------ .../server/handler/AuctionHandlerTest.java | 6 ++--- .../handler/openrtb2/AmpHandlerTest.java | 6 ++--- .../handler/openrtb2/AuctionHandlerTest.java | 6 ++--- .../org/prebid/server/metric/MetricsTest.java | 22 +++++-------------- .../org/prebid/server/util/HttpUtilTest.java | 10 --------- 13 files changed, 24 insertions(+), 88 deletions(-) diff --git a/docs/metrics.md b/docs/metrics.md index c3b3f2d00a6..e8a3afecb48 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -38,8 +38,6 @@ where `[DATASOURCE]` is a data source name, `DEFAULT_DS` by defaul. ## General auction metrics - `app_requests` - number of requests received from applications - `no_cookie_requests` - number of requests without `uids` cookie or with one that didn't contain at least one live UID -- `safari_requests` - number of requests received from Safari browser -- `safari_no_cookie_requests` - number of requests received from Safari browser without `uids` cookie or with one that didn't contain at least one live UID - `request_time` - timer tracking how long did it take for Prebid Server to serve a request - `imps_requested` - number if impressions requested - `imps_banner` - number of banner impressions diff --git a/src/main/java/org/prebid/server/handler/AuctionHandler.java b/src/main/java/org/prebid/server/handler/AuctionHandler.java index 821e3314edc..0aeadc8d5a2 100644 --- a/src/main/java/org/prebid/server/handler/AuctionHandler.java +++ b/src/main/java/org/prebid/server/handler/AuctionHandler.java @@ -107,16 +107,11 @@ public AuctionHandler(ApplicationSettings applicationSettings, public void handle(RoutingContext context) { final long startTime = clock.millis(); - final boolean isSafari = HttpUtil.isSafari(context.request().headers().get(HttpUtil.USER_AGENT_HEADER)); - - metrics.updateSafariRequestsMetric(isSafari); - preBidRequestContextFactory.fromRequest(context) .recover(exception -> failWithInvalidRequest( String.format("Error parsing request: %s", exception.getMessage()), exception)) - .map(preBidRequestContext -> - updateAppAndNoCookieAndImpsMetrics(preBidRequestContext, isSafari)) + .map(this::updateAppAndNoCookieAndImpsMetrics) .compose(preBidRequestContext -> accountFrom(preBidRequestContext) .map(account -> Tuple2.of(preBidRequestContext, account))) @@ -147,13 +142,12 @@ public void handle(RoutingContext context) { respondWith(bidResponseOrError(preBidResponseResult), context, startTime)); } - private PreBidRequestContext updateAppAndNoCookieAndImpsMetrics(PreBidRequestContext preBidRequestContext, - boolean isSafari) { + private PreBidRequestContext updateAppAndNoCookieAndImpsMetrics(PreBidRequestContext preBidRequestContext) { final PreBidRequest preBidRequest = preBidRequestContext.getPreBidRequest(); final List adUnits = preBidRequest.getAdUnits(); metrics.updateAppAndNoCookieAndImpsRequestedMetrics(preBidRequest.getApp() != null, - !preBidRequestContext.isNoLiveUids(), isSafari, adUnits.size()); + !preBidRequestContext.isNoLiveUids(), adUnits.size()); final Map mediaTypeToCount = adUnits.stream() .map(AdUnit::getMediaTypes) diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java index ad5b50fb092..9be3171ee9b 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java @@ -115,16 +115,13 @@ public void handle(RoutingContext routingContext) { // more accurately if we note the real start time, and use it to compute the auction timeout. final long startTime = clock.millis(); - final boolean isSafari = HttpUtil.isSafari(routingContext.request().headers().get(HttpUtil.USER_AGENT_HEADER)); - metrics.updateSafariRequestsMetric(isSafari); - final AmpEvent.AmpEventBuilder ampEventBuilder = AmpEvent.builder() .httpContext(HttpContext.from(routingContext)); ampRequestFactory.fromRequest(routingContext, startTime) .map(context -> addToEvent(context, ampEventBuilder::auctionContext, context)) - .map(context -> updateAppAndNoCookieAndImpsMetrics(context, isSafari)) + .map(this::updateAppAndNoCookieAndImpsMetrics) .compose(context -> exchangeService.holdAuction(context) .map(bidResponse -> Tuple2.of(bidResponse, context))) @@ -155,13 +152,13 @@ private static R addToEvent(T field, Consumer consumer, R result) { return result; } - private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context, boolean isSafari) { + private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context) { final BidRequest bidRequest = context.getBidRequest(); final UidsCookie uidsCookie = context.getUidsCookie(); final List imps = bidRequest.getImp(); metrics.updateAppAndNoCookieAndImpsRequestedMetrics(bidRequest.getApp() != null, uidsCookie.hasLiveUids(), - isSafari, imps.size()); + imps.size()); metrics.updateImpTypesMetrics(imps); diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java index 6e0b6e553ee..86651f0bb07 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java @@ -74,16 +74,13 @@ public void handle(RoutingContext routingContext) { // more accurately if we note the real start time, and use it to compute the auction timeout. final long startTime = clock.millis(); - final boolean isSafari = HttpUtil.isSafari(routingContext.request().headers().get(HttpUtil.USER_AGENT_HEADER)); - metrics.updateSafariRequestsMetric(isSafari); - final AuctionEvent.AuctionEventBuilder auctionEventBuilder = AuctionEvent.builder() .httpContext(HttpContext.from(routingContext)); auctionRequestFactory.fromRequest(routingContext, startTime) .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context)) - .map(context -> updateAppAndNoCookieAndImpsMetrics(context, isSafari)) + .map(context -> updateAppAndNoCookieAndImpsMetrics(context)) .compose(context -> exchangeService.holdAuction(context) .map(bidResponse -> Tuple2.of(bidResponse, context))) @@ -97,13 +94,13 @@ private static R addToEvent(T field, Consumer consumer, R result) { return result; } - private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context, boolean isSafari) { + private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context) { final BidRequest bidRequest = context.getBidRequest(); final UidsCookie uidsCookie = context.getUidsCookie(); final List imps = bidRequest.getImp(); metrics.updateAppAndNoCookieAndImpsRequestedMetrics(bidRequest.getApp() != null, uidsCookie.hasLiveUids(), - isSafari, imps.size()); + imps.size()); metrics.updateImpTypesMetrics(imps); diff --git a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java index 36218834993..d021c76df0f 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java @@ -63,8 +63,6 @@ public void handle(RoutingContext routingContext) { // more accurately if we note the real start time, and use it to compute the auction timeout. final long startTime = clock.millis(); - final boolean isSafari = HttpUtil.isSafari(routingContext.request().headers().get(HttpUtil.USER_AGENT_HEADER)); - metrics.updateSafariRequestsMetric(isSafari); final VideoEvent.VideoEventBuilder videoEventBuilder = VideoEvent.builder() .httpContext(HttpContext.from(routingContext)); diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index 49921d404ab..524d032b883 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -24,8 +24,6 @@ public enum MetricName { requests, app_requests, no_cookie_requests, - safari_requests, - safari_no_cookie_requests, request_time, prices, imps_requested, diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index bef85c06058..53f05612ed2 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -121,21 +121,11 @@ SettingsCacheMetrics forSettingsCacheType(MetricName type) { return settingsCacheMetrics.computeIfAbsent(type, settingsCacheMetricsCreator); } - public void updateSafariRequestsMetric(boolean isSafari) { - if (isSafari) { - incCounter(MetricName.safari_requests); - } - } - - public void updateAppAndNoCookieAndImpsRequestedMetrics(boolean isApp, boolean liveUidsPresent, boolean isSafari, - int numImps) { + public void updateAppAndNoCookieAndImpsRequestedMetrics(boolean isApp, boolean liveUidsPresent, int numImps) { if (isApp) { incCounter(MetricName.app_requests); } else if (!liveUidsPresent) { incCounter(MetricName.no_cookie_requests); - if (isSafari) { - incCounter(MetricName.safari_no_cookie_requests); - } } incCounter(MetricName.imps_requested, numImps); } diff --git a/src/main/java/org/prebid/server/util/HttpUtil.java b/src/main/java/org/prebid/server/util/HttpUtil.java index 1f12b937722..3344f8d0e04 100644 --- a/src/main/java/org/prebid/server/util/HttpUtil.java +++ b/src/main/java/org/prebid/server/util/HttpUtil.java @@ -53,20 +53,6 @@ public final class HttpUtil { private HttpUtil() { } - /** - * Detects whether browser is safari or not by user agent analysis. - */ - public static boolean isSafari(String userAgent) { - // this is a simple heuristic based on this article: - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent - // - // there are libraries available doing different kinds of User-Agent analysis but they impose performance - // implications as well, example: https://github.com/nielsbasjes/yauaa - return StringUtils.isNotBlank(userAgent) - && userAgent.contains("AppleWebKit") && userAgent.contains("Safari") - && !userAgent.contains("Chrome") && !userAgent.contains("Chromium"); - } - /** * Checks the input string for using as URL. */ diff --git a/src/test/java/org/prebid/server/handler/AuctionHandlerTest.java b/src/test/java/org/prebid/server/handler/AuctionHandlerTest.java index f8e71396e93..c53ed71eb3a 100644 --- a/src/test/java/org/prebid/server/handler/AuctionHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/AuctionHandlerTest.java @@ -569,7 +569,7 @@ public void shouldIncrementCommonMetrics() { // then verify(metrics).updateRequestTypeMetric(eq(MetricName.legacy), eq(MetricName.ok)); - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyBoolean(), eq(1)); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), eq(1)); verify(metrics).updateImpTypesMetrics(singletonMap("banner", 1L)); verify(metrics).updateAccountRequestMetrics(eq("accountId"), eq(MetricName.legacy)); verify(metrics).updateRequestTimeMetric(anyLong()); @@ -594,7 +594,7 @@ public void shouldIncrementNoBidMetrics() { } @Test - public void shouldIncrementSafariAndNoCookieMetrics() { + public void shouldIncrementNoCookieMetrics() { // given givenPreBidRequestContext(identity(), builder -> builder.noLiveUids(true)); @@ -605,7 +605,7 @@ public void shouldIncrementSafariAndNoCookieMetrics() { auctionHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), eq(true), anyInt()); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt()); } @Test diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java index 32513a625f4..f1992b0110b 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java @@ -471,7 +471,7 @@ public void shouldIncrementAppRequestMetrics() { ampHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyBoolean(), anyInt()); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyInt()); } @Test @@ -493,7 +493,7 @@ public void shouldIncrementNoCookieMetrics() { ampHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), eq(true), anyInt()); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt()); } @Test @@ -511,7 +511,7 @@ public void shouldIncrementImpsRequestedMetrics() { ampHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), anyBoolean(), eq(1)); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), eq(1)); } @Test diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java index cfca6fb8d69..a355efbd4f0 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java @@ -369,7 +369,7 @@ public void shouldIncrementAppRequestMetrics() { auctionHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyBoolean(), anyInt()); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyInt()); } @Test @@ -390,7 +390,7 @@ public void shouldIncrementNoCookieMetrics() { auctionHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), eq(true), anyInt()); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt()); } @Test @@ -407,7 +407,7 @@ public void shouldIncrementImpsRequestedMetrics() { auctionHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), anyBoolean(), eq(1)); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), eq(1)); } @Test diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index b0f63bbd6cf..8503dadfb39 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -341,29 +341,17 @@ public void forCircuitBreakerShouldReturnCircuitBreakerMetricsConfiguredWithId() assertThat(metricRegistry.gauge("circuit-breaker.db.opened.count", () -> null).getValue()).isEqualTo(1L); } - @Test - public void updateSafariRequestsMetricShouldIncrementMetric() { - // when - metrics.updateSafariRequestsMetric(true); - metrics.updateSafariRequestsMetric(false); - - // then - assertThat(metricRegistry.counter("safari_requests").getCount()).isOne(); - } - @Test public void updateAppAndNoCookieAndImpsRequestedMetricsShouldIncrementMetrics() { // when - metrics.updateAppAndNoCookieAndImpsRequestedMetrics(true, false, false, 1); - metrics.updateAppAndNoCookieAndImpsRequestedMetrics(false, false, false, 2); - metrics.updateAppAndNoCookieAndImpsRequestedMetrics(false, false, true, 1); - metrics.updateAppAndNoCookieAndImpsRequestedMetrics(false, true, false, 1); + metrics.updateAppAndNoCookieAndImpsRequestedMetrics(true, false, 1); + metrics.updateAppAndNoCookieAndImpsRequestedMetrics(false, false, 2); + metrics.updateAppAndNoCookieAndImpsRequestedMetrics(false, true, 1); // then assertThat(metricRegistry.counter("app_requests").getCount()).isOne(); - assertThat(metricRegistry.counter("no_cookie_requests").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("safari_no_cookie_requests").getCount()).isOne(); - assertThat(metricRegistry.counter("imps_requested").getCount()).isEqualTo(5); + assertThat(metricRegistry.counter("no_cookie_requests").getCount()).isOne(); + assertThat(metricRegistry.counter("imps_requested").getCount()).isEqualTo(4); } @Test diff --git a/src/test/java/org/prebid/server/util/HttpUtilTest.java b/src/test/java/org/prebid/server/util/HttpUtilTest.java index 77242c33ca2..889b402b4f6 100644 --- a/src/test/java/org/prebid/server/util/HttpUtilTest.java +++ b/src/test/java/org/prebid/server/util/HttpUtilTest.java @@ -26,16 +26,6 @@ public class HttpUtilTest { @Mock private RoutingContext routingContext; - @Test - public void isSafariShouldReturnTrue() { - assertThat(HttpUtil.isSafari("Useragent with Safari browser and AppleWebKit built-in.")).isTrue(); - } - - @Test - public void isSafariShouldReturnFalse() { - assertThat(HttpUtil.isSafari("Useragent with Safari browser but Chromium forked by.")).isFalse(); - } - @Test public void validateUrlShouldFailOnInvalidUrl() { // when and then From 3609032e57bff379c9fb3d2a1c60a43c8dae38a1 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Tue, 15 Dec 2020 17:11:07 +0200 Subject: [PATCH 066/129] Handling of eids with duplicate source (#1075) --- .../org/prebid/server/validation/RequestValidator.java | 7 +++++++ .../org/prebid/server/validation/RequestValidatorTest.java | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java index 829d790ad50..526c7114a41 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -65,6 +65,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -424,6 +425,12 @@ private void validateUser(User user, Map aliases) throws Validat } } } + final Set uniqueSources = eids.stream() + .map(ExtUserEid::getSource) + .collect(Collectors.toSet()); + if (eids.size() != uniqueSources.size()) { + throw new ValidationException("request.user.ext.eids must contain unique sources"); + } } } } diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index bb728ff306f..ee4e722de52 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -1653,7 +1653,7 @@ public void validateShouldReturnValidationMessageWhenEidUidIdIsMissing() { } @Test - public void validateShouldNotReturnAnyErrorWhenEidSourceIsNotUnique() { + public void validateShouldReturnErrorWhenEidSourceIsNotUnique() { // given final BidRequest bidRequest = validBidRequestBuilder() .user(User.builder() @@ -1671,7 +1671,7 @@ public void validateShouldNotReturnAnyErrorWhenEidSourceIsNotUnique() { final ValidationResult result = requestValidator.validate(bidRequest); // then - assertThat(result.getErrors()).isEmpty(); + assertThat(result.getErrors()).containsExactly("request.user.ext.eids must contain unique sources"); } @Test From 5e331cb2b009f0cd00132904c3193e5ff95ced2b Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Tue, 15 Dec 2020 17:37:15 +0200 Subject: [PATCH 067/129] Implement default bid request support (#850) --- docs/config-app.md | 3 + docs/developers/default-request.md | 29 ++++ .../prebid/server/auction/FpdResolver.java | 13 +- .../server/auction/OrtbTypesResolver.java | 10 +- .../auction/StoredRequestProcessor.java | 125 ++++++++++++------ .../server/auction/VideoRequestFactory.java | 12 +- .../auction/VideoStoredRequestProcessor.java | 95 +++++++++---- .../JsonMerger.java} | 8 +- ...figuration.java => JsonConfiguration.java} | 8 +- .../spring/config/ServiceConfiguration.java | 51 ++++--- .../server/auction/FpdResolverTest.java | 3 +- .../server/auction/OrtbTypesResolverTest.java | 4 +- .../auction/StoredRequestProcessorTest.java | 96 +++++++++++++- .../VideoStoredRequestProcessorTest.java | 23 +++- .../JsonMergerTest.java} | 8 +- 15 files changed, 367 insertions(+), 121 deletions(-) create mode 100644 docs/developers/default-request.md rename src/main/java/org/prebid/server/{util/JsonMergeUtil.java => json/JsonMerger.java} (94%) rename src/main/java/org/prebid/server/spring/config/{JacksonConfiguration.java => JsonConfiguration.java} (68%) rename src/test/java/org/prebid/server/{util/JsonMergeUtilTest.java => json/JsonMergerTest.java} (94%) diff --git a/docs/config-app.md b/docs/config-app.md index 68d60cd9ab6..b748a5c6544 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -63,6 +63,9 @@ Removes and downloads file again if depending service cant process probably corr - `max-timeout-ms` - this setting controls maximum timeout for /auction endpoint. - `timeout-adjustment-ms` - reduces timeout value passed in legacy Auction request so that Prebid Server can handle timeouts from adapters and respond to the request before it times out. +## Default bid request +- `default-request.file.path` - path to a JSON file containing the default request + ## Auction (OpenRTB) - `auction.blacklisted-accounts` - comma separated list of blacklisted account IDs. - `auction.blacklisted-apps` - comma separated list of blacklisted applications IDs, requests from which should not be processed. diff --git a/docs/developers/default-request.md b/docs/developers/default-request.md new file mode 100644 index 00000000000..08bf1041009 --- /dev/null +++ b/docs/developers/default-request.md @@ -0,0 +1,29 @@ +# Server Based Global Default Request + +This allows a default request to be defined that allows the server to set up some defaults for all incoming +requests. A stored request(s) referenced by a bid request override default request, and of course any options specified +directly in the bid request override both. The default request is only read on server startup, it is meant as an +installation static default rather than a dynamic tuning option. + +## Config Options + +Following config options are exposed to support this feature. +```yaml +default-request: + file: + path : path/to/default/request.json +``` + +The `path` option is the path to a JSON file containing the default request JSON. +```json +{ + "tmax": "2000", + "regs": { + "ext": { + "gdpr": 1 + } + } +} +``` +This will be JSON merged into the incoming requests at the top level. These will be used as fallbacks which can be +overridden by both Stored Requests _and_ the incoming HTTP request payload. diff --git a/src/main/java/org/prebid/server/auction/FpdResolver.java b/src/main/java/org/prebid/server/auction/FpdResolver.java index ba6643be2b7..2eab1a9229d 100644 --- a/src/main/java/org/prebid/server/auction/FpdResolver.java +++ b/src/main/java/org/prebid/server/auction/FpdResolver.java @@ -10,6 +10,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.proto.openrtb.ext.request.ExtApp; import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfig; import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigFpd; @@ -21,7 +22,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.request.Targeting; -import org.prebid.server.util.JsonMergeUtil; import java.util.ArrayList; import java.util.Arrays; @@ -53,12 +53,11 @@ public class FpdResolver { "privacypolicy", "mobile")); private final JacksonMapper jacksonMapper; - private final JsonMergeUtil jsonMergeUtil; + private final JsonMerger jsonMerger; - public FpdResolver(JacksonMapper jacksonMapper) { + public FpdResolver(JacksonMapper jacksonMapper, JsonMerger jsonMerger) { this.jacksonMapper = Objects.requireNonNull(jacksonMapper); - - this.jsonMergeUtil = new JsonMergeUtil(jacksonMapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); } public User resolveUser(User originUser, ObjectNode fpdUser) { @@ -183,7 +182,7 @@ public ObjectNode resolveImpExt(ObjectNode impExt, ObjectNode targeting) { : null; final ObjectNode resolvedData = extImpContextData != null - ? (ObjectNode) jsonMergeUtil.merge(targeting, extImpContextData) + ? (ObjectNode) jsonMerger.merge(targeting, extImpContextData) : targeting; return extImpContext != null && extImpContext.isObject() @@ -253,7 +252,7 @@ private ObjectNode mergeExtData(JsonNode fpdData, JsonNode originData) { } if (originData != null && originData.isObject()) { - return (ObjectNode) jsonMergeUtil.merge(fpdData, originData); + return (ObjectNode) jsonMerger.merge(fpdData, originData); } return fpdData.isObject() ? (ObjectNode) fpdData : null; } diff --git a/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java b/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java index fbe5eaf63b5..9773811950d 100644 --- a/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java +++ b/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java @@ -12,7 +12,7 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.util.JsonMergeUtil; +import org.prebid.server.json.JsonMerger; import java.util.ArrayList; import java.util.Arrays; @@ -69,11 +69,11 @@ public class OrtbTypesResolver { } private final JacksonMapper jacksonMapper; - private final JsonMergeUtil jsonMergeUtil; + private final JsonMerger jsonMerger; - public OrtbTypesResolver(JacksonMapper jacksonMapper) { + public OrtbTypesResolver(JacksonMapper jacksonMapper, JsonMerger jsonMerger) { this.jacksonMapper = Objects.requireNonNull(jacksonMapper); - this.jsonMergeUtil = new JsonMergeUtil(jacksonMapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); } /** @@ -254,7 +254,7 @@ public void normalizeDataExtension(ObjectNode containerNode, String containerNam final JsonNode extData = containerNode.path(EXT).path(DATA); final JsonNode ext = containerNode.get(EXT); if (!extData.isNull() && !extData.isMissingNode()) { - final JsonNode resolvedExtData = jsonMergeUtil.merge(extData, data); + final JsonNode resolvedExtData = jsonMerger.merge(extData, data); ((ObjectNode) ext).set(DATA, resolvedExtData); } else { copyDataToExtData(containerNode, containerName, nodePrefix, warnings, data); diff --git a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java index 2b156d2b379..37260870277 100644 --- a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java @@ -5,12 +5,14 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Video; import io.vertx.core.Future; +import io.vertx.core.file.FileSystem; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; @@ -20,7 +22,6 @@ import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.VideoStoredDataResult; -import org.prebid.server.util.JsonMergeUtil; import java.util.ArrayList; import java.util.Collections; @@ -39,25 +40,48 @@ public class StoredRequestProcessor { private final long defaultTimeout; + private final BidRequest defaultBidRequest; private final ApplicationSettings applicationSettings; private final TimeoutFactory timeoutFactory; private final Metrics metrics; private final JacksonMapper mapper; - private JsonMergeUtil jsonMergeUtil; + private final JsonMerger jsonMerger; - public StoredRequestProcessor(long defaultTimeout, - ApplicationSettings applicationSettings, - Metrics metrics, - TimeoutFactory timeoutFactory, - JacksonMapper mapper) { + private StoredRequestProcessor(long defaultTimeout, + BidRequest defaultBidRequest, + ApplicationSettings applicationSettings, + Metrics metrics, + TimeoutFactory timeoutFactory, + JacksonMapper mapper, + JsonMerger jsonMerger) { this.defaultTimeout = defaultTimeout; - this.applicationSettings = Objects.requireNonNull(applicationSettings); - this.timeoutFactory = Objects.requireNonNull(timeoutFactory); - this.metrics = Objects.requireNonNull(metrics); - this.mapper = Objects.requireNonNull(mapper); + this.defaultBidRequest = defaultBidRequest; + this.applicationSettings = applicationSettings; + this.timeoutFactory = timeoutFactory; + this.metrics = metrics; + this.mapper = mapper; + this.jsonMerger = jsonMerger; + } - jsonMergeUtil = new JsonMergeUtil(mapper); + public static StoredRequestProcessor create(long defaultTimeout, + String defaultBidRequestPath, + FileSystem fileSystem, + ApplicationSettings applicationSettings, + Metrics metrics, + TimeoutFactory timeoutFactory, + JacksonMapper mapper, + JsonMerger jsonMerger) { + + return new StoredRequestProcessor( + defaultTimeout, + readBidRequest( + defaultBidRequestPath, Objects.requireNonNull(fileSystem), Objects.requireNonNull(mapper)), + Objects.requireNonNull(applicationSettings), + Objects.requireNonNull(metrics), + Objects.requireNonNull(timeoutFactory), + Objects.requireNonNull(mapper), + Objects.requireNonNull(jsonMerger)); } /** @@ -89,25 +113,15 @@ Future processStoredRequests(String accountId, BidRequest bidRequest applicationSettings.getStoredData(accountId, requestIds, impIds, timeout(bidRequest)) .compose(storedDataResult -> updateMetrics(storedDataResult, requestIds, impIds)); - return storedRequestsToBidRequest(storedDataFuture, bidRequest, - bidRequestToStoredRequestId.get(bidRequest), impToStoredRequestId); - } - - private Future updateMetrics(StoredDataResult storedDataResult, Set requestIds, - Set impIds) { - requestIds.forEach( - id -> metrics.updateStoredRequestMetric(storedDataResult.getStoredIdToRequest().containsKey(id))); - impIds.forEach( - id -> metrics.updateStoredImpsMetric(storedDataResult.getStoredIdToImp().containsKey(id))); - - return Future.succeededFuture(storedDataResult); + return storedRequestsToBidRequest( + storedDataFuture, bidRequest, bidRequestToStoredRequestId.get(bidRequest), impToStoredRequestId); } /** * Fetches AMP request from the source. */ Future processAmpRequest(String accountId, String ampRequestId) { - final BidRequest bidRequest = BidRequest.builder().build(); + final BidRequest bidRequest = defaultBidRequest != null ? defaultBidRequest : BidRequest.builder().build(); final Future ampStoredDataFuture = applicationSettings.getAmpStoredData( accountId, Collections.singleton(ampRequestId), Collections.emptySet(), timeout(bidRequest)) @@ -132,9 +146,27 @@ Future videoStoredDataResult(String accountId, List .map(storedDataResult -> makeVideoStoredDataResult(storedDataResult, storedIdToImpId, errors)); } + private Future updateMetrics(StoredDataResult storedDataResult, Set requestIds, + Set impIds) { + requestIds.forEach( + id -> metrics.updateStoredRequestMetric(storedDataResult.getStoredIdToRequest().containsKey(id))); + impIds.forEach(id -> metrics.updateStoredImpsMetric(storedDataResult.getStoredIdToImp().containsKey(id))); + + return Future.succeededFuture(storedDataResult); + } + + private static BidRequest readBidRequest( + String defaultBidRequestPath, FileSystem fileSystem, JacksonMapper mapper) { + + return StringUtils.isNotBlank(defaultBidRequestPath) + ? mapper.decodeValue(fileSystem.readFileBlocking(defaultBidRequestPath), BidRequest.class) + : null; + } + private VideoStoredDataResult makeVideoStoredDataResult(StoredDataResult storedDataResult, Map storedIdToImpId, List errors) { + final Map storedIdToStoredImp = storedDataResult.getStoredIdToImp(); final Map impIdToStoredVideo = new HashMap<>(); @@ -172,8 +204,10 @@ private Video parseVideoFromImp(String storedJson) { } private Future storedRequestsToBidRequest(Future storedDataFuture, - BidRequest bidRequest, String storedBidRequestId, + BidRequest bidRequest, + String storedBidRequestId, Map impsToStoredRequestId) { + return storedDataFuture .recover(exception -> Future.failedFuture(new InvalidRequestException( String.format("Stored request fetching failed: %s", exception.getMessage())))) @@ -187,21 +221,31 @@ private Future storedRequestsToBidRequest(Future s /** * Runs {@link BidRequest} and {@link Imp}s merge processes. */ - private BidRequest mergeBidRequestAndImps(BidRequest bidRequest, String storedRequestId, - Map impToStoredId, StoredDataResult storedDataResult) { - return mergeBidRequestImps(mergeBidRequest(bidRequest, storedRequestId, storedDataResult), - impToStoredId, storedDataResult); + private BidRequest mergeBidRequestAndImps(BidRequest bidRequest, + String storedRequestId, + Map impToStoredId, + StoredDataResult storedDataResult) { + + return mergeBidRequestImps( + mergeBidRequest(mergeDefaultRequest(bidRequest), storedRequestId, storedDataResult), + impToStoredId, + storedDataResult); + } + + private BidRequest mergeDefaultRequest(BidRequest bidRequest) { + return jsonMerger.merge(bidRequest, defaultBidRequest, BidRequest.class); } /** * Merges original request with request from stored request source. Values from original request * has higher priority than stored request values. */ - private BidRequest mergeBidRequest(BidRequest originalRequest, String storedRequestId, - StoredDataResult storedDataResult) { + private BidRequest mergeBidRequest( + BidRequest originalRequest, String storedRequestId, StoredDataResult storedDataResult) { + final String storedRequest = storedDataResult.getStoredIdToRequest().get(storedRequestId); return StringUtils.isNotBlank(storedRequestId) - ? jsonMergeUtil.merge(originalRequest, storedRequest, storedRequestId, BidRequest.class) + ? jsonMerger.merge(originalRequest, storedRequest, storedRequestId, BidRequest.class) : originalRequest; } @@ -209,8 +253,9 @@ private BidRequest mergeBidRequest(BidRequest originalRequest, String storedRequ * Merges {@link Imp}s from original request with Imps from stored request source. Values from original request * has higher priority than stored request values. */ - private BidRequest mergeBidRequestImps(BidRequest bidRequest, Map impToStoredId, - StoredDataResult storedDataResult) { + private BidRequest mergeBidRequestImps( + BidRequest bidRequest, Map impToStoredId, StoredDataResult storedDataResult) { + if (impToStoredId.isEmpty()) { return bidRequest; } @@ -220,7 +265,7 @@ private BidRequest mergeBidRequestImps(BidRequest bidRequest, Map i final String storedRequestId = impToStoredId.get(imp); if (storedRequestId != null) { final String storedImp = storedDataResult.getStoredIdToImp().get(storedRequestId); - final Imp mergedImp = jsonMergeUtil.merge(imp, storedImp, storedRequestId, Imp.class); + final Imp mergedImp = jsonMerger.merge(imp, storedImp, storedRequestId, Imp.class); mergedImps.set(i, mergedImp); } } @@ -228,10 +273,10 @@ private BidRequest mergeBidRequestImps(BidRequest bidRequest, Map i } /** - * Maps object to its StoredRequestId if exists. If object's extension contains storedRequest field, expected that - * it includes id too, in another case error about missed id in stored request will be added to error list. - * Gathers all errors into list, and in case if it is not empty, throws {@link InvalidRequestException} with - * list of errors. + * Maps object to its StoredRequestId if exists. If object's extension contains storedRequest field, expected + * that it includes id too, in another case error about missed id in stored request will be added to error list. + * Gathers all errors into list, and in case if it is not empty, throws {@link InvalidRequestException} with list + * of errors. */ private Map mapStoredRequestHolderToStoredRequestId( List storedRequestHolders, Function storedRequestExtractor) { diff --git a/src/main/java/org/prebid/server/auction/VideoRequestFactory.java b/src/main/java/org/prebid/server/auction/VideoRequestFactory.java index f815cf27da7..556eedcb9c8 100644 --- a/src/main/java/org/prebid/server/auction/VideoRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/VideoRequestFactory.java @@ -42,10 +42,13 @@ public class VideoRequestFactory { private final TimeoutResolver timeoutResolver; private final JacksonMapper mapper; - public VideoRequestFactory(int maxRequestSize, boolean enforceStoredRequest, + public VideoRequestFactory(int maxRequestSize, + boolean enforceStoredRequest, VideoStoredRequestProcessor storedRequestProcessor, - AuctionRequestFactory auctionRequestFactory, TimeoutResolver timeoutResolver, + AuctionRequestFactory auctionRequestFactory, + TimeoutResolver timeoutResolver, JacksonMapper mapper) { + this.enforceStoredRequest = enforceStoredRequest; this.maxRequestSize = maxRequestSize; this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); @@ -175,8 +178,9 @@ private Future> createBidRequest(RoutingContext routin BidRequestVideo bidRequestVideo, String storedVideoId, Set podConfigIds) { - return storedRequestProcessor.processVideoRequest(accountIdFrom(bidRequestVideo), storedVideoId, podConfigIds, - bidRequestVideo) + + return storedRequestProcessor.processVideoRequest( + accountIdFrom(bidRequestVideo), storedVideoId, podConfigIds, bidRequestVideo) .map(bidRequestToErrors -> fillImplicitParameters(routingContext, bidRequestToErrors)) .map(this::validateRequest); } diff --git a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java index 0b87f2a49d3..65f95ba330f 100644 --- a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java @@ -16,6 +16,7 @@ import com.iab.openrtb.request.video.PodError; import com.iab.openrtb.request.video.Podconfig; import io.vertx.core.Future; +import io.vertx.core.file.FileSystem; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; @@ -27,6 +28,7 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -36,7 +38,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.StoredDataResult; -import org.prebid.server.util.JsonMergeUtil; import org.prebid.server.validation.VideoRequestValidator; import java.util.ArrayList; @@ -63,39 +64,69 @@ public class VideoStoredRequestProcessor { private final long defaultTimeout; private final String currency; private final BidRequest defaultBidRequest; - private final VideoRequestValidator validator; private final ApplicationSettings applicationSettings; + private final VideoRequestValidator validator; private final Metrics metrics; - private final TimeoutFactory timeoutFactory; private final TimeoutResolver timeoutResolver; + private final TimeoutFactory timeoutFactory; private final JacksonMapper mapper; - private final JsonMergeUtil jsonMergeUtil; - - public VideoStoredRequestProcessor(boolean enforceStoredRequest, - List blacklistedAccounts, - long defaultTimeout, - String adServerCurrency, - BidRequest defaultBidRequest, - VideoRequestValidator validator, - ApplicationSettings applicationSettings, - Metrics metrics, - TimeoutFactory timeoutFactory, - TimeoutResolver timeoutResolver, - JacksonMapper mapper) { + private final JsonMerger jsonMerger; + + private VideoStoredRequestProcessor(boolean enforceStoredRequest, + List blacklistedAccounts, + long defaultTimeout, + String adServerCurrency, + BidRequest defaultBidRequest, + ApplicationSettings applicationSettings, + VideoRequestValidator validator, + Metrics metrics, + TimeoutFactory timeoutFactory, + TimeoutResolver timeoutResolver, + JacksonMapper mapper, + JsonMerger jsonMerger) { this.enforceStoredRequest = enforceStoredRequest; this.blacklistedAccounts = blacklistedAccounts; this.defaultTimeout = defaultTimeout; this.currency = StringUtils.isBlank(adServerCurrency) ? DEFAULT_CURRENCY : adServerCurrency; - this.defaultBidRequest = Objects.requireNonNull(defaultBidRequest); - this.validator = Objects.requireNonNull(validator); - this.applicationSettings = Objects.requireNonNull(applicationSettings); - this.metrics = Objects.requireNonNull(metrics); - this.timeoutFactory = Objects.requireNonNull(timeoutFactory); - this.timeoutResolver = Objects.requireNonNull(timeoutResolver); - this.mapper = Objects.requireNonNull(mapper); - - jsonMergeUtil = new JsonMergeUtil(mapper); + this.defaultBidRequest = defaultBidRequest; + this.applicationSettings = applicationSettings; + this.validator = validator; + this.metrics = metrics; + this.timeoutFactory = timeoutFactory; + this.timeoutResolver = timeoutResolver; + this.mapper = mapper; + this.jsonMerger = jsonMerger; + } + + public static VideoStoredRequestProcessor create(boolean enforceStoredRequest, + List blacklistedAccounts, + long defaultTimeout, + String adServerCurrency, + String defaultBidRequestPath, + FileSystem fileSystem, + ApplicationSettings applicationSettings, + VideoRequestValidator validator, + Metrics metrics, + TimeoutFactory timeoutFactory, + TimeoutResolver timeoutResolver, + JacksonMapper mapper, + JsonMerger jsonMerger) { + + return new VideoStoredRequestProcessor( + enforceStoredRequest, + Objects.requireNonNull(blacklistedAccounts), + defaultTimeout, + adServerCurrency, + readBidRequest( + defaultBidRequestPath, Objects.requireNonNull(fileSystem), Objects.requireNonNull(mapper)), + Objects.requireNonNull(applicationSettings), + Objects.requireNonNull(validator), + Objects.requireNonNull(metrics), + Objects.requireNonNull(timeoutFactory), + Objects.requireNonNull(timeoutResolver), + Objects.requireNonNull(mapper), + Objects.requireNonNull(jsonMerger)); } /** @@ -116,6 +147,14 @@ Future> processVideoRequest(String accountId, String s String.format("Stored request fetching failed: %s", exception.getMessage())))); } + private static BidRequest readBidRequest( + String defaultBidRequestPath, FileSystem fileSystem, JacksonMapper mapper) { + + return StringUtils.isNotBlank(defaultBidRequestPath) + ? mapper.decodeValue(fileSystem.readFileBlocking(defaultBidRequestPath), BidRequest.class) + : null; + } + private Future updateMetrics(StoredDataResult storedDataResult, Set requestIds, Set impIds) { requestIds.forEach( @@ -150,7 +189,7 @@ private BidRequestVideo mergeBidRequest(BidRequestVideo originalRequest, String } return StringUtils.isNotBlank(storedRequest) - ? jsonMergeUtil.merge(originalRequest, storedRequest, storedRequestId, BidRequestVideo.class) + ? jsonMerger.merge(originalRequest, storedRequest, storedRequestId, BidRequestVideo.class) : originalRequest; } @@ -256,7 +295,9 @@ private static Video updateVideo(Video video, Integer maxDuration, Integer minDu } private BidRequest mergeWithDefaultBidRequest(BidRequestVideo validatedVideoRequest, List imps) { - final BidRequest.BidRequestBuilder bidRequestBuilder = defaultBidRequest.toBuilder(); + final BidRequest.BidRequestBuilder bidRequestBuilder = + defaultBidRequest != null ? defaultBidRequest.toBuilder() : BidRequest.builder(); + final Site site = validatedVideoRequest.getSite(); if (site != null) { final Site.SiteBuilder siteBuilder = site.toBuilder(); diff --git a/src/main/java/org/prebid/server/util/JsonMergeUtil.java b/src/main/java/org/prebid/server/json/JsonMerger.java similarity index 94% rename from src/main/java/org/prebid/server/util/JsonMergeUtil.java rename to src/main/java/org/prebid/server/json/JsonMerger.java index de5b4371095..ee21248df27 100644 --- a/src/main/java/org/prebid/server/util/JsonMergeUtil.java +++ b/src/main/java/org/prebid/server/json/JsonMerger.java @@ -1,4 +1,4 @@ -package org.prebid.server.util; +package org.prebid.server.json; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -6,17 +6,15 @@ import com.github.fge.jsonpatch.mergepatch.JsonMergePatch; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.json.JacksonMapper; import java.io.IOException; import java.util.Objects; -// TODO: refactor to be instance instead of util -public class JsonMergeUtil { +public class JsonMerger { private final JacksonMapper mapper; - public JsonMergeUtil(JacksonMapper mapper) { + public JsonMerger(JacksonMapper mapper) { this.mapper = Objects.requireNonNull(mapper); } diff --git a/src/main/java/org/prebid/server/spring/config/JacksonConfiguration.java b/src/main/java/org/prebid/server/spring/config/JsonConfiguration.java similarity index 68% rename from src/main/java/org/prebid/server/spring/config/JacksonConfiguration.java rename to src/main/java/org/prebid/server/spring/config/JsonConfiguration.java index 52239591ca5..eda4e2fb32d 100644 --- a/src/main/java/org/prebid/server/spring/config/JacksonConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/JsonConfiguration.java @@ -1,15 +1,21 @@ package org.prebid.server.spring.config; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.json.ObjectMapperProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration -public class JacksonConfiguration { +public class JsonConfiguration { @Bean JacksonMapper jacksonMapper() { return new JacksonMapper(ObjectMapperProvider.mapper()); } + + @Bean + JsonMerger jsonMerger(JacksonMapper mapper) { + return new JsonMerger(mapper); + } } diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 637a95eddf4..48acafe055b 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -1,9 +1,9 @@ package org.prebid.server.spring.config; -import com.iab.openrtb.request.BidRequest; import de.malkusch.whoisServerList.publicSuffixList.PublicSuffixList; import de.malkusch.whoisServerList.publicSuffixList.PublicSuffixListFactory; import io.vertx.core.Vertx; +import io.vertx.core.file.FileSystem; import io.vertx.core.http.HttpClientOptions; import io.vertx.core.net.JksOptions; import org.prebid.server.auction.AmpRequestFactory; @@ -42,6 +42,7 @@ import org.prebid.server.identity.NoneIdGenerator; import org.prebid.server.identity.UUIDIdGenerator; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.log.LoggerControlKnob; import org.prebid.server.metric.Metrics; @@ -124,13 +125,13 @@ IpAddressHelper ipAddressHelper(@Value("${ipv6.always-mask-right}") int ipv6Alwa } @Bean - FpdResolver fpdResolver(JacksonMapper mapper) { - return new FpdResolver(mapper); + FpdResolver fpdResolver(JacksonMapper mapper, JsonMerger jsonMerger) { + return new FpdResolver(mapper, jsonMerger); } @Bean - OrtbTypesResolver ortbTypesResolver(JacksonMapper jacksonMapper) { - return new OrtbTypesResolver(jacksonMapper); + OrtbTypesResolver ortbTypesResolver(JacksonMapper jacksonMapper, JsonMerger jsonMerger) { + return new OrtbTypesResolver(jacksonMapper, jsonMerger); } @Bean @@ -283,32 +284,35 @@ VideoStoredRequestProcessor videoStoredRequestProcessor( @Value("${auction.blacklisted-accounts}") String blacklistedAccountsString, @Value("${video.stored-requests-timeout-ms}") long defaultTimeoutMs, @Value("${auction.ad-server-currency:#{null}}") String adServerCurrency, - BidRequest defaultVideoBidRequest, + @Value("${default-request.file.path:#{null}}") String defaultBidRequestPath, + FileSystem fileSystem, ApplicationSettings applicationSettings, + VideoRequestValidator videoRequestValidator, Metrics metrics, TimeoutFactory timeoutFactory, TimeoutResolver timeoutResolver, - JacksonMapper mapper) { - - final List blacklistedAccounts = splitCommaSeparatedString(blacklistedAccountsString); + JacksonMapper mapper, + JsonMerger jsonMerger) { - return new VideoStoredRequestProcessor( + return VideoStoredRequestProcessor.create( enforceStoredRequest, - blacklistedAccounts, + splitCommaSeparatedString(blacklistedAccountsString), defaultTimeoutMs, adServerCurrency, - defaultVideoBidRequest, - new VideoRequestValidator(), + defaultBidRequestPath, + fileSystem, applicationSettings, + videoRequestValidator, metrics, timeoutFactory, timeoutResolver, - mapper); + mapper, + jsonMerger); } @Bean - BidRequest defaultVideoBidRequest() { - return BidRequest.builder().build(); + VideoRequestValidator videoRequestValidator() { + return new VideoRequestValidator(); } @Bean @@ -500,12 +504,23 @@ ExchangeService exchangeService( @Bean StoredRequestProcessor storedRequestProcessor( @Value("${auction.stored-requests-timeout-ms}") long defaultTimeoutMs, + @Value("${default-request.file.path:#{null}}") String defaultBidRequestPath, + FileSystem fileSystem, ApplicationSettings applicationSettings, Metrics metrics, TimeoutFactory timeoutFactory, - JacksonMapper mapper) { + JacksonMapper mapper, + JsonMerger jsonMerger) { - return new StoredRequestProcessor(defaultTimeoutMs, applicationSettings, metrics, timeoutFactory, mapper); + return StoredRequestProcessor.create( + defaultTimeoutMs, + defaultBidRequestPath, + fileSystem, + applicationSettings, + metrics, + timeoutFactory, + mapper, + jsonMerger); } @Bean diff --git a/src/test/java/org/prebid/server/auction/FpdResolverTest.java b/src/test/java/org/prebid/server/auction/FpdResolverTest.java index bb0c6def595..46e9bec4fda 100644 --- a/src/test/java/org/prebid/server/auction/FpdResolverTest.java +++ b/src/test/java/org/prebid/server/auction/FpdResolverTest.java @@ -14,6 +14,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; +import org.prebid.server.json.JsonMerger; import org.prebid.server.proto.openrtb.ext.request.ExtApp; import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfig; @@ -40,7 +41,7 @@ public class FpdResolverTest extends VertxTest { @Before public void setUp() { - fpdResolver = new FpdResolver(jacksonMapper); + fpdResolver = new FpdResolver(jacksonMapper, new JsonMerger(jacksonMapper)); } @Test diff --git a/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java b/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java index 3071553c82b..89ef6ec9751 100644 --- a/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java +++ b/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Test; import org.prebid.server.VertxTest; +import org.prebid.server.json.JsonMerger; import java.util.ArrayList; import java.util.List; @@ -13,7 +14,8 @@ public class OrtbTypesResolverTest extends VertxTest { - private final OrtbTypesResolver ortbTypesResolver = new OrtbTypesResolver(jacksonMapper); + private final OrtbTypesResolver ortbTypesResolver = + new OrtbTypesResolver(jacksonMapper, new JsonMerger(jacksonMapper)); @Test public void normalizeTargetingShouldNotChangeNodeIfItsTypeIsNotObject() { diff --git a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java index 4eae1ad5459..fe1911813ec 100644 --- a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java @@ -9,6 +9,8 @@ import com.iab.openrtb.request.Imp.ImpBuilder; import com.iab.openrtb.request.Video; import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.file.FileSystem; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -20,6 +22,7 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.json.JsonMerger; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; @@ -50,6 +53,7 @@ import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; @@ -57,9 +61,13 @@ public class StoredRequestProcessorTest extends VertxTest { + private static final int DEFAULT_TIMEOUT = 500; + @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock + private FileSystem fileSystem; @Mock private ApplicationSettings applicationSettings; @Mock @@ -70,12 +78,15 @@ public class StoredRequestProcessorTest extends VertxTest { @Before public void setUp() { final TimeoutFactory timeoutFactory = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault())); - storedRequestProcessor = new StoredRequestProcessor( - 500, + storedRequestProcessor = StoredRequestProcessor.create( + DEFAULT_TIMEOUT, + null, + fileSystem, applicationSettings, metrics, timeoutFactory, - jacksonMapper); + jacksonMapper, + new JsonMerger(jacksonMapper)); } @Test @@ -156,6 +167,52 @@ public void shouldReturnMergedBidRequest() throws IOException { .build()); } + @Test + public void shouldReturnMergedDefaultAndBidRequest() throws IOException { + // given + given(fileSystem.readFileBlocking(anyString())) + .willReturn(Buffer.buffer(mapper.writeValueAsString(BidRequest.builder().at(1).build()))); + + final TimeoutFactory timeoutFactory = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault())); + storedRequestProcessor = StoredRequestProcessor.create( + DEFAULT_TIMEOUT, + "path/to/default/request.json", + fileSystem, + applicationSettings, + metrics, + timeoutFactory, + jacksonMapper, + new JsonMerger(jacksonMapper)); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .storedrequest(ExtStoredRequest.of("123")) + .build()))); + + final String storedRequestBidRequestJson = mapper.writeValueAsString(BidRequest.builder() + .id("test-request-id") + .tmax(1000L) + .imp(singletonList(Imp.builder().build())) + .build()); + given(applicationSettings.getStoredData(any(), anySet(), anySet(), any())) + .willReturn(Future.succeededFuture( + StoredDataResult.of(singletonMap("123", storedRequestBidRequestJson), emptyMap(), + emptyList()))); + + // when + final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest); + + // then + assertThat(bidRequestFuture.succeeded()).isTrue(); + assertThat(bidRequestFuture.result()).isEqualTo(BidRequest.builder() + .id("test-request-id") + .at(1) + .tmax(1000L) + .imp(singletonList(Imp.builder().build())) + .ext(ExtRequest.of(ExtRequestPrebid.builder().storedrequest(ExtStoredRequest.of("123")).build())) + .build()); + } + @Test public void shouldReturnAmpRequest() throws IOException { // given @@ -174,6 +231,39 @@ public void shouldReturnAmpRequest() throws IOException { .build()); } + @Test + public void shouldReturnMergedDefaultAndAmpRequest() throws IOException { + // given + given(fileSystem.readFileBlocking(anyString())) + .willReturn(Buffer.buffer(mapper.writeValueAsString(BidRequest.builder().at(1).build()))); + + final TimeoutFactory timeoutFactory = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault())); + storedRequestProcessor = StoredRequestProcessor.create( + DEFAULT_TIMEOUT, + "path/to/default/request.json", + fileSystem, + applicationSettings, + metrics, + timeoutFactory, + jacksonMapper, + new JsonMerger(jacksonMapper)); + + given(applicationSettings.getAmpStoredData(any(), anySet(), anySet(), any())) + .willReturn(Future.succeededFuture(StoredDataResult.of( + singletonMap("123", mapper.writeValueAsString( + BidRequest.builder().id("test-request-id").build())), emptyMap(), emptyList()))); + + // when + final Future bidRequestFuture = storedRequestProcessor.processAmpRequest(null, "123"); + + // then + assertThat(bidRequestFuture.succeeded()).isTrue(); + assertThat(bidRequestFuture.result()).isEqualTo(BidRequest.builder() + .id("test-request-id") + .at(1) + .build()); + } + @Test public void shouldReturnFailedFutureWhenStoredBidRequestJsonIsNotValid() { // given diff --git a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java index c1eb0f71069..ca604d7b3cd 100644 --- a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java @@ -1,5 +1,6 @@ package org.prebid.server.auction; +import com.fasterxml.jackson.core.JsonProcessingException; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Content; import com.iab.openrtb.request.Imp; @@ -12,6 +13,8 @@ import com.iab.openrtb.request.video.PodError; import com.iab.openrtb.request.video.Podconfig; import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.file.FileSystem; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -22,6 +25,7 @@ import org.prebid.server.auction.model.WithPodErrors; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.json.JsonMerger; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -46,6 +50,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.never; @@ -58,6 +63,8 @@ public class VideoStoredRequestProcessorTest extends VertxTest { @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock + private FileSystem fileSystem; @Mock private ApplicationSettings applicationSettings; @Mock @@ -72,19 +79,24 @@ public class VideoStoredRequestProcessorTest extends VertxTest { private VideoStoredRequestProcessor target; @Before - public void setUp() { - target = new VideoStoredRequestProcessor( + public void setUp() throws JsonProcessingException { + given(fileSystem.readFileBlocking(anyString())) + .willReturn(Buffer.buffer(mapper.writeValueAsString(BidRequest.builder().at(1).build()))); + + target = VideoStoredRequestProcessor.create( false, emptyList(), 2000L, "USD", - BidRequest.builder().build(), - validator, + "path/to/default/request.json", + fileSystem, applicationSettings, + validator, metrics, timeoutFactory, timeoutResolver, - jacksonMapper); + jacksonMapper, + new JsonMerger(jacksonMapper)); } @Test @@ -171,6 +183,7 @@ public void shouldReturnFutureWithMergedStoredAndDefaultRequest() { .build(); final BidRequest expectedMergedRequest = BidRequest.builder() .id("bid_id") + .at(1) .imp(Arrays.asList(expectedImp1, expectedImp2)) .user(User.builder().buyeruid("appnexus").yob(123).gender("gender").keywords("keywords").build()) .site(Site.builder().id("siteId").content(content).build()) diff --git a/src/test/java/org/prebid/server/util/JsonMergeUtilTest.java b/src/test/java/org/prebid/server/json/JsonMergerTest.java similarity index 94% rename from src/test/java/org/prebid/server/util/JsonMergeUtilTest.java rename to src/test/java/org/prebid/server/json/JsonMergerTest.java index b355617bfad..7ac8e4432f9 100644 --- a/src/test/java/org/prebid/server/util/JsonMergeUtilTest.java +++ b/src/test/java/org/prebid/server/json/JsonMergerTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.util; +package org.prebid.server.json; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; @@ -11,13 +11,13 @@ import static org.assertj.core.api.Assertions.assertThat; -public class JsonMergeUtilTest extends VertxTest { +public class JsonMergerTest extends VertxTest { - private JsonMergeUtil target; + private JsonMerger target; @Before public void setUp() { - target = new JsonMergeUtil(jacksonMapper); + target = new JsonMerger(jacksonMapper); } @Test From e8f6b8ff8c82b27aed4c45987bf4d4b9d0c3d615 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Thu, 17 Dec 2020 15:16:12 +0200 Subject: [PATCH 068/129] Add bid size and secure markup validations, controlled by host through configuration (#830) * Add bid size and secure markup validations, controlled by host through configuration * Add account-level bid validations overrides * Apply account level overrides for bid validations * Represent sizes list as string * Update validation error metrics * Include new configuration properties in documentation * Fix broken test * Make use of BidderAliases to correctly resolve aliases on metrics reporting * Change validation logic for banners: validate max size if enforced by configuration or account * Change validation logic for secure creative: look for secure markers in addition to verifying there is no insecure markers * Add warnings support for bids validation * Fix compilation error after merge * Update validation metrics * Fix documentation and remove obsolete class --- docs/application-settings.md | 18 +- docs/config-app.md | 2 + docs/metrics.md | 4 + .../server/auction/ExchangeService.java | 66 ++- .../prebid/server/metric/AccountMetrics.java | 6 + .../prebid/server/metric/AdapterMetrics.java | 7 + .../org/prebid/server/metric/MetricName.java | 3 + .../org/prebid/server/metric/Metrics.java | 10 + .../prebid/server/metric/ResponseMetrics.java | 33 ++ .../metric/SpecificValidationMetrics.java | 27 ++ .../server/metric/ValidationMetrics.java | 41 ++ .../settings/JdbcApplicationSettings.java | 9 +- .../prebid/server/settings/model/Account.java | 2 + .../model/AccountBidValidationConfig.java | 11 + .../model/BidValidationEnforcement.java | 6 + .../spring/config/ServiceConfiguration.java | 23 +- .../validation/ResponseBidValidator.java | 173 ++++++- .../validation/model/ValidationResult.java | 20 +- src/main/resources/application.yaml | 3 + .../auction/AuctionRequestFactoryTest.java | 3 +- .../server/auction/ExchangeServiceTest.java | 72 ++- .../org/prebid/server/metric/MetricsTest.java | 30 ++ .../settings/FileApplicationSettingsTest.java | 6 + .../settings/JdbcApplicationSettingsTest.java | 10 +- .../validation/ResponseBidValidatorTest.java | 453 ++++++++++++++++-- 25 files changed, 930 insertions(+), 108 deletions(-) create mode 100644 src/main/java/org/prebid/server/metric/ResponseMetrics.java create mode 100644 src/main/java/org/prebid/server/metric/SpecificValidationMetrics.java create mode 100644 src/main/java/org/prebid/server/metric/ValidationMetrics.java create mode 100644 src/main/java/org/prebid/server/settings/model/AccountBidValidationConfig.java create mode 100644 src/main/java/org/prebid/server/settings/model/BidValidationEnforcement.java diff --git a/docs/application-settings.md b/docs/application-settings.md index b6e5b7bc8b6..f2f33b1b499 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -23,6 +23,7 @@ There are two ways to configure application settings: database and file. This do - `truncate-target-attr` - Maximum targeting attributes size. Values between 1 and 255. - `default-integration` - Default integration to assume. - `analytics-config.auction-events.` - defines which channels are supported by analytics for this account +- `bid-validations.banner-creative-max-size` - Overrides creative max size validation for banners. ``` Purpose | Purpose goal | Purpose meaning for PBS (n\a - not affected) @@ -187,6 +188,7 @@ Query to create accounts_account table: `truncate_target_attr` tinyint(3) unsigned DEFAULT NULL, `default_integration` varchar(64) DEFAULT NULL, `analytics_config` varchar(512) DEFAULT NULL, +`bid_validations` json DEFAULT NULL, `status` enum('active','inactive') DEFAULT 'active', `updated_by` int(11) DEFAULT NULL, `updated_by_user` varchar(64) DEFAULT NULL, @@ -198,7 +200,7 @@ ENGINE=InnoDB DEFAULT CHARSET=utf8' where tcf_config column is json with next format -``` +```json { "enabled": true, "integration-enabled": { @@ -206,8 +208,8 @@ where tcf_config column is json with next format "web": true, "app": true, "amp": true - } - "purpose-one-treatment-interpretation": "ignore" + }, + "purpose-one-treatment-interpretation": "ignore", "purposes": { "p1": { "enforce-purpose": "full", @@ -309,9 +311,17 @@ where tcf_config column is json with next format } ``` +and bid_validations column is json with next format + +```json +{ + "banner-creative-max-size": "enforce" +} +``` + Query used to get an account: ``` -SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config +SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, bid_validations FROM accounts_account where uuid = ? LIMIT 1 diff --git a/docs/config-app.md b/docs/config-app.md index b748a5c6544..69d0b760303 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -79,6 +79,8 @@ Removes and downloads file again if depending service cant process probably corr - `auction.cache.only-winning-bids` - if equals to `true` only the winning bids would be cached. Has lower priority than request-specific flags. - `auction.generate-bid-id` - whether to generate seatbid[].bid[].ext.prebid.bidid in the OpenRTB response. - `auction.id-generator-type` - if generate-bid-id is on, then this defines how the ID should be generated. Currently onlye `uuid` is supported. +- `auction.validations.banner-creative-max-size` - enables creative max size validation for banners. Possible values: `skip`, `enforce`, `warn`. Default is `skip`. +- `auction.validations.secure-markup` - enables secure markup validation. Possible values: `skip`, `enforce`, `warn`. Default is `skip`. ## Amp (OpenRTB) - `amp.default-timeout-ms` - default operation timeout for OpenRTB Amp requests. diff --git a/docs/metrics.md b/docs/metrics.md index e8a3afecb48..7360cdb0d04 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -78,10 +78,14 @@ where `[DATASOURCE]` is a data source name, `DEFAULT_DS` by defaul. - `adapter..(openrtb2-web|openrtb-app|amp|legacy).tcf.geo_masked` - number of requests made to `` that required geo information removed as a result of TCF enforcement for that bidder - `adapter..(openrtb2-web|openrtb-app|amp|legacy).tcf.request_blocked` - number of requests made to `` that were blocked as a result of TCF enforcement for that bidder - `adapter..(openrtb2-web|openrtb-app|amp|legacy).tcf.analytics_blocked` - number of requests made to `` that required analytics blocked as a result of TCF enforcement for that bidder +- `adapter..response.validation.size.(warn|err)` - number of banner bids received from the `` that had invalid size +- `adapter..response.validation.secure.(warn|err)` - number of bids received from the `` that had insecure creative while in secure context ## Auction per-account metrics Following metrics are collected and submitted if account is configured with `basic` verbosity: - `account..requests` - number of requests received from account with `` +- `account..response.validation.size.(warn|err)` - number of banner bids received from account with `` that had invalid size +- `account..response.validation.secure.(warn|err)` - number of bids received from account with `` that had insecure creative while in secure context Following metrics are collected and submitted if account is configured with `detailed` verbosity: - `account..requests.type.(openrtb2-web,openrtb-app,amp,legacy)` - number of requests received from account with `` broken down by type of incoming request diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index bb6dfb17646..4d78f1a4415 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -154,18 +154,19 @@ public Future holdAuction(AuctionContext context) { .compose(impsRequiredRequest -> extractBidderRequests(context, impsRequiredRequest, aliases)) .map(bidderRequests -> updateRequestMetric( bidderRequests, uidsCookie, aliases, publisherId, context.getRequestTypeMetric())) - .compose(bidderRequests -> CompositeFuture.join(bidderRequests.stream() - .map(bidderRequest -> requestBids( - bidderRequest, - auctionTimeout(timeout, cacheInfo.isDoCaching()), - debugEnabled, - aliases)) - .collect(Collectors.toList()))) + .compose(bidderRequests -> CompositeFuture.join( + bidderRequests.stream() + .map(bidderRequest -> requestBids( + bidderRequest, + auctionTimeout(timeout, cacheInfo.isDoCaching()), + debugEnabled, + aliases)) + .collect(Collectors.toList()))) // send all the requests to the bidders and gathers results .map(CompositeFuture::list) .map(bidderResponses -> storedResponseProcessor.mergeWithBidderResponses( bidderResponses, storedResponse, bidRequest.getImp())) - .map(bidderResponses -> validateAndAdjustBids(bidRequest, bidderResponses)) + .map(bidderResponses -> validateAndAdjustBids(bidderResponses, context, aliases)) .map(bidderResponses -> updateMetricsFromResponses(bidderResponses, publisherId, aliases)) // produce response from bidder results .compose(bidderResponses -> bidResponseCreator.create( @@ -811,8 +812,10 @@ private static BigDecimal bidAdjustmentForBidder(BidRequest bidRequest, String b * Passes the request to a corresponding bidder and wraps response in {@link BidderResponse} which also holds * recorded response time. */ - private Future requestBids( - BidderRequest bidderRequest, Timeout timeout, boolean debugEnabled, BidderAliases aliases) { + private Future requestBids(BidderRequest bidderRequest, + Timeout timeout, + boolean debugEnabled, + BidderAliases aliases) { final String bidderName = bidderRequest.getBidder(); final Bidder bidder = bidderCatalog.bidderByName(aliases.resolveBidder(bidderName)); @@ -822,10 +825,12 @@ private Future requestBids( .map(seatBid -> BidderResponse.of(bidderName, seatBid, responseTime(startTime))); } - private List validateAndAdjustBids(BidRequest bidRequest, List bidderResponses) { + private List validateAndAdjustBids( + List bidderResponses, AuctionContext auctionContext, BidderAliases aliases) { + return bidderResponses.stream() - .map(bidderResponse -> validBidderResponse(bidderResponse, bidRequest.getCur())) - .map(bidderResponse -> applyBidPriceChanges(bidderResponse, bidRequest)) + .map(bidderResponse -> validBidderResponse(bidderResponse, auctionContext, aliases)) + .map(bidderResponse -> applyBidPriceChanges(bidderResponse, auctionContext.getBidRequest())) .collect(Collectors.toList()); } @@ -836,28 +841,37 @@ private List validateAndAdjustBids(BidRequest bidRequest, List * Returns input argument as the result if no errors found or creates new {@link BidderResponse} otherwise. */ - private BidderResponse validBidderResponse(BidderResponse bidderResponse, List requestCurrencies) { - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - final List bids = seatBid.getBids(); + private BidderResponse validBidderResponse( + BidderResponse bidderResponse, AuctionContext auctionContext, BidderAliases aliases) { - final List validBids = new ArrayList<>(bids.size()); + final BidRequest bidRequest = auctionContext.getBidRequest(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); final List errors = new ArrayList<>(seatBid.getErrors()); + final List requestCurrencies = bidRequest.getCur(); if (requestCurrencies.size() > 1) { errors.add(BidderError.badInput( String.format("Cur parameter contains more than one currency. %s will be used", requestCurrencies.get(0)))); } - for (BidderBid bid : bids) { - final ValidationResult validationResult = responseBidValidator.validate(bid); + final List bids = seatBid.getBids(); + final List validBids = new ArrayList<>(bids.size()); + + for (final BidderBid bid : bids) { + final ValidationResult validationResult = + responseBidValidator.validate(bid, bidderResponse.getBidder(), auctionContext, aliases); + + if (validationResult.hasWarnings()) { + addAsBidderErrors(validationResult.getWarnings(), errors); + } + if (validationResult.hasErrors()) { - for (String error : validationResult.getErrors()) { - errors.add(BidderError.generic(error)); - } - } else { - validBids.add(bid); + addAsBidderErrors(validationResult.getErrors(), errors); + continue; } + + validBids.add(bid); } return errors.isEmpty() @@ -865,6 +879,10 @@ private BidderResponse validBidderResponse(BidderResponse bidderResponse, List messages, List errors) { + messages.stream().map(BidderError::generic).forEach(errors::add); + } + /** * Performs changes on {@link Bid}s price depends on different between adServerCurrency and bidCurrency, * and adjustment factor. Will drop bid if currency conversion is needed but not possible. diff --git a/src/main/java/org/prebid/server/metric/AccountMetrics.java b/src/main/java/org/prebid/server/metric/AccountMetrics.java index 2dcfbba217a..80ae7853dd6 100644 --- a/src/main/java/org/prebid/server/metric/AccountMetrics.java +++ b/src/main/java/org/prebid/server/metric/AccountMetrics.java @@ -21,6 +21,7 @@ class AccountMetrics extends UpdatableMetrics { private final Map requestTypeMetrics; private final RequestMetrics requestsMetrics; private final CacheMetrics cacheMetrics; + private final ResponseMetrics responseMetrics; AccountMetrics(MetricRegistry metricRegistry, CounterType counterType, String account) { super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), @@ -32,6 +33,7 @@ class AccountMetrics extends UpdatableMetrics { requestTypeMetrics = new HashMap<>(); requestsMetrics = new RequestMetrics(metricRegistry, counterType, createPrefix(account)); cacheMetrics = new CacheMetrics(metricRegistry, counterType, createPrefix(account)); + responseMetrics = new ResponseMetrics(metricRegistry, counterType, createPrefix(account)); } private static String createPrefix(String account) { @@ -57,4 +59,8 @@ RequestMetrics requests() { CacheMetrics cache() { return cacheMetrics; } + + ResponseMetrics response() { + return responseMetrics; + } } diff --git a/src/main/java/org/prebid/server/metric/AdapterMetrics.java b/src/main/java/org/prebid/server/metric/AdapterMetrics.java index 80b406ec94a..2f98a32cdb1 100644 --- a/src/main/java/org/prebid/server/metric/AdapterMetrics.java +++ b/src/main/java/org/prebid/server/metric/AdapterMetrics.java @@ -17,6 +17,7 @@ class AdapterMetrics extends UpdatableMetrics { private final RequestMetrics requestMetrics; private final Function bidTypeMetricsCreator; private final Map bidTypeMetrics; + private final ResponseMetrics responseMetrics; AdapterMetrics(MetricRegistry metricRegistry, CounterType counterType, String adapterType) { super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), @@ -29,6 +30,7 @@ class AdapterMetrics extends UpdatableMetrics { requestTypeMetrics = new HashMap<>(); requestMetrics = new RequestMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType)); bidTypeMetrics = new HashMap<>(); + responseMetrics = new ResponseMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType)); } AdapterMetrics(MetricRegistry metricRegistry, CounterType counterType, String account, String adapterType) { @@ -44,6 +46,7 @@ class AdapterMetrics extends UpdatableMetrics { requestTypeMetrics = null; bidTypeMetricsCreator = null; bidTypeMetrics = null; + responseMetrics = null; } private static String createAdapterPrefix(String adapterType) { @@ -69,4 +72,8 @@ RequestMetrics request() { BidTypeMetrics forBidType(String bidType) { return bidTypeMetrics.computeIfAbsent(bidType, bidTypeMetricsCreator); } + + ResponseMetrics response() { + return responseMetrics; + } } diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index 524d032b883..e1580af8958 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -60,6 +60,9 @@ public enum MetricName { err, networkerr, + // bids validation + warn, + // cookie sync cookie_sync_requests, opt_outs, diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 53f05612ed2..5980206f0a4 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -266,6 +266,16 @@ public void updateAdapterRequestErrorMetric(String bidder, MetricName errorMetri forAdapter(resolveMetricsBidderName(bidder)).request().incCounter(errorMetric); } + public void updateSizeValidationMetrics(String bidder, String accountId, MetricName type) { + forAdapter(resolveMetricsBidderName(bidder)).response().validation().size().incCounter(type); + forAccount(accountId).response().validation().size().incCounter(type); + } + + public void updateSecureValidationMetrics(String bidder, String accountId, MetricName type) { + forAdapter(resolveMetricsBidderName(bidder)).response().validation().secure().incCounter(type); + forAccount(accountId).response().validation().secure().incCounter(type); + } + public void updateUserSyncOptoutMetric() { userSync().incCounter(MetricName.opt_outs); } diff --git a/src/main/java/org/prebid/server/metric/ResponseMetrics.java b/src/main/java/org/prebid/server/metric/ResponseMetrics.java new file mode 100644 index 00000000000..ba7e93fae81 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/ResponseMetrics.java @@ -0,0 +1,33 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Request metrics support. + */ +class ResponseMetrics extends UpdatableMetrics { + + private final ValidationMetrics validationMetrics; + + ResponseMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix)))); + + validationMetrics = new ValidationMetrics(metricRegistry, counterType, createPrefix(prefix)); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + private static String createPrefix(String prefix) { + return String.format("%s.response", prefix); + } + + ValidationMetrics validation() { + return validationMetrics; + } +} diff --git a/src/main/java/org/prebid/server/metric/SpecificValidationMetrics.java b/src/main/java/org/prebid/server/metric/SpecificValidationMetrics.java new file mode 100644 index 00000000000..4a1bc289860 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/SpecificValidationMetrics.java @@ -0,0 +1,27 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Request metrics support. + */ +class SpecificValidationMetrics extends UpdatableMetrics { + + SpecificValidationMetrics( + MetricRegistry metricRegistry, CounterType counterType, String prefix, String validation) { + + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix), Objects.requireNonNull(validation)))); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + private static String createPrefix(String prefix, String validation) { + return String.format("%s.%s", prefix, validation); + } +} diff --git a/src/main/java/org/prebid/server/metric/ValidationMetrics.java b/src/main/java/org/prebid/server/metric/ValidationMetrics.java new file mode 100644 index 00000000000..a00ebea15d2 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/ValidationMetrics.java @@ -0,0 +1,41 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Request metrics support. + */ +class ValidationMetrics extends UpdatableMetrics { + + private final SpecificValidationMetrics sizeValidationMetrics; + private final SpecificValidationMetrics secureValidationMetrics; + + ValidationMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix)))); + + sizeValidationMetrics = new SpecificValidationMetrics( + metricRegistry, counterType, createPrefix(prefix), "size"); + secureValidationMetrics = new SpecificValidationMetrics( + metricRegistry, counterType, createPrefix(prefix), "secure"); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + private static String createPrefix(String prefix) { + return String.format("%s.validation", prefix); + } + + SpecificValidationMetrics size() { + return sizeValidationMetrics; + } + + SpecificValidationMetrics secure() { + return secureValidationMetrics; + } +} diff --git a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java index 39f1e3c75b3..7ace747d3ca 100644 --- a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java @@ -13,6 +13,7 @@ import org.prebid.server.settings.helper.JdbcStoredResponseResultMapper; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAnalyticsConfig; +import org.prebid.server.settings.model.AccountBidValidationConfig; import org.prebid.server.settings.model.AccountGdprConfig; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; @@ -97,9 +98,10 @@ public JdbcApplicationSettings(JdbcClient jdbcClient, */ @Override public Future getAccountById(String accountId, Timeout timeout) { - return jdbcClient.executeQuery("SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl," - + " events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr," - + " default_integration, analytics_config FROM accounts_account where uuid = ? LIMIT 1", + return jdbcClient.executeQuery("SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, " + + "events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, " + + "default_integration, analytics_config, bid_validations " + + "FROM accounts_account where uuid = ? LIMIT 1", Collections.singletonList(accountId), result -> mapToModelOrError(result, row -> Account.builder() .id(row.getString(0)) @@ -113,6 +115,7 @@ public Future getAccountById(String accountId, Timeout timeout) { .truncateTargetAttr(row.getInteger(8)) .defaultIntegration(row.getString(9)) .analyticsConfig(toModel(row.getString(10), AccountAnalyticsConfig.class)) + .bidValidations(toModel(row.getString(11), AccountBidValidationConfig.class)) .build()), timeout) .compose(result -> failedIfNull(result, accountId, "Account")); diff --git a/src/main/java/org/prebid/server/settings/model/Account.java b/src/main/java/org/prebid/server/settings/model/Account.java index 0d011c9df31..7e736416298 100644 --- a/src/main/java/org/prebid/server/settings/model/Account.java +++ b/src/main/java/org/prebid/server/settings/model/Account.java @@ -29,6 +29,8 @@ public class Account { AccountAnalyticsConfig analyticsConfig; + AccountBidValidationConfig bidValidations; + public static Account empty(String id) { return Account.builder() .id(id) diff --git a/src/main/java/org/prebid/server/settings/model/AccountBidValidationConfig.java b/src/main/java/org/prebid/server/settings/model/AccountBidValidationConfig.java new file mode 100644 index 00000000000..38c5afb2bfb --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountBidValidationConfig.java @@ -0,0 +1,11 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountBidValidationConfig { + + @JsonProperty("banner-creative-max-size") + BidValidationEnforcement bannerMaxSizeEnforcement; +} diff --git a/src/main/java/org/prebid/server/settings/model/BidValidationEnforcement.java b/src/main/java/org/prebid/server/settings/model/BidValidationEnforcement.java new file mode 100644 index 00000000000..f5ed5a6f510 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/BidValidationEnforcement.java @@ -0,0 +1,6 @@ +package org.prebid.server.settings.model; + +public enum BidValidationEnforcement { + + skip, enforce, warn +} diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 48acafe055b..ba7957c8d80 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -50,6 +50,7 @@ import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.spring.config.model.CircuitBreakerProperties; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.spring.config.model.HttpClientProperties; @@ -203,8 +204,8 @@ AuctionRequestFactory auctionRequestFactory( IdGenerator idGenerator, JacksonMapper mapper) { - final List blacklistedApps = splitCommaSeparatedString(blacklistedAppsString); - final List blacklistedAccounts = splitCommaSeparatedString(blacklistedAccountsString); + final List blacklistedApps = splitToList(blacklistedAppsString); + final List blacklistedAccounts = splitToList(blacklistedAccountsString); return new AuctionRequestFactory( maxRequestSize, @@ -296,7 +297,7 @@ VideoStoredRequestProcessor videoStoredRequestProcessor( return VideoStoredRequestProcessor.create( enforceStoredRequest, - splitCommaSeparatedString(blacklistedAccountsString), + splitToList(blacklistedAccountsString), defaultTimeoutMs, adServerCurrency, defaultBidRequestPath, @@ -578,8 +579,12 @@ BidderParamValidator bidderParamValidator(BidderCatalog bidderCatalog, JacksonMa } @Bean - ResponseBidValidator responseValidator() { - return new ResponseBidValidator(); + ResponseBidValidator responseValidator( + @Value("${auction.validations.banner-creative-max-size}") BidValidationEnforcement bannerMaxSizeEnforcement, + @Value("${auction.validations.secure-markup}") BidValidationEnforcement secureMarkupEnforcement, + Metrics metrics) { + + return new ResponseBidValidator(bannerMaxSizeEnforcement, secureMarkupEnforcement, metrics); } @Bean @@ -659,9 +664,11 @@ LoggerControlKnob loggerControlKnob(Vertx vertx) { return new LoggerControlKnob(vertx); } - private static List splitCommaSeparatedString(String listString) { - return Stream.of(listString.split(",")) + private static List splitToList(String listAsString) { + return listAsString != null + ? Stream.of(listAsString.split(",")) .map(String::trim) - .collect(Collectors.toList()); + .collect(Collectors.toList()) + : null; } } diff --git a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java index 86e29e29bc1..f0e6ac31d89 100644 --- a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java +++ b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java @@ -1,29 +1,74 @@ package org.prebid.server.validation; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountBidValidationConfig; +import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.validation.model.ValidationResult; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; import java.util.Currency; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; /** * Validator for response {@link Bid} object. */ public class ResponseBidValidator { - public ValidationResult validate(BidderBid bidderBid) { + private static final String[] INSECURE_MARKUP_MARKERS = {"http:", "http%3A"}; + private static final String[] SECURE_MARKUP_MARKERS = {"https:", "https%3A"}; + + private final BidValidationEnforcement bannerMaxSizeEnforcement; + private final BidValidationEnforcement secureMarkupEnforcement; + private final Metrics metrics; + + public ResponseBidValidator(BidValidationEnforcement bannerMaxSizeEnforcement, + BidValidationEnforcement secureMarkupEnforcement, + Metrics metrics) { + + this.bannerMaxSizeEnforcement = Objects.requireNonNull(bannerMaxSizeEnforcement); + this.secureMarkupEnforcement = Objects.requireNonNull(secureMarkupEnforcement); + this.metrics = Objects.requireNonNull(metrics); + } + + public ValidationResult validate( + BidderBid bidderBid, String bidder, AuctionContext auctionContext, BidderAliases aliases) { + + final List warnings = new ArrayList<>(); + try { - validateFieldsFor(bidderBid.getBid()); + validateCommonFields(bidderBid.getBid()); validateCurrency(bidderBid.getBidCurrency()); + + if (bidderBid.getType() == BidType.banner) { + warnings.addAll(validateBannerFields(bidderBid, bidder, auctionContext, aliases)); + } + + warnings.addAll(validateSecureMarkup(bidderBid, bidder, auctionContext, aliases)); } catch (ValidationException e) { - return ValidationResult.error(e.getMessage()); + return ValidationResult.error(warnings, e.getMessage()); } - return ValidationResult.success(); + return ValidationResult.success(warnings); } - private static void validateFieldsFor(Bid bid) throws ValidationException { + private static void validateCommonFields(Bid bid) throws ValidationException { if (bid == null) { throw new ValidationException("Empty bid object submitted."); } @@ -64,4 +109,122 @@ private static void validateCurrency(String currency) { throw new IllegalArgumentException(String.format("BidResponse currency is not valid: %s", currency), e); } } + + private List validateBannerFields(BidderBid bidderBid, + String bidder, + AuctionContext auctionContext, + BidderAliases aliases) throws ValidationException { + + final Bid bid = bidderBid.getBid(); + final Account account = auctionContext.getAccount(); + + final BidValidationEnforcement bannerMaxSizeEnforcement = effectiveBannerMaxSizeEnforcement(account); + if (bannerMaxSizeEnforcement != BidValidationEnforcement.skip + && bannerSizeIsNotValid(bid, auctionContext.getBidRequest())) { + + return singleWarningOrValidationException( + bannerMaxSizeEnforcement, + metricName -> metrics.updateSizeValidationMetrics( + aliases.resolveBidder(bidder), account.getId(), metricName), + "Bid \"%s\" has 'w' and 'h' that are not valid. Bid dimensions: '%dx%d'", + bid.getId(), bid.getW(), bid.getH()); + } + + return Collections.emptyList(); + } + + private BidValidationEnforcement effectiveBannerMaxSizeEnforcement(Account account) { + final AccountBidValidationConfig validationConfig = account.getBidValidations(); + final BidValidationEnforcement accountBannerMaxSizeEnforcement = + validationConfig != null ? validationConfig.getBannerMaxSizeEnforcement() : null; + + return ObjectUtils.defaultIfNull(accountBannerMaxSizeEnforcement, bannerMaxSizeEnforcement); + } + + private static boolean bannerSizeIsNotValid(Bid bid, BidRequest bidRequest) throws ValidationException { + final Format maxSize = maxSizeForBanner(bid, bidRequest); + final Integer bidW = bid.getW(); + final Integer bidH = bid.getH(); + + return bidW == null || bidW > maxSize.getW() + || bidH == null || bidH > maxSize.getH(); + } + + private static Format maxSizeForBanner(Bid bid, BidRequest bidRequest) throws ValidationException { + int maxW = 0; + int maxH = 0; + for (final Format size : bannerFormats(bid, bidRequest)) { + maxW = Math.max(0, size.getW()); + maxH = Math.max(0, size.getH()); + } + + return Format.builder().w(maxW).h(maxH).build(); + } + + private static List bannerFormats(Bid bid, BidRequest bidRequest) throws ValidationException { + final Imp imp = findCorrespondingImp(bidRequest, bid); + final Banner banner = imp.getBanner(); + + return ListUtils.emptyIfNull(banner != null ? banner.getFormat() : null); + } + + private List validateSecureMarkup(BidderBid bidderBid, + String bidder, + AuctionContext auctionContext, + BidderAliases aliases) throws ValidationException { + + if (secureMarkupEnforcement == BidValidationEnforcement.skip) { + return Collections.emptyList(); + } + + final Bid bid = bidderBid.getBid(); + final Imp imp = findCorrespondingImp(auctionContext.getBidRequest(), bidderBid.getBid()); + + if (isImpSecure(imp) && markupIsNotSecure(bid)) { + return singleWarningOrValidationException( + secureMarkupEnforcement, + metricName -> metrics.updateSecureValidationMetrics( + aliases.resolveBidder(bidder), auctionContext.getAccount().getId(), metricName), + "Bid \"%s\" has insecure creative but should be in secure context", + bid.getId()); + } + + return Collections.emptyList(); + } + + private static Imp findCorrespondingImp(BidRequest bidRequest, Bid bid) throws ValidationException { + return bidRequest.getImp().stream() + .filter(curImp -> Objects.equals(curImp.getId(), bid.getImpid())) + .findFirst() + .orElseThrow(() -> new ValidationException( + "Bid \"%s\" has no corresponding imp in request", bid.getId())); + } + + public static boolean isImpSecure(Imp imp) { + return Objects.equals(imp.getSecure(), 1); + } + + private static boolean markupIsNotSecure(Bid bid) { + final String adm = bid.getAdm(); + + return StringUtils.containsAny(adm, INSECURE_MARKUP_MARKERS) + || !StringUtils.containsAny(adm, SECURE_MARKUP_MARKERS); + } + + private static List singleWarningOrValidationException(BidValidationEnforcement enforcement, + Consumer metricsRecorder, + String message, + Object... args) throws ValidationException { + + switch (enforcement) { + case enforce: + metricsRecorder.accept(MetricName.err); + throw new ValidationException(message, args); + case warn: + metricsRecorder.accept(MetricName.warn); + return Collections.singletonList(String.format(message, args)); + default: + throw new IllegalStateException(String.format("Unexpected enforcement: %s", enforcement)); + } + } } diff --git a/src/main/java/org/prebid/server/validation/model/ValidationResult.java b/src/main/java/org/prebid/server/validation/model/ValidationResult.java index af0f9c13f0d..9b5fc66eee4 100644 --- a/src/main/java/org/prebid/server/validation/model/ValidationResult.java +++ b/src/main/java/org/prebid/server/validation/model/ValidationResult.java @@ -1,26 +1,38 @@ package org.prebid.server.validation.model; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.Collections; import java.util.List; -@AllArgsConstructor @Value public class ValidationResult { + List warnings; + List errors; + public boolean hasWarnings() { + return !warnings.isEmpty(); + } + public boolean hasErrors() { return !errors.isEmpty(); } public static ValidationResult error(String errorMessageFormat, Object... args) { - return new ValidationResult(Collections.singletonList(String.format(errorMessageFormat, args))); + return error(Collections.emptyList(), errorMessageFormat, args); + } + + public static ValidationResult error(List warnings, String errorMessageFormat, Object... args) { + return new ValidationResult(warnings, Collections.singletonList(String.format(errorMessageFormat, args))); } public static ValidationResult success() { - return new ValidationResult(Collections.emptyList()); + return success(Collections.emptyList()); + } + + public static ValidationResult success(List warnings) { + return new ValidationResult(warnings, Collections.emptyList()); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 722a769c10f..5e86babb241 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -87,6 +87,9 @@ auction: cache: expected-request-time-ms: 10 only-winning-bids: false + validations: + banner-creative-max-size: skip + secure-markup: skip video: stored-request-required: false stored-requests-timeout-ms: 90 diff --git a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java index c01a38e70f0..732a13b1b38 100644 --- a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java @@ -1363,7 +1363,8 @@ public void shouldReturnFailedFutureIfRequestValidationFailed() { given(storedRequestProcessor.processStoredRequests(any(), any())) .willReturn(Future.succeededFuture(BidRequest.builder().build())); - given(requestValidator.validate(any())).willReturn(new ValidationResult(asList("error1", "error2"))); + given(requestValidator.validate(any())) + .willReturn(new ValidationResult(emptyList(), asList("error1", "error2"))); // when final Future future = factory.fromRequest(routingContext, 0L); diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 639155fbcde..6fb43852308 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -188,7 +188,7 @@ public void setUp() { given(fpdResolver.resolveSite(any(), any())).willAnswer(invocation -> invocation.getArgument(0)); given(fpdResolver.resolveApp(any(), any())).willAnswer(invocation -> invocation.getArgument(0)); - given(responseBidValidator.validate(any())).willReturn(ValidationResult.success()); + given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); given(usersyncer.getCookieFamilyName()).willReturn("cookieFamily"); given(currencyService.convertCurrency(any(), any(), any(), any(), any())) @@ -942,8 +942,9 @@ public void shouldTolerateNullRequestExtPrebidTargeting() { .allSatisfy(map -> assertThat(map).isNull()); } + @SuppressWarnings("unchecked") @Test - public void shouldTolerateResponseBidValidationErrors() throws JsonProcessingException { + public void shouldTolerateResponseBidValidationErrors() { // given givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( givenBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build())))); @@ -955,20 +956,67 @@ public void shouldTolerateResponseBidValidationErrors() throws JsonProcessingExc .auctiontimestamp(1000L) .build()))); - given(responseBidValidator.validate(any())) - .willReturn(ValidationResult.error("bid validation error")); - - final List bidderErrors = singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(), + given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.error( + singletonList("bid validation warning"), "bid validation error")); - givenBidResponseCreator(singletonMap("bidder1", bidderErrors)); + + givenBidResponseCreator(singletonList(Bid.builder().build())); // when - final BidResponse bidResponse = exchangeService.holdAuction(givenRequestContext(bidRequest)).result(); + exchangeService.holdAuction(givenRequestContext(bidRequest)).result(); // then - final ExtBidResponse ext = mapper.treeToValue(bidResponse.getExt(), ExtBidResponse.class); - assertThat(ext.getErrors()).hasSize(1) - .containsOnly(entry("bidder1", bidderErrors)); + final ArgumentCaptor> bidderResponsesCaptor = ArgumentCaptor.forClass(List.class); + verify(bidResponseCreator).create(bidderResponsesCaptor.capture(), any(), any(), anyBoolean()); + final List bidderResponses = bidderResponsesCaptor.getValue(); + + assertThat(bidderResponses) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .isEmpty(); + assertThat(bidderResponses) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getErrors) + .containsOnly( + BidderError.generic("bid validation warning"), + BidderError.generic("bid validation error")); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldTolerateResponseBidValidationWarnings() { + // given + givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( + givenBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build())))); + + final BidRequest bidRequest = givenBidRequest(singletonList( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .auctiontimestamp(1000L) + .build()))); + + given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success( + singletonList("bid validation warning"))); + + givenBidResponseCreator(singletonList(Bid.builder().build())); + + // when + exchangeService.holdAuction(givenRequestContext(bidRequest)).result(); + + // then + final ArgumentCaptor> bidderResponsesCaptor = ArgumentCaptor.forClass(List.class); + verify(bidResponseCreator).create(bidderResponsesCaptor.capture(), any(), any(), anyBoolean()); + final List bidderResponses = bidderResponsesCaptor.getValue(); + + assertThat(bidderResponses) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .hasSize(1); + assertThat(bidderResponses) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getErrors) + .containsOnly(BidderError.generic("bid validation warning")); } @Test @@ -985,7 +1033,7 @@ public void shouldRejectBidIfCurrencyIsNotValid() throws JsonProcessingException .auctiontimestamp(1000L) .build()))); - given(responseBidValidator.validate(any())) + given(responseBidValidator.validate(any(), any(), any(), any())) .willReturn(ValidationResult.error("BidResponse currency is not valid: USDD")); final List bidderErrors = singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(), diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index 8503dadfb39..a47b0dea119 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -560,6 +560,36 @@ public void updateAdapterRequestErrorMetricShouldIncrementMetrics() { assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.badinput").getCount()).isEqualTo(2); } + @Test + public void updateSizeValidationMetricsShouldIncrementMetrics() { + // given + given(bidderCatalog.isValidName(INVALID_BIDDER)).willReturn(false); + + // when + metrics.updateSizeValidationMetrics(RUBICON, ACCOUNT_ID, MetricName.err); + metrics.updateSizeValidationMetrics(INVALID_BIDDER, ACCOUNT_ID, MetricName.err); + + // then + assertThat(metricRegistry.counter("adapter.rubicon.response.validation.size.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.UNKNOWN.response.validation.size.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.response.validation.size.err").getCount()).isEqualTo(2); + } + + @Test + public void updateSecureValidationMetricsShouldIncrementMetrics() { + // given + given(bidderCatalog.isValidName(INVALID_BIDDER)).willReturn(false); + + // when + metrics.updateSecureValidationMetrics(RUBICON, ACCOUNT_ID, MetricName.err); + metrics.updateSecureValidationMetrics(INVALID_BIDDER, ACCOUNT_ID, MetricName.err); + + // then + assertThat(metricRegistry.counter("adapter.rubicon.response.validation.secure.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.UNKNOWN.response.validation.secure.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.response.validation.secure.err").getCount()).isEqualTo(2); + } + @Test public void updateCookieSyncRequestMetricShouldIncrementMetric() { // when diff --git a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java index 412ae6f402e..aab6540a56f 100644 --- a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java @@ -10,7 +10,9 @@ import org.mockito.junit.MockitoRule; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAnalyticsConfig; +import org.prebid.server.settings.model.AccountBidValidationConfig; import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.settings.model.EnabledForRequestType; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -104,6 +106,9 @@ public void getAccountByIdShouldReturnPresentAccount() { + "defaultIntegration: 'web'," + "analyticsConfig: {" + "auction-events: {amp: 'true'}" + + "}," + + "bidValidations: {" + + "banner-creative-max-size: 'enforce'" + "}" + "}" + "]")); @@ -140,6 +145,7 @@ public void getAccountByIdShouldReturnPresentAccount() { .truncateTargetAttr(20) .defaultIntegration("web") .analyticsConfig(AccountAnalyticsConfig.of(singletonMap("amp", true))) + .bidValidations(AccountBidValidationConfig.of(BidValidationEnforcement.enforce)) .build()); } diff --git a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java index a54f21e9648..253537e6163 100644 --- a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java @@ -24,7 +24,9 @@ import org.prebid.server.metric.Metrics; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAnalyticsConfig; +import org.prebid.server.settings.model.AccountBidValidationConfig; import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.settings.model.EnabledForRequestType; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; @@ -107,7 +109,7 @@ public static void beforeClass() throws SQLException { + "uuid varchar(40) NOT NULL, price_granularity varchar(6), granularityMultiplier numeric(9,3), " + "banner_cache_ttl INT, video_cache_ttl INT, events_enabled BIT, enforce_ccpa BIT, " + "tcf_config varchar(512), analytics_sampling_factor INT, truncate_target_attr INT, " - + "default_integration varchar(64), analytics_config varchar(512));"); + + "default_integration varchar(64), analytics_config varchar(512), bid_validations varchar(512));"); connection.createStatement().execute("CREATE TABLE s2sconfig_config (id SERIAL PRIMARY KEY, uuid varchar(40) " + "NOT NULL, config varchar(512));"); connection.createStatement().execute("CREATE TABLE stored_requests (id SERIAL PRIMARY KEY, " @@ -125,10 +127,11 @@ public static void beforeClass() throws SQLException { + "varchar(40) NOT NULL);"); connection.createStatement().execute("insert into accounts_account " + "(uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, " - + "tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config) " + + "tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, " + + "bid_validations) " + "values ('1001','med', 100, 100, TRUE, TRUE, '{\"enabled\": true, " + "\"integration-enabled\": {\"amp\": true, \"app\": true, \"video\": true, \"web\": true}}', 1, 0, " - + "'web', '{\"auction-events\": {\"amp\": true}}');"); + + "'web', '{\"auction-events\": {\"amp\": true}}', '{\"banner-creative-max-size\": \"enforce\"}');"); connection.createStatement().execute( "insert into s2sconfig_config (uuid, config) values ('adUnitConfigId', 'config');"); connection.createStatement().execute( @@ -195,6 +198,7 @@ public void getAccountByIdShouldReturnAccountWithAllFieldsPopulated(TestContext .truncateTargetAttr(0) .defaultIntegration("web") .analyticsConfig(AccountAnalyticsConfig.of(singletonMap("amp", true))) + .bidValidations(AccountBidValidationConfig.of(BidValidationEnforcement.enforce)) .build()); async.complete(); })); diff --git a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java index 77e2e38aa58..61428c1e16e 100644 --- a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java @@ -1,86 +1,142 @@ package org.prebid.server.validation; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountBidValidationConfig; import org.prebid.server.validation.model.ValidationResult; import java.math.BigDecimal; import java.util.function.Function; +import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.prebid.server.settings.model.BidValidationEnforcement.enforce; +import static org.prebid.server.settings.model.BidValidationEnforcement.skip; +import static org.prebid.server.settings.model.BidValidationEnforcement.warn; -public class ResponseBidValidatorTest { +public class ResponseBidValidatorTest extends VertxTest { + + private static final String BIDDER_NAME = "bidder"; + private static final String ACCOUNT_ID = "account"; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private Metrics metrics; private ResponseBidValidator responseBidValidator; + @Mock + private BidderAliases bidderAliases; + @Before public void setUp() { - responseBidValidator = new ResponseBidValidator(); + responseBidValidator = new ResponseBidValidator(enforce, enforce, metrics); + + given(bidderAliases.resolveBidder(anyString())).willReturn(BIDDER_NAME); } @Test public void validateShouldFailedIfBidderBidCurrencyIsIncorrect() { assertThatIllegalArgumentException().isThrownBy(() -> - responseBidValidator.validate(BidderBid.of( - Bid.builder() - .id("bidId1") - .impid("impId1") - .crid("crid1") - .price(BigDecimal.ONE).build(), - null, - "USDD"))); + responseBidValidator.validate( + BidderBid.of( + Bid.builder() + .id("bidId1") + .impid("impId1") + .crid("crid1") + .price(BigDecimal.ONE) + .build(), + null, + "USDD"), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases)); } @Test - public void validateShouldFailedIfMissingBid() { - final ValidationResult result = responseBidValidator.validate(BidderBid.of(null, null, "USD")); + public void validateShouldFailIfMissingBid() { + // when + final ValidationResult result = responseBidValidator.validate( + BidderBid.of(null, null, "USD"), BIDDER_NAME, givenAuctionContext(), bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Empty bid object submitted."); + // then + assertThat(result.getErrors()).containsOnly("Empty bid object submitted."); } @Test - public void validateShouldFailedIfBidHasNoId() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.id(null))); + public void validateShouldFailIfBidHasNoId() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.id(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Bid missing required field 'id'"); + // then + assertThat(result.getErrors()).containsOnly("Bid missing required field 'id'"); } @Test - public void validateShouldFailedIfBidHasNoImpId() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.impid(null))); + public void validateShouldFailIfBidHasNoImpId() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.impid(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Bid \"bidId1\" missing required field 'impid'"); + // then + assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing required field 'impid'"); } @Test - public void validateShouldFailedIfBidHasNoPrice() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.price(null))); + public void validateShouldFailIfBidHasNoPrice() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.price(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Bid \"bidId1\" does not contain a 'price'"); + // then + assertThat(result.getErrors()).hasSize(1).containsOnly("Bid \"bidId1\" does not contain a 'price'"); } @Test - public void validateShouldFailedIfBidHasNegativePrice() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.price( - BigDecimal.valueOf(-1)))); + public void validateShouldFailIfBidHasNegativePrice() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.price(BigDecimal.valueOf(-1))), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Bid \"bidId1\" `price `has negative value"); + // then + assertThat(result.getErrors()).hasSize(1).containsOnly("Bid \"bidId1\" `price `has negative value"); } @Test public void validateShouldFailedIfNonDealBidHasZeroPrice() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.price( - BigDecimal.valueOf(0)))); + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.price(BigDecimal.valueOf(0))), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); assertThat(result.getErrors()).hasSize(1) .containsOnly("Non deal bid \"bidId1\" has 0 price"); @@ -88,37 +144,346 @@ public void validateShouldFailedIfNonDealBidHasZeroPrice() { @Test public void validateShouldSuccessForDealZeroPriceBid() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.price( - BigDecimal.valueOf(0)).dealid("dealId"))); + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.price(BigDecimal.valueOf(0)).dealid("dealId")), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); assertThat(result.hasErrors()).isFalse(); } @Test - public void validateShouldFailedIfBidHasNoCrid() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.crid(null))); + public void validateShouldFailIfBidHasNoCrid() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.crid(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Bid \"bidId1\" missing creative ID"); + // then + assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing creative ID"); + } + + @Test + public void validateShouldFailIfBannerBidHasNoWidthAndHeight() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.w(null).h(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("Bid \"bidId1\" has 'w' and 'h' that are not valid. Bid dimensions: 'nullxnull'"); + } + + @Test + public void validateShouldFailIfBannerBidWidthIsGreaterThanImposedByImp() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.w(150).h(150)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("Bid \"bidId1\" has 'w' and 'h' that are not valid. Bid dimensions: '150x150'"); + } + + @Test + public void validateShouldFailIfBannerBidHeightIsGreaterThanImposedByImp() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.w(50).h(250)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("Bid \"bidId1\" has 'w' and 'h' that are not valid. Bid dimensions: '50x250'"); + } + + @Test + public void validateShouldReturnSuccessIfNonBannerBidHasAnySize() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(BidType.video, builder -> builder.w(3).h(3)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldReturnSuccessIfBannerBidHasInvalidSizeButAccountDoesNotEnforceValidation() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.w(150).h(150)), + BIDDER_NAME, + givenAuctionContext( + givenAccount(builder -> builder.bidValidations(AccountBidValidationConfig.of(skip)))), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldFailIfBidHasNoCorrespondingImp() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.impid("nonExistentsImpid")), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("Bid \"bidId1\" has no corresponding imp in request"); + } + + @Test + public void validateShouldFailIfBidHasInsecureMarkerInCreativeInSecureContext() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("Bid \"bidId1\" has insecure creative but should be in secure context"); + } + + @Test + public void validateShouldFailIfBidHasInsecureEncodedMarkerInCreativeInSecureContext() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("http%3A//site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("Bid \"bidId1\" has insecure creative but should be in secure context"); + } + + @Test + public void validateShouldFailIfBidHasNoSecureMarkersInCreativeInSecureContext() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("//site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("Bid \"bidId1\" has insecure creative but should be in secure context"); + } + + @Test + public void validateShouldReturnSuccessIfBidHasInsecureCreativeInInsecureContext() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); } @Test public void validateShouldReturnSuccessfulResultForValidBid() { - final ValidationResult result = responseBidValidator.validate(givenBid(identity())); + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(identity()), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + } + @Test + public void validateShouldReturnSuccessIfBannerSizeValidationNotEnabled() { + // given + responseBidValidator = new ResponseBidValidator(skip, enforce, metrics); + + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(identity()), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldReturnSuccessWithWarningIfBannerSizeEnforcementIsWarn() { + // given + responseBidValidator = new ResponseBidValidator(warn, enforce, metrics); + + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.w(null).h(null)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + assertThat(result.getWarnings()) + .containsOnly("Bid \"bidId1\" has 'w' and 'h' that are not valid. Bid dimensions: 'nullxnull'"); + } + + @Test + public void validateShouldReturnSuccessIfSecureMarkupValidationNotEnabled() { + // given + responseBidValidator = new ResponseBidValidator(enforce, skip, metrics); + + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then assertThat(result.hasErrors()).isFalse(); } - private static BidderBid givenBid(Function bidCustomizer, BidType mediaType) { + @Test + public void validateShouldReturnSuccessWithWarningIfSecureMarkupEnforcementIsWarn() { + // given + responseBidValidator = new ResponseBidValidator(enforce, warn, metrics); + + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + assertThat(result.getWarnings()) + .containsOnly("Bid \"bidId1\" has insecure creative but should be in secure context"); + } + + @Test + public void validateShouldIncrementSizeValidationErrMetrics() { + // when + responseBidValidator.validate( + givenBid(builder -> builder.w(150).h(200)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err); + } + + @Test + public void validateShouldIncrementSizeValidationWarnMetrics() { + // given + responseBidValidator = new ResponseBidValidator(warn, warn, metrics); + + // when + responseBidValidator.validate( + givenBid(builder -> builder.w(150).h(200)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn); + } + + @Test + public void validateShouldIncrementSecureValidationErrMetrics() { + // when + responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err); + } + + @Test + public void validateShouldIncrementSecureValidationWarnMetrics() { + // given + responseBidValidator = new ResponseBidValidator(warn, warn, metrics); + + // when + responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn); + } + + private static BidderBid givenBid(Function bidCustomizer) { + return givenBid(BidType.banner, bidCustomizer); + } + + private static BidderBid givenBid(BidType type, Function bidCustomizer) { final Bid.BidBuilder bidBuilder = Bid.builder() .id("bidId1") .impid("impId1") .crid("crid1") + .w(1) + .h(1) + .adm("https://site.com/creative.jpg") .price(BigDecimal.ONE); - return BidderBid.of(bidCustomizer.apply(bidBuilder).build(), mediaType, "USD"); + + return BidderBid.of(bidCustomizer.apply(bidBuilder).build(), type, "USD"); } - private static BidderBid givenBid(Function bidCustomizer) { - return givenBid(bidCustomizer, null); + private static AuctionContext givenAuctionContext(BidRequest bidRequest, Account account) { + return AuctionContext.builder() + .account(account) + .bidRequest(bidRequest) + .build(); + } + + private static AuctionContext givenAuctionContext(BidRequest bidRequest) { + return givenAuctionContext(bidRequest, givenAccount()); + } + + private static AuctionContext givenAuctionContext(Account account) { + return givenAuctionContext(givenBidRequest(identity()), account); + } + + private static AuctionContext givenAuctionContext() { + return givenAuctionContext(givenBidRequest(identity()), givenAccount()); + } + + private static BidRequest givenBidRequest(Function impCustomizer) { + final Imp.ImpBuilder impBuilder = Imp.builder() + .id("impId1") + .banner(Banner.builder() + .format(singletonList(Format.builder().w(100).h(200).build())) + .build()); + + return BidRequest.builder() + .imp(singletonList(impCustomizer.apply(impBuilder).build())) + .build(); + } + + private static Account givenAccount() { + return givenAccount(identity()); + } + + private static Account givenAccount(Function accountCustomizer) { + return accountCustomizer.apply(Account.builder().id(ACCOUNT_ID)).build(); } } From 3ce7df94f7b6f42a4d4eaa9fb52d10b54f048b93 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Fri, 18 Dec 2020 10:57:46 +0200 Subject: [PATCH 069/129] Make SQL query for retrieving account configurable (#811) --- docs/application-settings.md | 115 ++++++++++++------ docs/config-app.md | 1 + .../settings/JdbcApplicationSettings.java | 47 ++++--- .../spring/config/SettingsConfiguration.java | 10 +- .../settings/JdbcApplicationSettingsTest.java | 77 +++++++++--- 5 files changed, 171 insertions(+), 79 deletions(-) diff --git a/docs/application-settings.md b/docs/application-settings.md index f2f33b1b499..56afe37c730 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -49,14 +49,14 @@ In file based approach all configuration stores in .yaml files, path to which ar ### Configuration in application.yaml -``` +```yaml settings: filesystem: settings-filename: ``` ### File format -``` +```yaml accounts: - id: 14062 bannerCacheTtl: 100 @@ -152,53 +152,46 @@ accounts: purpose-one-treatment-interpretation: ignore ``` - ## Database application setting -In database approach account properties are stored in database table with name accounts_account. +In database approach account properties are stored in database table. + +SQL query for retrieving account is configurable and can be specified in [application configuration](config-app.md). +Requirements for the SQL query stated below. ### Configuration in application.yaml -``` + +```yaml settings: database: - type: pool-size: 20 - type: mysql + type: host: port: + account-query: ``` -### Table description +### SQL query for account requirements -Query to create accounts_account table: +The SQL query for account must: +* return following columns, with specified type, in that order: + * account ID, string + * price granularity, string + * banner cache TTL, integer + * video cache TTL, integer + * events enabled flag, boolean + * enforce CCPA flag, boolean + * TCF configuration, JSON string, see below + * analytics sampling factor, integer + * maximum targeting attribute size, integer + * default integration value, string + * analytics configuration, JSON string, see below +* specify a special single `%ACCOUNT_ID%` placeholder in the `WHERE` clause that will be replaced with account ID in +runtime -``` -'CREATE TABLE `accounts_account` ( -`id` int(10) unsigned NOT NULL AUTO_INCREMENT, -`uuid` varchar(40) NOT NULL, -`price_granularity` enum('low','med','high','auto','dense','unknown') NOT NULL DEFAULT 'unknown', -`granularityMultiplier` decimal(9,3) DEFAULT NULL, -`banner_cache_ttl` int(11) DEFAULT NULL, -`video_cache_ttl` int(11) DEFAULT NULL, -`events_enabled` bit(1) DEFAULT NULL, -`enforce_ccpa` bit(1) DEFAULT NULL, -`enforce_gdpr` bit(1) DEFAULT NULL, -`tcf_config` json DEFAULT NULL, -`analytics_sampling_factor` tinyint(4) DEFAULT NULL, -`truncate_target_attr` tinyint(3) unsigned DEFAULT NULL, -`default_integration` varchar(64) DEFAULT NULL, -`analytics_config` varchar(512) DEFAULT NULL, -`bid_validations` json DEFAULT NULL, -`status` enum('active','inactive') DEFAULT 'active', -`updated_by` int(11) DEFAULT NULL, -`updated_by_user` varchar(64) DEFAULT NULL, -`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -PRIMARY KEY (`id`), -UNIQUE KEY `uuid` (`uuid`)) -ENGINE=InnoDB DEFAULT CHARSET=utf8' -``` +It is recommended to include `LIMIT 1` clause in the query because only the very first result returned will be taken. -where tcf_config column is json with next format +TCF configuration column format: ```json { @@ -319,10 +312,54 @@ and bid_validations column is json with next format } ``` -Query used to get an account: + +Analytics configuration column format: + +```json +{ + "auction-events": { + "web": true, + "amp": true, + "app": false + } +} ``` -SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, bid_validations -FROM accounts_account where uuid = ? -LIMIT 1 +#### Example + +Query to create accounts_account table: + +```sql +'CREATE TABLE `accounts_account` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(40) NOT NULL, + `price_granularity` enum('low','med','high','auto','dense','unknown') NOT NULL DEFAULT 'unknown', + `granularityMultiplier` decimal(9,3) DEFAULT NULL, + `banner_cache_ttl` int(11) DEFAULT NULL, + `video_cache_ttl` int(11) DEFAULT NULL, + `events_enabled` bit(1) DEFAULT NULL, + `enforce_ccpa` bit(1) DEFAULT NULL, + `enforce_gdpr` bit(1) DEFAULT NULL, + `tcf_config` json DEFAULT NULL, + `analytics_sampling_factor` tinyint(4) DEFAULT NULL, + `truncate_target_attr` tinyint(3) unsigned DEFAULT NULL, + `default_integration` varchar(64) DEFAULT NULL, + `analytics_config` varchar(512) DEFAULT NULL, + `bid_validations` json DEFAULT NULL, + `status` enum('active','inactive') DEFAULT 'active', + `updated_by` int(11) DEFAULT NULL, + `updated_by_user` varchar(64) DEFAULT NULL, + `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +PRIMARY KEY (`id`), +UNIQUE KEY `uuid` (`uuid`)) +ENGINE=InnoDB DEFAULT CHARSET=utf8' +``` + +Query used to get an account: + +```sql +SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, + analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, bid_validations +FROM accounts_account where uuid = %ACCOUNT_ID% +LIMIT 1 ``` diff --git a/docs/config-app.md b/docs/config-app.md index 69d0b760303..988297c2684 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -244,6 +244,7 @@ For database data source available next options: - `settings.database.user` - database user. - `settings.database.password` - database password. - `settings.database.pool-size` - set the initial/min/max pool size of database connections. +- `settings.database.account-query` - the SQL query to fetch account. - `settings.database.stored-requests-query` - the SQL query to fetch stored requests. - `settings.database.amp-stored-requests-query` - the SQL query to fetch AMP stored requests. - `settings.database.stored-responses-query` - the SQL query to fetch stored responses. diff --git a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java index 7ace747d3ca..3190652d84b 100644 --- a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java @@ -38,13 +38,20 @@ */ public class JdbcApplicationSettings implements ApplicationSettings { + private static final String ACCOUNT_ID_PLACEHOLDER = "%ACCOUNT_ID%"; private static final String REQUEST_ID_PLACEHOLDER = "%REQUEST_ID_LIST%"; private static final String IMP_ID_PLACEHOLDER = "%IMP_ID_LIST%"; private static final String RESPONSE_ID_PLACEHOLDER = "%RESPONSE_ID_LIST%"; + private static final String QUERY_PARAM_PLACEHOLDER = "?"; private final JdbcClient jdbcClient; private final JacksonMapper mapper; + /** + * Query to select account by ids. + */ + private final String selectAccountQuery; + /** * Query to select stored requests and imps by ids, for example: *
@@ -57,7 +64,7 @@ public class JdbcApplicationSettings implements ApplicationSettings {
      *   WHERE impid in (%IMP_ID_LIST%)
      * 
*/ - private final String selectQuery; + private final String selectStoredRequestsQuery; /** * Query to select amp stored requests by ids, for example: @@ -67,7 +74,7 @@ public class JdbcApplicationSettings implements ApplicationSettings { * WHERE reqid in (%REQUEST_ID_LIST%) *
*/ - private final String selectAmpQuery; + private final String selectAmpStoredRequestsQuery; /** * Query to select stored responses by ids, for example: @@ -77,19 +84,22 @@ public class JdbcApplicationSettings implements ApplicationSettings { * WHERE respid in (%RESPONSE_ID_LIST%) *
*/ - private final String selectResponseQuery; + private final String selectStoredResponsesQuery; public JdbcApplicationSettings(JdbcClient jdbcClient, JacksonMapper mapper, - String selectQuery, - String selectAmpQuery, - String selectResponseQuery) { + String selectAccountQuery, + String selectStoredRequestsQuery, + String selectAmpStoredRequestsQuery, + String selectStoredResponsesQuery) { this.jdbcClient = Objects.requireNonNull(jdbcClient); this.mapper = Objects.requireNonNull(mapper); - this.selectQuery = Objects.requireNonNull(selectQuery); - this.selectAmpQuery = Objects.requireNonNull(selectAmpQuery); - this.selectResponseQuery = Objects.requireNonNull(selectResponseQuery); + this.selectAccountQuery = Objects.requireNonNull(selectAccountQuery) + .replace(ACCOUNT_ID_PLACEHOLDER, QUERY_PARAM_PLACEHOLDER); + this.selectStoredRequestsQuery = Objects.requireNonNull(selectStoredRequestsQuery); + this.selectAmpStoredRequestsQuery = Objects.requireNonNull(selectAmpStoredRequestsQuery); + this.selectStoredResponsesQuery = Objects.requireNonNull(selectStoredResponsesQuery); } /** @@ -98,10 +108,7 @@ public JdbcApplicationSettings(JdbcClient jdbcClient, */ @Override public Future getAccountById(String accountId, Timeout timeout) { - return jdbcClient.executeQuery("SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, " - + "events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, " - + "default_integration, analytics_config, bid_validations " - + "FROM accounts_account where uuid = ? LIMIT 1", + return jdbcClient.executeQuery(selectAccountQuery, Collections.singletonList(accountId), result -> mapToModelOrError(result, row -> Account.builder() .id(row.getString(0)) @@ -171,7 +178,7 @@ private T toModel(String source, Class targetClass) { @Override public Future getStoredData(String accountId, Set requestIds, Set impIds, Timeout timeout) { - return fetchStoredData(selectQuery, accountId, requestIds, impIds, timeout); + return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout); } /** @@ -181,7 +188,7 @@ public Future getStoredData(String accountId, Set requ @Override public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, Timeout timeout) { - return fetchStoredData(selectAmpQuery, accountId, requestIds, Collections.emptySet(), timeout); + return fetchStoredData(selectAmpStoredRequestsQuery, accountId, requestIds, Collections.emptySet(), timeout); } /** @@ -191,7 +198,7 @@ public Future getAmpStoredData(String accountId, Set r @Override public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, Timeout timeout) { - return fetchStoredData(selectQuery, accountId, requestIds, impIds, timeout); + return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout); } /** @@ -200,11 +207,11 @@ public Future getVideoStoredData(String accountId, Set */ @Override public Future getStoredResponses(Set responseIds, Timeout timeout) { - final String queryResolvedWithParameters = selectResponseQuery.replaceAll(RESPONSE_ID_PLACEHOLDER, + final String queryResolvedWithParameters = selectStoredResponsesQuery.replaceAll(RESPONSE_ID_PLACEHOLDER, parameterHolders(responseIds.size())); final List idsQueryParameters = new ArrayList<>(); - IntStream.rangeClosed(1, StringUtils.countMatches(selectResponseQuery, RESPONSE_ID_PLACEHOLDER)) + IntStream.rangeClosed(1, StringUtils.countMatches(selectStoredResponsesQuery, RESPONSE_ID_PLACEHOLDER)) .forEach(i -> idsQueryParameters.addAll(responseIds)); return jdbcClient.executeQuery(queryResolvedWithParameters, idsQueryParameters, @@ -253,6 +260,8 @@ private static String createParametrizedQuery(String query, int requestIdsSize, private static String parameterHolders(int paramsSize) { return paramsSize == 0 ? "NULL" - : IntStream.range(0, paramsSize).mapToObj(i -> "?").collect(Collectors.joining(",")); + : IntStream.range(0, paramsSize) + .mapToObj(i -> QUERY_PARAM_PLACEHOLDER) + .collect(Collectors.joining(",")); } } 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 7fa5b0fc547..fc21729a7b6 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -73,14 +73,20 @@ static class DatabaseSettingsConfiguration { @Bean JdbcApplicationSettings jdbcApplicationSettings( + @Value("${settings.database.account-query}") String accountQuery, @Value("${settings.database.stored-requests-query}") String storedRequestsQuery, @Value("${settings.database.amp-stored-requests-query}") String ampStoredRequestsQuery, - @Value("${settings.database.stored-responses-query}") String storedResponseQuery, + @Value("${settings.database.stored-responses-query}") String storedResponsesQuery, JdbcClient jdbcClient, JacksonMapper jacksonMapper) { return new JdbcApplicationSettings( - jdbcClient, jacksonMapper, storedRequestsQuery, ampStoredRequestsQuery, storedResponseQuery); + jdbcClient, + jacksonMapper, + accountQuery, + storedRequestsQuery, + ampStoredRequestsQuery, + storedResponsesQuery); } @Bean diff --git a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java index 253537e6163..3e4f43ee60a 100644 --- a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java @@ -60,6 +60,12 @@ public class JdbcApplicationSettingsTest extends VertxTest { private static final String JDBC_URL = "jdbc:h2:mem:test"; + private static final String SELECT_ACCOUNT_QUERY = + "SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, " + + "events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, " + + "default_integration, analytics_config, bid_validations " + + "FROM accounts_account where uuid = %ACCOUNT_ID% LIMIT 1"; + private static final String SELECT_QUERY = "SELECT accountId, reqid, requestData, 'request' as dataType FROM stored_requests " + "WHERE reqid IN (%REQUEST_ID_LIST%) " @@ -80,15 +86,14 @@ public class JdbcApplicationSettingsTest extends VertxTest { + "SELECT accountId, impid, impData, 'imp' as dataType FROM stored_imps2 " + "WHERE impid IN (%IMP_ID_LIST%)"; - private static final String SELECT_FROM_ONE_COLUMN_TABLE_QUERY = - "SELECT reqid FROM one_column_table WHERE reqid IN " - + "(%REQUEST_ID_LIST%)"; + private static final String SELECT_FROM_ONE_COLUMN_TABLE_QUERY = "SELECT reqid FROM one_column_table " + + "WHERE reqid IN (%REQUEST_ID_LIST%)"; - private static final String SELECT_RESPONSE_QUERY = "SELECT responseId, responseData FROM stored_responses" - + " WHERE responseId IN (%RESPONSE_ID_LIST%)"; + private static final String SELECT_RESPONSE_QUERY = "SELECT responseId, responseData FROM stored_responses " + + "WHERE responseId IN (%RESPONSE_ID_LIST%)"; - private static final String SELECT_ONE_COLUMN_RESPONSE_QUERY = "SELECT responseId FROM stored_responses" - + " WHERE responseId IN (%RESPONSE_ID_LIST%)"; + private static final String SELECT_ONE_COLUMN_RESPONSE_QUERY = "SELECT responseId FROM stored_responses " + + "WHERE responseId IN (%RESPONSE_ID_LIST%)"; private static Connection connection; @@ -166,7 +171,12 @@ public void setUp() { vertx = Vertx.vertx(); clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); timeout = new TimeoutFactory(clock).create(5000L); - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, SELECT_QUERY, SELECT_QUERY, + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_QUERY, + SELECT_QUERY, SELECT_RESPONSE_QUERY); } @@ -308,8 +318,13 @@ public void getVideoStoredDataShouldReturnExpectedResult(TestContext context) { @Test public void getVideoStoredDataShouldReturnStoredRequests(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, SELECT_UNION_QUERY, - SELECT_UNION_QUERY, SELECT_RESPONSE_QUERY); + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_UNION_QUERY, + SELECT_UNION_QUERY, + SELECT_RESPONSE_QUERY); // when final Future storedRequestResultFuture = @@ -336,8 +351,13 @@ public void getVideoStoredDataShouldReturnStoredRequests(TestContext context) { @Test public void getStoredDataUnionSelectByIdShouldReturnStoredRequests(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, SELECT_UNION_QUERY, - SELECT_UNION_QUERY, SELECT_RESPONSE_QUERY); + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_UNION_QUERY, + SELECT_UNION_QUERY, + SELECT_RESPONSE_QUERY); // when final Future storedRequestResultFuture = @@ -364,8 +384,13 @@ public void getStoredDataUnionSelectByIdShouldReturnStoredRequests(TestContext c @Test public void getAmpStoredDataUnionSelectByIdShouldReturnStoredRequests(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, SELECT_UNION_QUERY, - SELECT_UNION_QUERY, SELECT_RESPONSE_QUERY); + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_UNION_QUERY, + SELECT_UNION_QUERY, + SELECT_RESPONSE_QUERY); // when final Future storedRequestResultFuture = @@ -434,8 +459,13 @@ public void getAmpStoredDataShouldReturnResultWithErrorIfNoStoredRequestFound(Te @Test public void getStoredDataShouldReturnErrorIfResultContainsLessColumnsThanExpected(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, - SELECT_FROM_ONE_COLUMN_TABLE_QUERY, SELECT_FROM_ONE_COLUMN_TABLE_QUERY, SELECT_RESPONSE_QUERY); + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_FROM_ONE_COLUMN_TABLE_QUERY, + SELECT_FROM_ONE_COLUMN_TABLE_QUERY, + SELECT_RESPONSE_QUERY); // when final Future storedRequestResultFuture = @@ -454,9 +484,13 @@ public void getStoredDataShouldReturnErrorIfResultContainsLessColumnsThanExpecte @Test public void getAmpStoredDataShouldReturnErrorIfResultContainsLessColumnsThanExpected(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, SELECT_FROM_ONE_COLUMN_TABLE_QUERY, - SELECT_FROM_ONE_COLUMN_TABLE_QUERY, SELECT_RESPONSE_QUERY); + SELECT_FROM_ONE_COLUMN_TABLE_QUERY, + SELECT_RESPONSE_QUERY); // when final Future storedRequestResultFuture = @@ -556,7 +590,12 @@ public void getStoredResponseShouldReturnResultWithErrorIfNotAllStoredResponsesW @Test public void getStoredResponseShouldReturnErrorIfResultContainsLessColumnsThanExpected(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, SELECT_QUERY, SELECT_QUERY, + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_QUERY, + SELECT_QUERY, SELECT_ONE_COLUMN_RESPONSE_QUERY); // when From 04616e810fb601b572e0cefe4ecf1d2db7d3a173 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Wed, 23 Dec 2020 14:47:49 +0200 Subject: [PATCH 070/129] Update deepintent usersync config (#1083) --- .../openrtb/ext/request/deepintent/ExtImpDeepintent.java | 2 +- src/main/resources/bidder-config/deepintent.yaml | 2 +- src/main/resources/static/bidder-params/deepintent.json | 4 ++-- .../openrtb2/deepintent/test-auction-deepintent-request.json | 2 +- .../it/openrtb2/deepintent/test-deepintent-bid-request.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/deepintent/ExtImpDeepintent.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/deepintent/ExtImpDeepintent.java index c313b9dd2e1..235c5e6c7f5 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/deepintent/ExtImpDeepintent.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/deepintent/ExtImpDeepintent.java @@ -8,6 +8,6 @@ @Value public class ExtImpDeepintent { - @JsonProperty("TagID") + @JsonProperty("tagId") String tagId; } diff --git a/src/main/resources/bidder-config/deepintent.yaml b/src/main/resources/bidder-config/deepintent.yaml index a833dff3bba..b140708beb3 100644 --- a/src/main/resources/bidder-config/deepintent.yaml +++ b/src/main/resources/bidder-config/deepintent.yaml @@ -16,7 +16,7 @@ adapters: supported-vendors: vendor-id: 541 usersync: - url: https://cdn.deepintent.com/syncpixel.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir= + url: https://match.deepintent.com/usersync/136?id=unk&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir= redirect-url: /setuid?bidder=deepintent&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=[UID] cookie-family-name: deepintent type: iframe diff --git a/src/main/resources/static/bidder-params/deepintent.json b/src/main/resources/static/bidder-params/deepintent.json index fbb607a346b..772938805f1 100644 --- a/src/main/resources/static/bidder-params/deepintent.json +++ b/src/main/resources/static/bidder-params/deepintent.json @@ -5,10 +5,10 @@ "type": "object", "properties": { - "TagID": { + "tagId": { "type": "string", "description": "An ID which identifies the deepintent ad tag" } }, - "required" : [ "TagID" ] + "required" : [ "tagId" ] } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-request.json b/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-request.json index dfc241aab42..e7507d0e050 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-request.json @@ -12,7 +12,7 @@ "tagid" : "possibleTagId", "ext": { "deepintent": { - "TagID": "possibleTagId" + "tagId": "possibleTagId" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-deepintent-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-deepintent-bid-request.json index f8460a68d84..74055235383 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-deepintent-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-deepintent-bid-request.json @@ -12,7 +12,7 @@ "tagid" : "possibleTagId", "ext": { "bidder": { - "TagID": "possibleTagId" + "tagId": "possibleTagId" } } From 55a4d9c9393cb9a81d16d647d51dc1710ebe8a50 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Wed, 23 Dec 2020 14:50:06 +0200 Subject: [PATCH 071/129] Support excluded bidders from config in userId/deviceId masking (#1082) --- .../purpose/PurposeStrategy.java | 21 ++--- .../purpose/PurposeEightStrategyTest.java | 74 ++++++++++-------- .../purpose/PurposeFiveStrategyTest.java | 74 ++++++++++-------- .../purpose/PurposeFourStrategyTest.java | 74 ++++++++++-------- .../purpose/PurposeNineStrategyTest.java | 73 ++++++++++-------- .../purpose/PurposeOneStrategyTest.java | 70 ++++++++++------- .../purpose/PurposeSevenStrategyTest.java | 76 +++++++++++-------- .../purpose/PurposeSixStrategyTest.java | 74 ++++++++++-------- .../purpose/PurposeTenStrategyTest.java | 73 ++++++++++-------- .../purpose/PurposeThreeStrategyTest.java | 74 ++++++++++-------- .../purpose/PurposeTwoStrategyTest.java | 75 ++++++++++-------- 11 files changed, 430 insertions(+), 328 deletions(-) diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java index 71ae7eeb7bf..17d0c890b9f 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java @@ -52,13 +52,18 @@ public Collection processTypePurposeStrategy( Collection vendorPermissions, boolean wasDowngraded) { - allowedByTypeStrategy(vendorConsent, purpose, vendorPermissions).stream() + final Collection excludedVendors = excludedVendors(vendorPermissions, purpose); + final Collection vendorForPurpose = vendorPermissions.stream() + .filter(vendorPermission -> !excludedVendors.contains(vendorPermission)) + .collect(Collectors.toList()); + + allowedByTypeStrategy(vendorConsent, purpose, vendorForPurpose, excludedVendors).stream() .map(VendorPermission::getPrivacyEnforcementAction) .forEach(this::allow); final Collection naturalVendorPermission = wasDowngraded - ? allowedByBasicTypeStrategy(vendorConsent, true, vendorPermissions, Collections.emptyList()) - : allowedByFullTypeStrategy(vendorConsent, true, vendorPermissions, Collections.emptyList()); + ? allowedByBasicTypeStrategy(vendorConsent, true, vendorForPurpose, excludedVendors) + : allowedByFullTypeStrategy(vendorConsent, true, vendorForPurpose, excludedVendors); naturalVendorPermission.stream() .map(VendorPermission::getPrivacyEnforcementAction) @@ -71,12 +76,8 @@ public Collection processTypePurposeStrategy( private Collection allowedByTypeStrategy(TCString vendorConsent, Purpose purpose, - Collection vendorPermissions) { - - final Collection excludedVendors = excludedVendors(vendorPermissions, purpose); - final Collection vendorForPurpose = vendorPermissions.stream() - .filter(vendorPermission -> !excludedVendors.contains(vendorPermission)) - .collect(Collectors.toList()); + Collection vendorForPurpose, + Collection excludedVendors) { final boolean isEnforceVendors = BooleanUtils.isNotFalse(purpose.getEnforceVendors()); final EnforcePurpose purposeType = purpose.getEnforcePurpose(); @@ -105,7 +106,7 @@ protected Collection excludedVendors(Collection - bidderNameExceptions.contains(vendorPermission.getVendorPermission().getBidderName())); + bidderNameExceptions.contains(vendorPermission.getVendorPermission().getBidderName())); } protected Collection allowedByBasicTypeStrategy( diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategyTest.java index f59fccb1b6c..13c50e18de0 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategyTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeEightStrategyTest { @@ -222,20 +223,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +248,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +290,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +330,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategyTest.java index ed9efb63fdc..0465bfce725 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategyTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeFiveStrategyTest { @@ -222,20 +223,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +248,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +290,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +330,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategyTest.java index 5e63f2f8a7a..d0f1795c292 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategyTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeFourStrategyTest { @@ -224,20 +225,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -249,34 +250,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -288,36 +292,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setMaskDeviceInfo(false); - privacyEnforcementAction.setRemoveUserIds(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -325,7 +332,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategyTest.java index 8bbeefe1aa8..0856f237e12 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategyTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeNineStrategyTest { @@ -222,20 +223,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +248,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +290,38 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +329,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategyTest.java index e503d5521ee..d508b952c9b 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategyTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeOneStrategyTest { @@ -224,20 +225,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -249,34 +250,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); - assertThat(result).usingFieldByFieldElementComparator().isEqualTo( - Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); + assertThat(result).usingFieldByFieldElementComparator() + .isEqualTo(Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -288,29 +292,35 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } private static PrivacyEnforcementAction allowPurpose() { @@ -318,4 +328,8 @@ private static PrivacyEnforcementAction allowPurpose() { privacyEnforcementAction.setBlockPixelSync(false); return privacyEnforcementAction; } + + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowPurpose(); + } } diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategyTest.java index 99a2002c45a..8d7b0806ce0 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategyTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeSevenStrategyTest { @@ -224,20 +225,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -249,34 +250,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -288,37 +292,40 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setBlockAnalyticsReport(false); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -328,7 +335,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategyTest.java index f0a210d0770..c9dc22660ad 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategyTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeSixStrategyTest { @@ -222,20 +223,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +248,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +290,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +330,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategyTest.java index 064e4930702..d24a1b04544 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategyTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeTenStrategyTest { @@ -222,20 +223,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +248,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +290,38 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +329,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategyTest.java index 3301fc0744a..a233c3a78ed 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategyTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeThreeStrategyTest { @@ -222,20 +223,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +248,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +290,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +330,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategyTest.java index 695bc2db299..03f322df0b2 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategyTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeTwoStrategyTest { @@ -222,20 +223,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +248,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,37 +290,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setBlockBidderRequest(false); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -326,7 +332,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; From c44b25cad09d095d04b6dff165cc4963cfb35d9a Mon Sep 17 00:00:00 2001 From: bretg Date: Wed, 13 Jan 2021 05:01:57 -0500 Subject: [PATCH 072/129] accounts config doc updates (#1096) Adding detail and transitions around the accounts config --- docs/application-settings.md | 71 +++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/docs/application-settings.md b/docs/application-settings.md index 56afe37c730..3390a055579 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -25,6 +25,7 @@ There are two ways to configure application settings: database and file. This do - `analytics-config.auction-events.` - defines which channels are supported by analytics for this account - `bid-validations.banner-creative-max-size` - Overrides creative max size validation for banners. +Here are the definitions of the "purposes" that can be defined in the GDPR setting configurations: ``` Purpose | Purpose goal | Purpose meaning for PBS (n\a - not affected) ----------|---------------------------------|--------------------------------------------- @@ -43,12 +44,14 @@ sf1 | Precise geo | Verifies user opt-in. If the user sf2 | Fingerprinting | n\a ``` -## File application setting +## Setting Account Configuration in Files In file based approach all configuration stores in .yaml files, path to which are defined in application properties. ### Configuration in application.yaml +The general idea is that you'll place all the account-specific settings in a separate YAML file and point to that file. + ```yaml settings: filesystem: @@ -56,9 +59,11 @@ settings: ``` ### File format +Here's an example YAML file containing account-specific settings: + ```yaml accounts: - - id: 14062 + - id: 1111 bannerCacheTtl: 100 videoCacheTtl: 100 eventsEnabled: true @@ -152,7 +157,7 @@ accounts: purpose-one-treatment-interpretation: ignore ``` -## Database application setting +## Setting Account Configuration in the Database In database approach account properties are stored in database table. @@ -171,10 +176,20 @@ settings: account-query: ``` -### SQL query for account requirements +### Configurable SQL query for account requirements + +The general approach is that each host company can set up their database however they wish, so long as the configurable query run by +Prebid Server returns expected data in the expected order. Here's an example configuration: + +```yaml +settings: + database: + type: mysql + account-query: SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, bid_validations FROM accounts_account where uuid = ? LIMIT 1 +``` The SQL query for account must: -* return following columns, with specified type, in that order: +* return following columns, with specified type, in this order: * account ID, string * price granularity, string * banner cache TTL, integer @@ -191,7 +206,19 @@ runtime It is recommended to include `LIMIT 1` clause in the query because only the very first result returned will be taken. -TCF configuration column format: +If a host company doesn't support a given field, or they have a different table name, they can just update the query with whatever values are needed. e.g. + +```yaml +settings: + database: + type: mysql + account-query: SELECT uuid, 'med', banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, 0, null, default_integration, '{}', '{}' FROM myaccountstable where uuid = ? LIMIT 1 +``` +### Configuration Details + +#### TCF configuration JSON + +Here's an example of the value that the `tcf_config` column can take: ```json { @@ -304,7 +331,9 @@ TCF configuration column format: } ``` -and bid_validations column is json with next format +#### Bid Validations configuration JSON + +The `bid_validations` column is json with this format: ```json { @@ -312,22 +341,29 @@ and bid_validations column is json with next format } ``` +Valid values are: +- "skip": don't do anything about creative max size for this publisher +- "warn": if a bidder returns a creative that's larger in height or width than any of the allowed sizes, log an operational warning. +- "enforce": if a bidder returns a creative that's larger in height or width than any of the allowed sizes, reject the bid and log an operational warning. -Analytics configuration column format: +#### Analytics Validations configuration JSON + +The `analytics_config` configuration column format: ```json { "auction-events": { - "web": true, - "amp": true, - "app": false + "web": true, // the analytics adapter should log auction events when the channel is web + "amp": true, // the analytics adapter should log auction events when the channel is AMP + "app": false // the analytics adapter should not log auction events when the channel is app } } ``` -#### Example +#### Creating the accounts table -Query to create accounts_account table: +Traditionally the table name used by Prebid Server is `accounts_account`. No one remembers why. But here's SQL +you could use to create your table: ```sql 'CREATE TABLE `accounts_account` ( @@ -354,12 +390,3 @@ PRIMARY KEY (`id`), UNIQUE KEY `uuid` (`uuid`)) ENGINE=InnoDB DEFAULT CHARSET=utf8' ``` - -Query used to get an account: - -```sql -SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, - analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, bid_validations -FROM accounts_account where uuid = %ACCOUNT_ID% -LIMIT 1 -``` From 4183e2da1c0e71ae78aa4931593431d02a718496 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Wed, 13 Jan 2021 12:14:29 +0200 Subject: [PATCH 073/129] Prebid Server prepare release 1.52.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c05133abc29..71d7e1eebae 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.52.0-SNAPSHOT + 1.52.0 prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 1.52.0 From 993ecea941d5a20ef56e6150da2043298a783716 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Wed, 13 Jan 2021 12:14:42 +0200 Subject: [PATCH 074/129] Prebid Server prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 71d7e1eebae..deb13eaf7a4 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.52.0 + 1.53.0-SNAPSHOT prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - 1.52.0 + HEAD From b508b4b445e9f5f8890848840f6e5428b900bdf5 Mon Sep 17 00:00:00 2001 From: Braslavskyi Andrii Date: Thu, 14 Jan 2021 14:32:00 +0200 Subject: [PATCH 075/129] Log missing video size id for RubiconBidder (#877) * Log missing video size id for RubiconBidder * Update sampling logging way * Use conditional logger for conditional logging --- .../server/bidder/rubicon/RubiconBidder.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index 1a1ff24424b..ca14e9f750b 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -65,6 +65,7 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.ConditionalLogger; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtApp; import org.prebid.server.proto.openrtb.ext.request.ExtDevice; @@ -111,6 +112,8 @@ public class RubiconBidder implements Bidder { private static final Logger logger = LoggerFactory.getLogger(RubiconBidder.class); + private static final ConditionalLogger MISSING_VIDEO_SIZE_LOGGER = + new ConditionalLogger("missing_video_size", logger); private static final String TK_XINT_QUERY_PARAMETER = "tk_xint"; private static final String PREBID_SERVER_USER_AGENT = "prebid-server/1.0"; @@ -383,7 +386,7 @@ private Imp makeImp(Imp imp, ExtImpPrebid extPrebid, ExtImpRubicon extRubicon, S if (isVideo(imp)) { builder .banner(null) - .video(makeVideo(imp.getVideo(), extRubicon.getVideo(), extPrebid)); + .video(makeVideo(imp, site, extRubicon.getVideo(), extPrebid)); } else { builder .banner(makeBanner(imp, overriddenSizes(extRubicon))) @@ -653,7 +656,11 @@ private static boolean isFullyPopulatedVideo(Video video) { && video.getLinearity() != null && video.getApi() != null; } - private Video makeVideo(Video video, RubiconVideoParams rubiconVideoParams, ExtImpPrebid prebidImpExt) { + private Video makeVideo(Imp imp, Site site, RubiconVideoParams rubiconVideoParams, ExtImpPrebid prebidImpExt) { + final Video video = imp.getVideo(); + final String impId = imp.getId(); + final String referer = site != null ? site.getPage() : null; + validateVideoSizeId(rubiconVideoParams, referer, impId); final String videoType = prebidImpExt != null && prebidImpExt.getIsRewardedInventory() != null && prebidImpExt.getIsRewardedInventory() == 1 ? "rewarded" : null; @@ -670,6 +677,15 @@ private Video makeVideo(Video video, RubiconVideoParams rubiconVideoParams, ExtI .build(); } + private void validateVideoSizeId(RubiconVideoParams rubiconVideoParams, String referer, String impId) { + final Integer videoSizeId = rubiconVideoParams != null ? rubiconVideoParams.getSizeId() : null; + // log only 1% of cases to monitor how often video impressions does not have size id + if (videoSizeId == null || videoSizeId == 0) { + MISSING_VIDEO_SIZE_LOGGER.warn(String.format("RP adapter: video request with no size_id. Referrer URL = %s," + + " impId = %s", referer, impId), 0.01d); + } + } + private static List overriddenSizes(ExtImpRubicon rubiconImpExt) { final List overriddenSizes; From c60c5394e7dd104f847849f93c4d8c6dd4cd49d5 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Thu, 14 Jan 2021 14:40:17 +0200 Subject: [PATCH 076/129] Add log with key to not depend on message (#1089) --- .../org/prebid/server/log/ConditionalLogger.java | 4 ++++ .../prebid/server/log/ConditionalLoggerTest.java | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/org/prebid/server/log/ConditionalLogger.java b/src/main/java/org/prebid/server/log/ConditionalLogger.java index cf375849d08..0a2fdd59d8d 100644 --- a/src/main/java/org/prebid/server/log/ConditionalLogger.java +++ b/src/main/java/org/prebid/server/log/ConditionalLogger.java @@ -44,6 +44,10 @@ public ConditionalLogger(Logger logger) { this(null, logger); } + public void infoWithKey(String key, String message, int limit) { + log(key, limit, logger -> logger.info(message)); + } + public void info(String message, int limit) { log(message, limit, logger -> logger.info(message)); } diff --git a/src/test/java/org/prebid/server/log/ConditionalLoggerTest.java b/src/test/java/org/prebid/server/log/ConditionalLoggerTest.java index 943cf54f87f..7623c7e273c 100644 --- a/src/test/java/org/prebid/server/log/ConditionalLoggerTest.java +++ b/src/test/java/org/prebid/server/log/ConditionalLoggerTest.java @@ -19,6 +19,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; @RunWith(VertxUnitRunner.class) public class ConditionalLoggerTest { @@ -44,6 +45,19 @@ public void tearDown(TestContext context) { vertx.close(context.asyncAssertSuccess()); } + @Test + public void infoWithKeyShouldCallLoggerWithExpectedCount() { + // when + for (int i = 0; i < 10; i++) { + conditionalLogger.infoWithKey("key", "Log Message1", 2); + conditionalLogger.infoWithKey("key", "Log Message2", 2); + } + + // then + verify(logger, times(10)).info("Log Message2"); + verifyNoMoreInteractions(logger); + } + @Test public void infoShouldCallLoggerWithExpectedCount() { // when From 0817d1e02b2fa3331a8daf3f98020c0b1403ac29 Mon Sep 17 00:00:00 2001 From: Braslavskyi Andrii Date: Thu, 14 Jan 2021 15:03:04 +0200 Subject: [PATCH 077/129] Update appnexus ext request when include targeting is not null (#913) --- .../org/prebid/server/bidder/appnexus/AppnexusBidder.java | 2 +- .../prebid/server/bidder/appnexus/AppnexusBidderTest.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/appnexus/AppnexusBidder.java b/src/main/java/org/prebid/server/bidder/appnexus/AppnexusBidder.java index 085b5e40312..52568155086 100644 --- a/src/main/java/org/prebid/server/bidder/appnexus/AppnexusBidder.java +++ b/src/main/java/org/prebid/server/bidder/appnexus/AppnexusBidder.java @@ -256,7 +256,7 @@ private static boolean isIncludeBrandCategory(ExtRequest extRequest) { final ExtIncludeBrandCategory includebrandcategory = targeting != null ? targeting.getIncludebrandcategory() : null; - return includebrandcategory != null && includebrandcategory.getPrimaryAdserver() != 0; + return includebrandcategory != null; } private List> splitHttpRequests(BidRequest outgoingRequest, List processedImps, diff --git a/src/test/java/org/prebid/server/bidder/appnexus/AppnexusBidderTest.java b/src/test/java/org/prebid/server/bidder/appnexus/AppnexusBidderTest.java index cca21834b53..e3a6e5f8745 100644 --- a/src/test/java/org/prebid/server/bidder/appnexus/AppnexusBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/appnexus/AppnexusBidderTest.java @@ -299,11 +299,11 @@ public void makeHttpRequestsShouldSetRequestExtAppnexusTrueWhenPrimaryAdserverIs } @Test - public void makeHttpRequestsShouldUpdateRequestExtAppnexusTrueWhenPrimaryAdserverIsNotZero() { + public void makeHttpRequestsShouldUpdateRequestExtAppnexusTrueWhenPrimaryAdserverIsNotNull() { // given final ExtRequestPrebid requestPrebid = ExtRequestPrebid.builder() .targeting(ExtRequestTargeting.builder() - .includebrandcategory(ExtIncludeBrandCategory.of(-120, null, null)) + .includebrandcategory(ExtIncludeBrandCategory.of(null, null, null)) .build()) .build(); @@ -327,11 +327,11 @@ public void makeHttpRequestsShouldUpdateRequestExtAppnexusTrueWhenPrimaryAdserve } @Test - public void makeHttpRequestsShouldNotUpdateRequestExtAppnexusWhenPrimaryAdserverIsZero() { + public void makeHttpRequestsShouldNotUpdateRequestExtAppnexusWhenIncludeBrandCategoryIsNull() { // given final ExtRequestPrebid requestPrebid = ExtRequestPrebid.builder() .targeting(ExtRequestTargeting.builder() - .includebrandcategory(ExtIncludeBrandCategory.of(0, null, null)) + .includebrandcategory(null) .build()) .build(); From 0880bb3c1c8df4f60a09f993dc53c151a0239aa2 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Thu, 14 Jan 2021 16:06:29 +0200 Subject: [PATCH 078/129] Change GVL URL (#1086) --- .../java/org/prebid/server/proto/response/BidderInfo.java | 4 ++-- src/main/resources/application.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/prebid/server/proto/response/BidderInfo.java b/src/main/java/org/prebid/server/proto/response/BidderInfo.java index 7e347265668..c1dcbc7ba12 100644 --- a/src/main/java/org/prebid/server/proto/response/BidderInfo.java +++ b/src/main/java/org/prebid/server/proto/response/BidderInfo.java @@ -65,8 +65,8 @@ public static class GdprInfo { /** * GDPR Vendor ID in the IAB Global Vendor List which refers to this Bidder. *

- * The Global Vendor list can be found here: https://vendorlist.consensu.org/vendorlist.json - * Bidders can register for the list here: https://register.consensu.org/ + * The Global Vendor list can be found at https://iabeurope.eu/ + * Bidders can be registered to the list at https://register.consensu.org/ *

* If you're not on the list, this should return 0. If cookie sync requests have GDPR consent info, * or the Prebid Server host company configures its deploy to be "cautious" when no GDPR info exists diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 5e86babb241..bd7c71ccbd9 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -147,11 +147,11 @@ gdpr: vendorlist: default-timeout-ms: 2000 v1: - http-endpoint-template: https://vendorlist.consensu.org/v-{VERSION}/vendorlist.json + http-endpoint-template: https://vendor-list.consensu.org/v-{VERSION}/vendorlist.json refresh-missing-list-period-ms: 3600000 deprecated: false v2: - http-endpoint-template: https://vendorlist.consensu.org/v2/archives/vendor-list-v{VERSION}.json + http-endpoint-template: https://vendor-list.consensu.org/v2/archives/vendor-list-v{VERSION}.json refresh-missing-list-period-ms: 3600000 deprecated: false purposes: From fa3ce16205e02145b89f6a895bdb3e7937edaf66 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Thu, 14 Jan 2021 17:34:08 +0200 Subject: [PATCH 079/129] Implement new logic for video sizeId in Rubicon bidder (#931) * Implement new logic for video sizeId. * Fixes after review * Additional test * Adding test changes --- .../server/bidder/rubicon/RubiconBidder.java | 29 +++++++-- .../bidder/rubicon/RubiconBidderTest.java | 63 +++++++++++++++++++ 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index ca14e9f750b..6d13717c48e 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -660,7 +660,6 @@ private Video makeVideo(Imp imp, Site site, RubiconVideoParams rubiconVideoParam final Video video = imp.getVideo(); final String impId = imp.getId(); final String referer = site != null ? site.getPage() : null; - validateVideoSizeId(rubiconVideoParams, referer, impId); final String videoType = prebidImpExt != null && prebidImpExt.getIsRewardedInventory() != null && prebidImpExt.getIsRewardedInventory() == 1 ? "rewarded" : null; @@ -671,21 +670,41 @@ private Video makeVideo(Imp imp, Site site, RubiconVideoParams rubiconVideoParam final Integer skip = rubiconVideoParams != null ? rubiconVideoParams.getSkip() : null; final Integer skipDelay = rubiconVideoParams != null ? rubiconVideoParams.getSkipdelay() : null; final Integer sizeId = rubiconVideoParams != null ? rubiconVideoParams.getSizeId() : null; + final Integer resolvedSizeId = sizeId == null || sizeId == 0 + ? resolveVideoSizeId(video.getPlacement(), imp.getInstl()) : sizeId; + validateVideoSizeId(resolvedSizeId, referer, impId); + return video.toBuilder() .ext(mapper.mapper().valueToTree( - RubiconVideoExt.of(skip, skipDelay, RubiconVideoExtRp.of(sizeId), videoType))) + RubiconVideoExt.of(skip, skipDelay, RubiconVideoExtRp.of(resolvedSizeId), videoType))) .build(); } - private void validateVideoSizeId(RubiconVideoParams rubiconVideoParams, String referer, String impId) { - final Integer videoSizeId = rubiconVideoParams != null ? rubiconVideoParams.getSizeId() : null; + private void validateVideoSizeId(Integer resolvedSizeId, String referer, String impId) { // log only 1% of cases to monitor how often video impressions does not have size id - if (videoSizeId == null || videoSizeId == 0) { + if (resolvedSizeId == null) { MISSING_VIDEO_SIZE_LOGGER.warn(String.format("RP adapter: video request with no size_id. Referrer URL = %s," + " impId = %s", referer, impId), 0.01d); } } + private Integer resolveVideoSizeId(Integer placement, Integer instl) { + if (placement != null) { + if (placement == 1) { + return 201; + } + if (placement == 3) { + return 203; + } + } + + if (instl != null && instl == 1) { + return 202; + } + + return null; + } + private static List overriddenSizes(ExtImpRubicon rubiconImpExt) { final List overriddenSizes; 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 01e2a6306f6..2a5b3b7dc05 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -420,6 +420,69 @@ public void makeHttpRequestsShouldCreateVideoRequestIfImpHasBannerAndVideoButAll .maxduration(60).linearity(2).api(singletonList(3)).build())); } + @Test + public void shouldSetSizeIdTo201IfplacementIs1IfSizeIdIsNotPresent() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder.instl(1).video(Video.builder().placement(1).build()), + builder -> builder.video(RubiconVideoParams.builder().skip(5).skipdelay(10).sizeId(null).build())); + + // when + final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).doesNotContainNull() + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getVideo).doesNotContainNull() + .extracting(Video::getExt).doesNotContainNull() + .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) + .containsOnly(RubiconVideoExt.of(5, 10, RubiconVideoExtRp.of(201), null)); + } + + @Test + public void shouldSetSizeIdTo203IfplacementIs3IfSizeIdIsNotPresent() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder.instl(1).video(Video.builder().placement(3).build()), + builder -> builder.video(RubiconVideoParams.builder().skip(5).skipdelay(10).sizeId(null).build())); + + // when + final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).doesNotContainNull() + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getVideo).doesNotContainNull() + .extracting(Video::getExt).doesNotContainNull() + .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) + .containsOnly(RubiconVideoExt.of(5, 10, RubiconVideoExtRp.of(203), null)); + } + + @Test + public void shouldCalculateSizeIdUsingInstlIfPlacementAndSizeIdIsNotPresent() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder.instl(1).video(Video.builder().placement(null).build()), + builder -> builder.video(RubiconVideoParams.builder().skip(5).skipdelay(10).sizeId(null).build())); + + // when + final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).doesNotContainNull() + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getVideo).doesNotContainNull() + .extracting(Video::getExt).doesNotContainNull() + .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) + .containsOnly(RubiconVideoExt.of(5, 10, RubiconVideoExtRp.of(202), null)); + } + @Test public void makeHttpRequestsShouldFillVideoExt() { // given From 06cd27ef759dd98fb02b8103ab777549dce945a2 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Fri, 15 Jan 2021 10:22:58 +0200 Subject: [PATCH 080/129] Add adapter prefix to bidder specific metrics (#973) --- docs/metrics.md | 6 +- .../prebid/server/metric/AccountMetrics.java | 10 +-- .../prebid/server/metric/AdapterMetrics.java | 63 +++----------- .../server/metric/AdapterTypeMetrics.java | 82 +++++++++++++++++++ .../org/prebid/server/metric/Metrics.java | 36 ++++---- .../org/prebid/server/metric/MetricsTest.java | 41 ++++++---- 6 files changed, 142 insertions(+), 96 deletions(-) create mode 100644 src/main/java/org/prebid/server/metric/AdapterTypeMetrics.java diff --git a/docs/metrics.md b/docs/metrics.md index 7360cdb0d04..189a5b242c8 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -89,10 +89,10 @@ Following metrics are collected and submitted if account is configured with `bas Following metrics are collected and submitted if account is configured with `detailed` verbosity: - `account..requests.type.(openrtb2-web,openrtb-app,amp,legacy)` - number of requests received from account with `` broken down by type of incoming request -- `account...request_time` - timer tracking how long did it take to make a request to `` when incoming request was from `` -- `account...bids_received` - number of bids received from `` when incoming request was from `` -- `account...requests.(gotbids|nobid)` - number of requests made to `` broken down by result status when incoming request was from `` - `account..requests.rejected` - number of rejected requests caused by incorrect `accountId` +- `account..adapter..request_time` - timer tracking how long did it take to make a request to `` when incoming request was from `` +- `account..adapter..bids_received` - number of bids received from `` when incoming request was from `` +- `account..adapter..requests.(gotbids|nobid)` - number of requests made to `` broken down by result status when incoming request was from `` ## General Prebid Cache metrics - `prebid_cache.requests.ok` - timer tracking how long did successful cache requests take diff --git a/src/main/java/org/prebid/server/metric/AccountMetrics.java b/src/main/java/org/prebid/server/metric/AccountMetrics.java index 80ae7853dd6..622e08f96a9 100644 --- a/src/main/java/org/prebid/server/metric/AccountMetrics.java +++ b/src/main/java/org/prebid/server/metric/AccountMetrics.java @@ -12,13 +12,12 @@ */ class AccountMetrics extends UpdatableMetrics { - private final Function adapterMetricsCreator; // not thread-safe maps are intentionally used here because it's harmless in this particular case - eventually // this all boils down to metrics lookup by underlying metric registry and that operation is guaranteed to be // thread-safe - private final Map adapterMetrics; private final Function requestTypeMetricsCreator; private final Map requestTypeMetrics; + private final AdapterMetrics adapterMetrics; private final RequestMetrics requestsMetrics; private final CacheMetrics cacheMetrics; private final ResponseMetrics responseMetrics; @@ -26,10 +25,9 @@ class AccountMetrics extends UpdatableMetrics { AccountMetrics(MetricRegistry metricRegistry, CounterType counterType, String account) { super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), nameCreator(createPrefix(Objects.requireNonNull(account)))); - adapterMetricsCreator = adapterType -> new AdapterMetrics(metricRegistry, counterType, account, adapterType); - adapterMetrics = new HashMap<>(); requestTypeMetricsCreator = requestType -> new RequestTypeMetrics(metricRegistry, counterType, createPrefix(account), requestType); + adapterMetrics = new AdapterMetrics(metricRegistry, counterType, createPrefix(account)); requestTypeMetrics = new HashMap<>(); requestsMetrics = new RequestMetrics(metricRegistry, counterType, createPrefix(account)); cacheMetrics = new CacheMetrics(metricRegistry, counterType, createPrefix(account)); @@ -44,8 +42,8 @@ private static Function nameCreator(String prefix) { return metricName -> String.format("%s.%s", prefix, metricName.toString()); } - AdapterMetrics forAdapter(String adapterType) { - return adapterMetrics.computeIfAbsent(adapterType, adapterMetricsCreator); + AdapterMetrics adapter() { + return adapterMetrics; } RequestTypeMetrics requestType(MetricName requestType) { diff --git a/src/main/java/org/prebid/server/metric/AdapterMetrics.java b/src/main/java/org/prebid/server/metric/AdapterMetrics.java index 2f98a32cdb1..96c3621e431 100644 --- a/src/main/java/org/prebid/server/metric/AdapterMetrics.java +++ b/src/main/java/org/prebid/server/metric/AdapterMetrics.java @@ -12,68 +12,27 @@ */ class AdapterMetrics extends UpdatableMetrics { - private final Function requestTypeMetricsCreator; - private final Map requestTypeMetrics; - private final RequestMetrics requestMetrics; - private final Function bidTypeMetricsCreator; - private final Map bidTypeMetrics; - private final ResponseMetrics responseMetrics; + private final Function adapterMetricsCreator; + private final Map adapterMetrics; - AdapterMetrics(MetricRegistry metricRegistry, CounterType counterType, String adapterType) { + AdapterMetrics(MetricRegistry metricRegistry, CounterType counterType, String accountPrefix) { super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), - nameCreator(createAdapterPrefix(Objects.requireNonNull(adapterType)))); + nameCreator(createAdapterSuffix(Objects.requireNonNull(accountPrefix)))); - bidTypeMetricsCreator = bidType -> - new BidTypeMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType), bidType); - requestTypeMetricsCreator = requestType -> - new RequestTypeMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType), requestType); - requestTypeMetrics = new HashMap<>(); - requestMetrics = new RequestMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType)); - bidTypeMetrics = new HashMap<>(); - responseMetrics = new ResponseMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType)); + adapterMetrics = new HashMap<>(); + adapterMetricsCreator = adapterType -> new AdapterTypeMetrics(metricRegistry, counterType, + createAdapterSuffix(Objects.requireNonNull(accountPrefix)), adapterType); } - AdapterMetrics(MetricRegistry metricRegistry, CounterType counterType, String account, String adapterType) { - super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), - nameCreator(createAccountAdapterPrefix(Objects.requireNonNull(account), - Objects.requireNonNull(adapterType)))); - - requestMetrics = new RequestMetrics(metricRegistry, counterType, - createAccountAdapterPrefix(account, adapterType)); - - // not used for account.adapter metrics - requestTypeMetricsCreator = null; - requestTypeMetrics = null; - bidTypeMetricsCreator = null; - bidTypeMetrics = null; - responseMetrics = null; - } - - private static String createAdapterPrefix(String adapterType) { - return String.format("adapter.%s", adapterType); - } - - private static String createAccountAdapterPrefix(String account, String adapterType) { - return String.format("account.%s.%s", account, adapterType); + private static String createAdapterSuffix(String prefix) { + return String.format("%s.adapter", prefix); } private static Function nameCreator(String prefix) { return metricName -> String.format("%s.%s", prefix, metricName.toString()); } - RequestTypeMetrics requestType(MetricName requestType) { - return requestTypeMetrics.computeIfAbsent(requestType, requestTypeMetricsCreator); - } - - RequestMetrics request() { - return requestMetrics; - } - - BidTypeMetrics forBidType(String bidType) { - return bidTypeMetrics.computeIfAbsent(bidType, bidTypeMetricsCreator); - } - - ResponseMetrics response() { - return responseMetrics; + AdapterTypeMetrics forAdapter(String adapterType) { + return adapterMetrics.computeIfAbsent(adapterType, adapterMetricsCreator); } } diff --git a/src/main/java/org/prebid/server/metric/AdapterTypeMetrics.java b/src/main/java/org/prebid/server/metric/AdapterTypeMetrics.java new file mode 100644 index 00000000000..5c4bf1ae1d2 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/AdapterTypeMetrics.java @@ -0,0 +1,82 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * AdapterType metrics support. + */ +class AdapterTypeMetrics extends UpdatableMetrics { + + private final Function requestTypeMetricsCreator; + private final Map requestTypeMetrics; + private final RequestMetrics requestMetrics; + private final Function bidTypeMetricsCreator; + private final Map bidTypeMetrics; + private final ResponseMetrics responseMetrics; + + AdapterTypeMetrics(MetricRegistry metricRegistry, CounterType counterType, String adapterType) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createAdapterPrefix(Objects.requireNonNull(adapterType)))); + + bidTypeMetricsCreator = bidType -> + new BidTypeMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType), bidType); + requestTypeMetricsCreator = requestType -> + new RequestTypeMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType), requestType); + requestTypeMetrics = new HashMap<>(); + requestMetrics = new RequestMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType)); + bidTypeMetrics = new HashMap<>(); + responseMetrics = new ResponseMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType)); + } + + AdapterTypeMetrics(MetricRegistry metricRegistry, + CounterType counterType, + String accountAdapterPrefix, + String adapterType) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createAdapterPrefix(Objects.requireNonNull(accountAdapterPrefix), + Objects.requireNonNull(adapterType)))); + + requestMetrics = new RequestMetrics(metricRegistry, counterType, + createAdapterPrefix(accountAdapterPrefix, adapterType)); + + // not used for account.adapter.adapters metrics + requestTypeMetricsCreator = null; + requestTypeMetrics = null; + bidTypeMetricsCreator = null; + bidTypeMetrics = null; + responseMetrics = null; + } + + private static String createAdapterPrefix(String adapterType) { + return String.format("adapter.%s", adapterType); + } + + private static String createAdapterPrefix(String adapterPrefix, String adapterType) { + return String.format("%s.%s", adapterPrefix, adapterType); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + RequestTypeMetrics requestType(MetricName requestType) { + return requestTypeMetrics.computeIfAbsent(requestType, requestTypeMetricsCreator); + } + + RequestMetrics request() { + return requestMetrics; + } + + BidTypeMetrics forBidType(String bidType) { + return bidTypeMetrics.computeIfAbsent(bidType, bidTypeMetricsCreator); + } + + ResponseMetrics response() { + return responseMetrics; + } +} diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 5980206f0a4..4eefaea1c80 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -29,7 +29,7 @@ public class Metrics extends UpdatableMetrics { private final Function requestMetricsCreator; private final Function accountMetricsCreator; - private final Function adapterMetricsCreator; + private final Function adapterMetricsCreator; private final Function bidderCardinalityMetricsCreator; private final Function circuitBreakerMetricsCreator; private final Function settingsCacheMetricsCreator; @@ -38,7 +38,7 @@ public class Metrics extends UpdatableMetrics { // thread-safe private final Map requestMetrics; private final Map accountMetrics; - private final Map adapterMetrics; + private final Map adapterMetrics; private final Map bidderCardinailtyMetrics; private final UserSyncMetrics userSyncMetrics; private final CookieSyncMetrics cookieSyncMetrics; @@ -58,7 +58,7 @@ public Metrics(MetricRegistry metricRegistry, CounterType counterType, AccountMe requestMetricsCreator = requestType -> new RequestStatusMetrics(metricRegistry, counterType, requestType); accountMetricsCreator = account -> new AccountMetrics(metricRegistry, counterType, account); - adapterMetricsCreator = adapterType -> new AdapterMetrics(metricRegistry, counterType, adapterType); + adapterMetricsCreator = adapterType -> new AdapterTypeMetrics(metricRegistry, counterType, adapterType); bidderCardinalityMetricsCreator = cardinality -> new BidderCardinalityMetrics( metricRegistry, counterType, cardinality); circuitBreakerMetricsCreator = type -> new CircuitBreakerMetrics(metricRegistry, counterType, type); @@ -89,7 +89,7 @@ AccountMetrics forAccount(String account) { return accountMetrics.computeIfAbsent(account, accountMetricsCreator); } - AdapterMetrics forAdapter(String adapterType) { + AdapterTypeMetrics forAdapter(String adapterType) { return adapterMetrics.computeIfAbsent(adapterType, adapterMetricsCreator); } @@ -211,22 +211,23 @@ public void updateAccountRequestRejectedMetrics(String accountId) { } public void updateAdapterRequestTypeAndNoCookieMetrics(String bidder, MetricName requestType, boolean noCookie) { - final AdapterMetrics adapterMetrics = forAdapter(resolveMetricsBidderName(bidder)); + final AdapterTypeMetrics adapterTypeMetrics = forAdapter(resolveMetricsBidderName(bidder)); - adapterMetrics.requestType(requestType).incCounter(MetricName.requests); + adapterTypeMetrics.requestType(requestType).incCounter(MetricName.requests); if (noCookie) { - adapterMetrics.incCounter(MetricName.no_cookie_requests); + adapterTypeMetrics.incCounter(MetricName.no_cookie_requests); } } public void updateAdapterResponseTime(String bidder, String accountId, int responseTime) { final String metricsBidderName = resolveMetricsBidderName(bidder); - final AdapterMetrics adapterMetrics = forAdapter(metricsBidderName); - adapterMetrics.updateTimer(MetricName.request_time, responseTime); + final AdapterTypeMetrics adapterTypeMetrics = forAdapter(metricsBidderName); + adapterTypeMetrics.updateTimer(MetricName.request_time, responseTime); if (accountMetricsVerbosity.forAccount(accountId).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { - final AdapterMetrics accountAdapterMetrics = forAccount(accountId).forAdapter(metricsBidderName); + final AdapterTypeMetrics accountAdapterMetrics = + forAccount(accountId).adapter().forAdapter(metricsBidderName); accountAdapterMetrics.updateTimer(MetricName.request_time, responseTime); } } @@ -235,7 +236,7 @@ public void updateAdapterRequestNobidMetrics(String bidder, String accountId) { final String metricsBidderName = resolveMetricsBidderName(bidder); forAdapter(metricsBidderName).request().incCounter(MetricName.nobid); if (accountMetricsVerbosity.forAccount(accountId).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { - forAccount(accountId).forAdapter(metricsBidderName).request().incCounter(MetricName.nobid); + forAccount(accountId).adapter().forAdapter(metricsBidderName).request().incCounter(MetricName.nobid); } } @@ -243,20 +244,21 @@ public void updateAdapterRequestGotbidsMetrics(String bidder, String accountId) final String metricsBidderName = resolveMetricsBidderName(bidder); forAdapter(metricsBidderName).request().incCounter(MetricName.gotbids); if (accountMetricsVerbosity.forAccount(accountId).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { - forAccount(accountId).forAdapter(metricsBidderName).request().incCounter(MetricName.gotbids); + forAccount(accountId).adapter().forAdapter(metricsBidderName).request().incCounter(MetricName.gotbids); } } public void updateAdapterBidMetrics(String bidder, String accountId, long cpm, boolean isAdm, String bidType) { final String metricsBidderName = resolveMetricsBidderName(bidder); - final AdapterMetrics adapterMetrics = forAdapter(metricsBidderName); - adapterMetrics.updateHistogram(MetricName.prices, cpm); - adapterMetrics.incCounter(MetricName.bids_received); - adapterMetrics.forBidType(bidType) + final AdapterTypeMetrics adapterTypeMetrics = forAdapter(metricsBidderName); + adapterTypeMetrics.updateHistogram(MetricName.prices, cpm); + adapterTypeMetrics.incCounter(MetricName.bids_received); + adapterTypeMetrics.forBidType(bidType) .incCounter(isAdm ? MetricName.adm_bids_received : MetricName.nurl_bids_received); if (accountMetricsVerbosity.forAccount(accountId).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { - final AdapterMetrics accountAdapterMetrics = forAccount(accountId).forAdapter(metricsBidderName); + final AdapterTypeMetrics accountAdapterMetrics = + forAccount(accountId).adapter().forAdapter(metricsBidderName); accountAdapterMetrics.updateHistogram(MetricName.prices, cpm); accountAdapterMetrics.incCounter(MetricName.bids_received); } diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index a47b0dea119..630ac19da8f 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -153,14 +153,15 @@ public void shouldReturnAdapterRequestMetricsConfiguredWithAdapterType() { @Test public void shouldReturnSameAccountAdapterMetricsOnSuccessiveCalls() { - assertThat(metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON)) - .isSameAs(metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON)); + assertThat(metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON)) + .isSameAs(metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON)); } @Test public void shouldReturnAccountAdapterMetricsConfiguredWithCounterType() { verifyCreatesConfiguredCounterType(metrics -> metrics .forAccount(ACCOUNT_ID) + .adapter() .forAdapter(RUBICON) .incCounter(MetricName.bids_received)); } @@ -168,22 +169,23 @@ public void shouldReturnAccountAdapterMetricsConfiguredWithCounterType() { @Test public void shouldReturnAccountAdapterMetricsConfiguredWithAccountAndAdapterType() { // when - metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON).incCounter(MetricName.bids_received); + metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON).incCounter(MetricName.bids_received); // then - assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.adapter.rubicon.bids_received").getCount()).isOne(); } @Test public void shouldReturnSameAccountAdapterRequestMetricsOnSuccessiveCalls() { - assertThat(metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON).request()) - .isSameAs(metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON).request()); + assertThat(metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON).request()) + .isSameAs(metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON).request()); } @Test public void shouldReturnAccountAdapterRequestMetricsConfiguredWithCounterType() { verifyCreatesConfiguredCounterType(metrics -> metrics .forAccount(ACCOUNT_ID) + .adapter() .forAdapter(RUBICON) .request() .incCounter(MetricName.gotbids)); @@ -192,10 +194,11 @@ public void shouldReturnAccountAdapterRequestMetricsConfiguredWithCounterType() @Test public void shouldReturnAccountAdapterRequestMetricsConfiguredWithAccountAndAdapterType() { // when - metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON).request().incCounter(MetricName.gotbids); + metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON).request().incCounter(MetricName.gotbids); // then - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.adapter.rubicon.requests.gotbids").getCount()) + .isOne(); } @Test @@ -481,9 +484,9 @@ public void updateAdapterResponseTimeShouldUpdateMetrics() { // then assertThat(metricRegistry.timer("adapter.rubicon.request_time").getCount()).isOne(); - assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isOne(); + assertThat(metricRegistry.timer("account.accountId.adapter.rubicon.request_time").getCount()).isOne(); assertThat(metricRegistry.timer("adapter.UNKNOWN.request_time").getCount()).isEqualTo(2); - assertThat(metricRegistry.timer("account.accountId.UNKNOWN.request_time").getCount()).isEqualTo(2); + assertThat(metricRegistry.timer("account.accountId.adapter.UNKNOWN.request_time").getCount()).isEqualTo(2); } @Test @@ -498,9 +501,9 @@ public void updateAdapterRequestNobidMetricsShouldIncrementMetrics() { // then assertThat(metricRegistry.counter("adapter.rubicon.requests.nobid").getCount()).isOne(); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.adapter.rubicon.requests.nobid").getCount()).isOne(); assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.nobid").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("account.accountId.UNKNOWN.requests.nobid").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("account.accountId.adapter.UNKNOWN.requests.nobid").getCount()).isEqualTo(2); } @Test @@ -515,9 +518,11 @@ public void updateAdapterRequestGotbidsMetricsShouldIncrementMetrics() { // then assertThat(metricRegistry.counter("adapter.rubicon.requests.gotbids").getCount()).isOne(); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.adapter.rubicon.requests.gotbids").getCount()) + .isOne(); assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.gotbids").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("account.accountId.UNKNOWN.requests.gotbids").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("account.accountId.adapter.UNKNOWN.requests.gotbids").getCount()) + .isEqualTo(2); } @Test @@ -533,15 +538,15 @@ public void updateAdapterBidMetricsShouldUpdateMetrics() { // then assertThat(metricRegistry.histogram("adapter.rubicon.prices").getCount()).isEqualTo(2); - assertThat(metricRegistry.histogram("account.accountId.rubicon.prices").getCount()).isEqualTo(2); + assertThat(metricRegistry.histogram("account.accountId.adapter.rubicon.prices").getCount()).isEqualTo(2); assertThat(metricRegistry.counter("adapter.rubicon.bids_received").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("account.accountId.adapter.rubicon.bids_received").getCount()).isEqualTo(2); assertThat(metricRegistry.counter("adapter.rubicon.banner.adm_bids_received").getCount()).isOne(); assertThat(metricRegistry.counter("adapter.rubicon.video.nurl_bids_received").getCount()).isOne(); assertThat(metricRegistry.histogram("adapter.UNKNOWN.prices").getCount()).isEqualTo(2); - assertThat(metricRegistry.histogram("account.accountId.UNKNOWN.prices").getCount()).isEqualTo(2); + assertThat(metricRegistry.histogram("account.accountId.adapter.UNKNOWN.prices").getCount()).isEqualTo(2); assertThat(metricRegistry.counter("adapter.UNKNOWN.bids_received").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("account.accountId.UNKNOWN.bids_received").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("account.accountId.adapter.UNKNOWN.bids_received").getCount()).isEqualTo(2); assertThat(metricRegistry.counter("adapter.UNKNOWN.banner.nurl_bids_received").getCount()).isEqualTo(2); } From 220481c3a8e2616340fc06964d6b3c5cc6de4604 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Fri, 15 Jan 2021 15:43:36 +0200 Subject: [PATCH 081/129] Default account configuration and account status support (#959) * Add support for default account configuration values in application config * Take into consideration account status * Update documentation * Address review comments --- docs/application-settings.md | 2 + docs/config-app.md | 18 +++ .../server/auction/AuctionRequestFactory.java | 12 +- .../EnrichingApplicationSettings.java | 71 +++++++++ .../settings/JdbcApplicationSettings.java | 13 +- .../prebid/server/settings/model/Account.java | 22 +++ .../settings/model/AccountGdprConfig.java | 8 +- .../server/settings/model/AccountStatus.java | 6 + .../spring/config/SettingsConfiguration.java | 30 +++- .../model/AccountConfigurationProperties.java | 56 +++++++ .../auction/AuctionRequestFactoryTest.java | 35 ++++- .../EnrichingApplicationSettingsTest.java | 144 ++++++++++++++++++ .../settings/FileApplicationSettingsTest.java | 5 +- .../settings/JdbcApplicationSettingsTest.java | 12 +- 14 files changed, 413 insertions(+), 21 deletions(-) create mode 100644 src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java create mode 100644 src/main/java/org/prebid/server/settings/model/AccountStatus.java create mode 100644 src/main/java/org/prebid/server/spring/config/model/AccountConfigurationProperties.java create mode 100644 src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java diff --git a/docs/application-settings.md b/docs/application-settings.md index 3390a055579..a6132a997f4 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -24,6 +24,7 @@ There are two ways to configure application settings: database and file. This do - `default-integration` - Default integration to assume. - `analytics-config.auction-events.` - defines which channels are supported by analytics for this account - `bid-validations.banner-creative-max-size` - Overrides creative max size validation for banners. +- `status` - allows to mark account as `active` or `inactive`. Here are the definitions of the "purposes" that can be defined in the GDPR setting configurations: ``` @@ -75,6 +76,7 @@ accounts: analytics-config: auction-events: amp: true + status: active gdpr: enabled: true integration-enabled: diff --git a/docs/config-app.md b/docs/config-app.md index 988297c2684..8557a6a3d67 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -261,6 +261,24 @@ For HTTP data source available next options: For account processing rules available next options: - `settings.enforce-valid-account` - if equals to `true` then request without account id will be rejected with 401. +It is possible to specify default account configuration values that will be assumed if account config have them +unspecified or missing at all. Example: +```yaml +settings: + default-account-config: + events-enabled: true + enforce-ccpa: true + gdpr: '{"enabled": true}' + analytics-sampling-factor: 1 + default-integration: pbjs + analytics-config: '{"auction-events":{"amp":true}}' +``` +See [application settings](application-settings.md) for full reference of available configuration parameters. +Be aware that individual configuration values will not be merged with concrete +account values if they exist in account configuration but account value will completely replace the default value. For +example, if account configuration defines `gdpr` field, it will completely replace `settings.default-account-config.gdpr` +value in the final account configuration model. + For caching available next options: - `settings.in-memory-cache.ttl-seconds` - how long (in seconds) data will be available in LRU cache. - `settings.in-memory-cache.cache-size` - the size of LRU cache. diff --git a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java index c2e254738f0..7160ae670ee 100644 --- a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java @@ -52,6 +52,7 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountStatus; import org.prebid.server.util.HttpUtil; import org.prebid.server.validation.RequestValidator; import org.prebid.server.validation.model.ValidationResult; @@ -778,7 +779,7 @@ private Future accountFrom(BidRequest bidRequest, Timeout timeout, Rout return blankAccountId ? responseForEmptyAccount(routingContext) : applicationSettings.getAccountById(accountId, timeout) - .recover(exception -> accountFallback(exception, accountId, routingContext)); + .compose(this::ensureAccountActive, exception -> accountFallback(exception, accountId, routingContext)); } /** @@ -844,6 +845,15 @@ private Future responseForUnknownAccount(String accountId) { : Future.succeededFuture(Account.empty(accountId)); } + private Future ensureAccountActive(Account account) { + final String accountId = account.getId(); + + return account.getStatus() == AccountStatus.inactive + ? Future.failedFuture( + new UnauthorizedAccountException(String.format("Account %s is inactive", accountId), accountId)) + : Future.succeededFuture(account); + } + private BidRequest enrichBidRequestWithAccountAndPrivacyData( BidRequest bidRequest, Account account, PrivacyContext privacyContext) { diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java new file mode 100644 index 00000000000..0a5df0756cb --- /dev/null +++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java @@ -0,0 +1,71 @@ +package org.prebid.server.settings; + +import io.vertx.core.Future; +import org.prebid.server.execution.Timeout; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredResponseDataResult; + +import java.util.Objects; +import java.util.Set; + +public class EnrichingApplicationSettings implements ApplicationSettings { + + private final ApplicationSettings delegate; + private final Account defaultAccount; + + public EnrichingApplicationSettings(ApplicationSettings delegate, Account defaultAccount) { + this.delegate = Objects.requireNonNull(delegate); + this.defaultAccount = Objects.equals(Account.builder().build(), defaultAccount) ? null : defaultAccount; + } + + @Override + public Future getAccountById(String accountId, Timeout timeout) { + final Future accountFuture = delegate.getAccountById(accountId, timeout); + + if (defaultAccount == null) { + return accountFuture; + } + + return accountFuture + .map(account -> account.merge(defaultAccount)) + .otherwise(Account.empty(accountId).merge(defaultAccount)); + } + + @Override + public Future getAdUnitConfigById(String adUnitConfigId, Timeout timeout) { + return delegate.getAdUnitConfigById(adUnitConfigId, timeout); + } + + @Override + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return delegate.getStoredData(accountId, requestIds, impIds, timeout); + } + + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return delegate.getStoredResponses(responseIds, timeout); + } + + @Override + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return delegate.getAmpStoredData(accountId, requestIds, impIds, timeout); + } + + @Override + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return delegate.getVideoStoredData(accountId, requestIds, impIds, timeout); + } +} diff --git a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java index 3190652d84b..85bbf93b74f 100644 --- a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java @@ -15,6 +15,7 @@ import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.settings.model.AccountBidValidationConfig; import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.AccountStatus; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; import org.prebid.server.vertx.jdbc.JdbcClient; @@ -108,7 +109,8 @@ public JdbcApplicationSettings(JdbcClient jdbcClient, */ @Override public Future getAccountById(String accountId, Timeout timeout) { - return jdbcClient.executeQuery(selectAccountQuery, + return jdbcClient.executeQuery( + selectAccountQuery, Collections.singletonList(accountId), result -> mapToModelOrError(result, row -> Account.builder() .id(row.getString(0)) @@ -123,6 +125,7 @@ public Future getAccountById(String accountId, Timeout timeout) { .defaultIntegration(row.getString(9)) .analyticsConfig(toModel(row.getString(10), AccountAnalyticsConfig.class)) .bidValidations(toModel(row.getString(11), AccountBidValidationConfig.class)) + .status(toAccountStatus(row.getString(12))) .build()), timeout) .compose(result -> failedIfNull(result, accountId, "Account")); @@ -171,6 +174,14 @@ private T toModel(String source, Class targetClass) { } } + private static AccountStatus toAccountStatus(String status) { + try { + return AccountStatus.valueOf(status); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + /** * Runs a process to get stored requests by a collection of ids from database * and returns {@link Future<{@link StoredDataResult }>}. diff --git a/src/main/java/org/prebid/server/settings/model/Account.java b/src/main/java/org/prebid/server/settings/model/Account.java index 7e736416298..75879559cbe 100644 --- a/src/main/java/org/prebid/server/settings/model/Account.java +++ b/src/main/java/org/prebid/server/settings/model/Account.java @@ -2,6 +2,7 @@ import lombok.Builder; import lombok.Value; +import org.apache.commons.lang3.ObjectUtils; @Builder @Value @@ -31,6 +32,27 @@ public class Account { AccountBidValidationConfig bidValidations; + AccountStatus status; + + public Account merge(Account another) { + return Account.builder() + .id(ObjectUtils.firstNonNull(id, another.id)) + .priceGranularity(ObjectUtils.firstNonNull(priceGranularity, another.priceGranularity)) + .bannerCacheTtl(ObjectUtils.firstNonNull(bannerCacheTtl, another.bannerCacheTtl)) + .videoCacheTtl(ObjectUtils.firstNonNull(videoCacheTtl, another.videoCacheTtl)) + .eventsEnabled(ObjectUtils.firstNonNull(eventsEnabled, another.eventsEnabled)) + .enforceCcpa(ObjectUtils.firstNonNull(enforceCcpa, another.enforceCcpa)) + .gdpr(ObjectUtils.firstNonNull(gdpr, another.gdpr)) + .analyticsSamplingFactor(ObjectUtils.firstNonNull( + analyticsSamplingFactor, another.analyticsSamplingFactor)) + .truncateTargetAttr(ObjectUtils.firstNonNull(truncateTargetAttr, another.truncateTargetAttr)) + .defaultIntegration(ObjectUtils.firstNonNull(defaultIntegration, another.defaultIntegration)) + .analyticsConfig(ObjectUtils.firstNonNull(analyticsConfig, another.analyticsConfig)) + .bidValidations(ObjectUtils.firstNonNull(bidValidations, another.bidValidations)) + .status(ObjectUtils.firstNonNull(status, another.status)) + .build(); + } + public static Account empty(String id) { return Account.builder() .id(id) diff --git a/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java b/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java index 62293e59059..16eb51b5f1f 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java @@ -1,15 +1,11 @@ package org.prebid.server.settings.model; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.Value; @Builder -@AllArgsConstructor -@NoArgsConstructor -@Data +@Value public class AccountGdprConfig { @JsonProperty("enabled") diff --git a/src/main/java/org/prebid/server/settings/model/AccountStatus.java b/src/main/java/org/prebid/server/settings/model/AccountStatus.java new file mode 100644 index 00000000000..0ac0ebcb66f --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountStatus.java @@ -0,0 +1,6 @@ +package org.prebid.server.settings.model; + +public enum AccountStatus { + + active, inactive +} 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 fc21729a7b6..06488c21fb5 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -15,12 +15,14 @@ import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.CachingApplicationSettings; import org.prebid.server.settings.CompositeApplicationSettings; +import org.prebid.server.settings.EnrichingApplicationSettings; import org.prebid.server.settings.FileApplicationSettings; import org.prebid.server.settings.HttpApplicationSettings; import org.prebid.server.settings.JdbcApplicationSettings; import org.prebid.server.settings.SettingsCache; import org.prebid.server.settings.service.HttpPeriodicRefreshService; import org.prebid.server.settings.service.JdbcPeriodicRefreshService; +import org.prebid.server.spring.config.model.AccountConfigurationProperties; import org.prebid.server.spring.config.model.CircuitBreakerProperties; import org.prebid.server.vertx.ContextRunner; import org.prebid.server.vertx.http.HttpClient; @@ -326,13 +328,33 @@ CompositeApplicationSettings compositeApplicationSettings( } } + @Configuration + static class EnrichingSettingsConfiguration { + + @Bean + @ConfigurationProperties("settings.default-account-config") + AccountConfigurationProperties defaultAccountConfigurationProperties() { + return new AccountConfigurationProperties(); + } + + @Bean + EnrichingApplicationSettings enrichingApplicationSettings( + CompositeApplicationSettings compositeApplicationSettings, + AccountConfigurationProperties defaultAccountConfigurationProperties, + JacksonMapper mapper) { + + return new EnrichingApplicationSettings( + compositeApplicationSettings, defaultAccountConfigurationProperties.toAccount(mapper)); + } + } + @Configuration static class CachingSettingsConfiguration { @Bean @ConditionalOnProperty(prefix = "settings.in-memory-cache", name = {"ttl-seconds", "cache-size"}) CachingApplicationSettings cachingApplicationSettings( - CompositeApplicationSettings compositeApplicationSettings, + EnrichingApplicationSettings enrichingApplicationSettings, ApplicationSettingsCacheProperties cacheProperties, @Qualifier("settingsCache") SettingsCache cache, @Qualifier("ampSettingsCache") SettingsCache ampCache, @@ -340,7 +362,7 @@ CachingApplicationSettings cachingApplicationSettings( Metrics metrics) { return new CachingApplicationSettings( - compositeApplicationSettings, + enrichingApplicationSettings, cache, ampCache, videoCache, @@ -356,8 +378,8 @@ static class ApplicationSettingsConfiguration { @Bean ApplicationSettings applicationSettings( @Autowired(required = false) CachingApplicationSettings cachingApplicationSettings, - @Autowired(required = false) CompositeApplicationSettings compositeApplicationSettings) { - return ObjectUtils.defaultIfNull(cachingApplicationSettings, compositeApplicationSettings); + EnrichingApplicationSettings enrichingApplicationSettings) { + return ObjectUtils.defaultIfNull(cachingApplicationSettings, enrichingApplicationSettings); } } diff --git a/src/main/java/org/prebid/server/spring/config/model/AccountConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/model/AccountConfigurationProperties.java new file mode 100644 index 00000000000..3eac6b136cb --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/model/AccountConfigurationProperties.java @@ -0,0 +1,56 @@ +package org.prebid.server.spring.config.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; +import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.AccountStatus; + +@Data +@NoArgsConstructor +public class AccountConfigurationProperties { + + private String priceGranularity; + + private Integer bannerCacheTtl; + + private Integer videoCacheTtl; + + private Boolean eventsEnabled; + + private Boolean enforceCcpa; + + private String gdpr; + + private Integer analyticsSamplingFactor; + + private Integer truncateTargetAttr; + + private String defaultIntegration; + + private String analyticsConfig; + + private AccountStatus status; + + public Account toAccount(JacksonMapper mapper) { + return Account.builder() + .priceGranularity(getPriceGranularity()) + .bannerCacheTtl(getBannerCacheTtl()) + .videoCacheTtl(getVideoCacheTtl()) + .eventsEnabled(getEventsEnabled()) + .enforceCcpa(getEnforceCcpa()) + .gdpr(toModel(mapper, getGdpr(), AccountGdprConfig.class)) + .analyticsSamplingFactor(getAnalyticsSamplingFactor()) + .truncateTargetAttr(getTruncateTargetAttr()) + .defaultIntegration(getDefaultIntegration()) + .analyticsConfig(toModel(mapper, getAnalyticsConfig(), AccountAnalyticsConfig.class)) + .status(getStatus()) + .build(); + } + + private static T toModel(JacksonMapper mapper, String source, Class targetClass) { + return source != null ? mapper.decodeValue(source, targetClass) : null; + } +} diff --git a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java index 732a13b1b38..527f8fd55a1 100644 --- a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java @@ -26,7 +26,6 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.assertion.FutureAssertion; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.IpAddress; import org.prebid.server.bidder.BidderCatalog; @@ -63,6 +62,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountStatus; import org.prebid.server.validation.RequestValidator; import org.prebid.server.validation.model.ValidationResult; @@ -89,6 +89,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; +import static org.prebid.server.assertion.FutureAssertion.assertThat; public class AuctionRequestFactoryTest extends VertxTest { @@ -272,6 +273,32 @@ public void shouldReturnFailedFutureIfAccountIsEnforcedAndFailedGetAccountById() .hasMessage("Unauthorized account id: absentId"); } + @Test + public void shouldReturnFailedFutureIfAccountIsInactive() { + // given + given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(Account.builder() + .id("accountId") + .status(AccountStatus.inactive) + .build())); + + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder() + .publisher(Publisher.builder().id("accountId").build()) + .build()) + .build(); + + givenBidRequest(bidRequest); + + // when + final Future future = factory.fromRequest(routingContext, 0L); + + // then + assertThat(future).isFailed(); + assertThat(future.cause()) + .isInstanceOf(UnauthorizedAccountException.class) + .hasMessage("Account accountId is inactive"); + } + @Test public void shouldReturnFailedFutureIfRequestBodyExceedsMaxRequestSize() { // given @@ -1643,7 +1670,7 @@ public void shouldReturnAuctionContextWithWebRequestTypeMetric() { final Future auctionContextFuture = factory.fromRequest(routingContext, 0L); // then - FutureAssertion.assertThat(auctionContextFuture).isSucceeded(); + assertThat(auctionContextFuture).isSucceeded(); assertThat(auctionContextFuture.result().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2web); } @@ -1656,7 +1683,7 @@ public void shouldReturnAuctionContextWithAppRequestTypeMetric() { final Future auctionContextFuture = factory.fromRequest(routingContext, 0L); // then - FutureAssertion.assertThat(auctionContextFuture).isSucceeded(); + assertThat(auctionContextFuture).isSucceeded(); assertThat(auctionContextFuture.result().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2app); } @@ -1678,7 +1705,7 @@ public void shouldEnrichRequestWithIpAddressAndCountryAndSaveAuctionContext() { final Future auctionContextFuture = factory.fromRequest(routingContext, 0L); // then - FutureAssertion.assertThat(auctionContextFuture).isSucceeded(); + assertThat(auctionContextFuture).isSucceeded(); final AuctionContext auctionContext = auctionContextFuture.result(); assertThat(auctionContext.getBidRequest().getDevice()).isEqualTo( diff --git a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java new file mode 100644 index 00000000000..be99c9a42ce --- /dev/null +++ b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java @@ -0,0 +1,144 @@ +package org.prebid.server.settings; + +import io.vertx.core.Future; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.execution.Timeout; +import org.prebid.server.settings.model.Account; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.prebid.server.assertion.FutureAssertion.assertThat; + +public class EnrichingApplicationSettingsTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private ApplicationSettings delegate; + + private EnrichingApplicationSettings enrichingApplicationSettings; + + @Mock + private Timeout timeout; + + @Test + public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsNull() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings(delegate, null); + + final Account returnedAccount = Account.builder().build(); + given(delegate.getAccountById(anyString(), any())).willReturn(Future.succeededFuture(returnedAccount)); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).isSucceeded(); + assertThat(accountFuture.result()).isSameAs(returnedAccount); + + verify(delegate).getAccountById(eq("123"), eq(timeout)); + } + + @Test + public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsEmpty() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings( + delegate, + Account.builder() + .bannerCacheTtl(null) + .analyticsSamplingFactor(null) + .build()); + + final Account returnedAccount = Account.builder().build(); + given(delegate.getAccountById(anyString(), any())).willReturn(Future.succeededFuture(returnedAccount)); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).isSucceeded(); + assertThat(accountFuture.result()).isSameAs(returnedAccount); + + verify(delegate).getAccountById(eq("123"), eq(timeout)); + } + + @Test + public void getAccountByIdShouldMergeAccountWithDefaultAccount() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings( + delegate, + Account.builder() + .bannerCacheTtl(100) + .analyticsSamplingFactor(50) + .build()); + + given(delegate.getAccountById(anyString(), any())).willReturn(Future.succeededFuture(Account.builder() + .id("123") + .videoCacheTtl(200) + .enforceCcpa(true) + .build())); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).succeededWith(Account.builder() + .id("123") + .bannerCacheTtl(100) + .videoCacheTtl(200) + .enforceCcpa(true) + .analyticsSamplingFactor(50) + .build()); + } + + @Test + public void getAccountByIdShouldReturnDefaultAccountWhenDelegateFailed() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings( + delegate, + Account.builder() + .bannerCacheTtl(100) + .analyticsSamplingFactor(50) + .build()); + + given(delegate.getAccountById(anyString(), any())).willReturn(Future.failedFuture("Exception")); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).succeededWith(Account.builder() + .id("123") + .bannerCacheTtl(100) + .analyticsSamplingFactor(50) + .build()); + } + + @Test + public void getAccountByIdShouldPassOnFailureWhenDefaultAccountIsEmpty() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings( + delegate, + Account.builder() + .bannerCacheTtl(null) + .analyticsSamplingFactor(null) + .build()); + + given(delegate.getAccountById(anyString(), any())).willReturn(Future.failedFuture("Exception")); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).isFailed(); + } +} diff --git a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java index aab6540a56f..9581f3cbb5f 100644 --- a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java @@ -13,6 +13,7 @@ import org.prebid.server.settings.model.AccountBidValidationConfig; import org.prebid.server.settings.model.AccountGdprConfig; import org.prebid.server.settings.model.BidValidationEnforcement; +import org.prebid.server.settings.model.AccountStatus; import org.prebid.server.settings.model.EnabledForRequestType; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -109,7 +110,8 @@ public void getAccountByIdShouldReturnPresentAccount() { + "}," + "bidValidations: {" + "banner-creative-max-size: 'enforce'" - + "}" + + "}," + + "status: 'active'" + "}" + "]")); @@ -146,6 +148,7 @@ public void getAccountByIdShouldReturnPresentAccount() { .defaultIntegration("web") .analyticsConfig(AccountAnalyticsConfig.of(singletonMap("amp", true))) .bidValidations(AccountBidValidationConfig.of(BidValidationEnforcement.enforce)) + .status(AccountStatus.active) .build()); } diff --git a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java index 3e4f43ee60a..b196f717ec6 100644 --- a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java @@ -26,6 +26,7 @@ import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.settings.model.AccountBidValidationConfig; import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.AccountStatus; import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.settings.model.EnabledForRequestType; import org.prebid.server.settings.model.StoredDataResult; @@ -63,7 +64,7 @@ public class JdbcApplicationSettingsTest extends VertxTest { private static final String SELECT_ACCOUNT_QUERY = "SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, " + "events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, " - + "default_integration, analytics_config, bid_validations " + + "default_integration, analytics_config, bid_validations, status " + "FROM accounts_account where uuid = %ACCOUNT_ID% LIMIT 1"; private static final String SELECT_QUERY = @@ -114,7 +115,8 @@ public static void beforeClass() throws SQLException { + "uuid varchar(40) NOT NULL, price_granularity varchar(6), granularityMultiplier numeric(9,3), " + "banner_cache_ttl INT, video_cache_ttl INT, events_enabled BIT, enforce_ccpa BIT, " + "tcf_config varchar(512), analytics_sampling_factor INT, truncate_target_attr INT, " - + "default_integration varchar(64), analytics_config varchar(512), bid_validations varchar(512));"); + + "default_integration varchar(64), analytics_config varchar(512), bid_validations varchar(512), " + + "status varchar(25));"); connection.createStatement().execute("CREATE TABLE s2sconfig_config (id SERIAL PRIMARY KEY, uuid varchar(40) " + "NOT NULL, config varchar(512));"); connection.createStatement().execute("CREATE TABLE stored_requests (id SERIAL PRIMARY KEY, " @@ -133,10 +135,11 @@ public static void beforeClass() throws SQLException { connection.createStatement().execute("insert into accounts_account " + "(uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, " + "tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, " - + "bid_validations) " + + "bid_validations, status) " + "values ('1001','med', 100, 100, TRUE, TRUE, '{\"enabled\": true, " + "\"integration-enabled\": {\"amp\": true, \"app\": true, \"video\": true, \"web\": true}}', 1, 0, " - + "'web', '{\"auction-events\": {\"amp\": true}}', '{\"banner-creative-max-size\": \"enforce\"}');"); + + "'web', '{\"auction-events\": {\"amp\": true}}', '{\"banner-creative-max-size\": \"enforce\"}', " + + "'active');"); connection.createStatement().execute( "insert into s2sconfig_config (uuid, config) values ('adUnitConfigId', 'config');"); connection.createStatement().execute( @@ -209,6 +212,7 @@ public void getAccountByIdShouldReturnAccountWithAllFieldsPopulated(TestContext .defaultIntegration("web") .analyticsConfig(AccountAnalyticsConfig.of(singletonMap("amp", true))) .bidValidations(AccountBidValidationConfig.of(BidValidationEnforcement.enforce)) + .status(AccountStatus.active) .build()); async.complete(); })); From 1f2d59b1890150eb21fa4a426998e90ca3ea0774 Mon Sep 17 00:00:00 2001 From: Braslavskyi Andrii Date: Fri, 15 Jan 2021 16:11:14 +0200 Subject: [PATCH 082/129] Update currencyService to accept bidRequest as parameter (#968) --- .../server/auction/ExchangeService.java | 17 +---- .../currency/CurrencyConversionService.java | 33 ++++++++- .../CurrencyConversionServiceTest.java | 70 ++++++++++++------- .../server/auction/ExchangeServiceTest.java | 38 +++++----- 4 files changed, 96 insertions(+), 62 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 4d78f1a4415..dbd39b5c921 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -45,7 +45,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigFpd; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; @@ -190,18 +189,6 @@ private static ExtRequestTargeting targeting(BidRequest bidRequest) { return prebid != null ? prebid.getTargeting() : null; } - private static Map> currencyRates(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); - final ExtRequestCurrency currency = prebid != null ? prebid.getCurrency() : null; - return currency != null ? currency.getRates() : null; - } - - private static Boolean usepbsrates(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); - final ExtRequestCurrency currency = prebid != null ? prebid.getCurrency() : null; - return currency != null ? currency.getUsepbsrates() : null; - } - /** * Creates {@link BidRequestCacheInfo} based on {@link BidRequest} model. */ @@ -903,7 +890,6 @@ private BidderResponse applyBidPriceChanges(BidderResponse bidderResponse, BidRe final String adServerCurrency = bidRequest.getCur().get(0); final BigDecimal priceAdjustmentFactor = bidAdjustmentForBidder(bidRequest, bidderResponse.getBidder()); - final Boolean usepbsrates = usepbsrates(bidRequest); for (final BidderBid bidderBid : bidderBids) { final Bid bid = bidderBid.getBid(); @@ -911,8 +897,7 @@ private BidderResponse applyBidPriceChanges(BidderResponse bidderResponse, BidRe final BigDecimal price = bid.getPrice(); try { final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency( - price, currencyRates(bidRequest), adServerCurrency, - StringUtils.stripToNull(bidCurrency), usepbsrates); + price, bidRequest, adServerCurrency, StringUtils.stripToNull(bidCurrency)); final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency); diff --git a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java index 5027fb6e447..21ebcb07a9a 100644 --- a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java +++ b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java @@ -1,5 +1,6 @@ package org.prebid.server.currency; +import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.logging.Logger; @@ -10,6 +11,9 @@ import org.prebid.server.currency.proto.CurrencyConversionRates; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.util.HttpUtil; import org.prebid.server.vertx.Initializable; @@ -159,13 +163,23 @@ public Map> getExternalCurrencyRates() { return externalCurrencyRates; } + /** + * Converts price from fromCurrency to toCurrency using rates from {@link BidRequest} or external currency service. + * If bidrequest.prebid.currecy.usepbsrates is true it takes rates from prebid server, if false from request. + * Default value of usepbsrates is true. + * Throws {@link PreBidException} in case conversion is not possible. + */ + public BigDecimal convertCurrency(BigDecimal price, BidRequest bidRequest, String fromCurrency, String toCurrency) { + return convertCurrency(price, currencyRates(bidRequest), fromCurrency, toCurrency, usepbsrates(bidRequest)); + } + /** * Converts price from bidCurrency to adServerCurrency using rates and usepbsrates flag defined in request. * If usepbsrates is true it takes rates from prebid server, if false from request. Default value of usepbsrates * is true. * Throws {@link PreBidException} in case conversion is not possible. */ - public BigDecimal convertCurrency(BigDecimal price, Map> requestCurrencyRates, + private BigDecimal convertCurrency(BigDecimal price, Map> requestCurrencyRates, String adServerCurrency, String bidCurrency, Boolean usepbsrates) { // use Default USD currency if bidder left this field empty. After, when bidder will implement multi currency // support it will be changed to throwing PrebidException. @@ -198,6 +212,23 @@ public BigDecimal convertCurrency(BigDecimal price, Map> currencyRates(BidRequest bidRequest) { + final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestCurrency currency = prebid != null ? prebid.getCurrency() : null; + return currency != null ? currency.getRates() : null; + } + + private static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { + final ExtRequest requestExt = bidRequest.getExt(); + return requestExt != null ? requestExt.getPrebid() : null; + } + + private static Boolean usepbsrates(BidRequest bidRequest) { + final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestCurrency currency = prebid != null ? prebid.getCurrency() : null; + return currency != null ? currency.getUsepbsrates() : null; + } + /** * Returns conversion rate from the given currency rates according to priority. */ diff --git a/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java b/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java index 23f2ea4368b..f24f4d6149e 100644 --- a/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java +++ b/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java @@ -1,6 +1,7 @@ package org.prebid.server.auction; import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; @@ -16,6 +17,9 @@ import org.prebid.server.currency.proto.CurrencyConversionRates; import org.prebid.server.exception.PreBidException; import org.prebid.server.metric.Metrics; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.vertx.http.HttpClient; import org.prebid.server.vertx.http.model.HttpClientResponse; @@ -117,7 +121,8 @@ public void convertCurrencyShouldReturnSamePriceIfBidAndServerCurrenciesEquals() final BigDecimal price = BigDecimal.valueOf(100); // when - final BigDecimal convertedPrice = currencyService.convertCurrency(price, null, USD, USD, false); + final BigDecimal convertedPrice = currencyService.convertCurrency(price, + givenBidRequestWithCurrencies(null, false), USD, USD); // then assertThat(convertedPrice).isSameAs(price); @@ -130,8 +135,8 @@ public void convertCurrencyShouldUseUSDByDefaultIfBidCurrencyIsNull() { singletonMap(GBP, singletonMap(USD, BigDecimal.valueOf(1.4306))); // when - final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, GBP, null, - false); + final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(requestConversionRates, false), GBP, null); // then assertThat(price.compareTo(BigDecimal.valueOf(0.699))).isEqualTo(0); @@ -144,8 +149,8 @@ public void convertCurrencyShouldReturnConvertedByStraightMultiplierPrice() { singletonMap(EUR, BigDecimal.valueOf(1.1565))); // when - final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, GBP, EUR, - false); + final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(requestConversionRates, false), GBP, EUR); // then assertThat(price.compareTo(BigDecimal.valueOf(0.865))).isEqualTo(0); @@ -158,8 +163,8 @@ public void convertCurrencyShouldReturnConvertedByInvertedMultiplierPrice() { BigDecimal.valueOf(1.1565))); // when - final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, GBP, - false); + final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(requestConversionRates, false), EUR, GBP); // then assertThat(price.compareTo(BigDecimal.valueOf(1.156))).isEqualTo(0); @@ -173,8 +178,8 @@ public void convertCurrencyShouldReturnConvertedByIntermediateMultiplierPrice() requestConversionRates.put(EUR, singletonMap(USD, BigDecimal.valueOf(1.2304))); // when - final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, GBP, - false); + final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(requestConversionRates, false), EUR, GBP); // then assertThat(price.compareTo(BigDecimal.valueOf(1.163))).isEqualTo(0); @@ -187,8 +192,8 @@ public void convertCurrencyShouldReturnConvertedBySingleDigitMultiplierPrice() { requestConversionRates.put(EUR, singletonMap(USD, BigDecimal.valueOf(0.5))); // when - final BigDecimal price = currencyService.convertCurrency(new BigDecimal("1.23"), requestConversionRates, EUR, - USD, false); + final BigDecimal price = currencyService.convertCurrency(new BigDecimal("1.23"), + givenBidRequestWithCurrencies(requestConversionRates, false), EUR, USD); // then assertThat(price.compareTo(BigDecimal.valueOf(2.460))).isEqualTo(0); @@ -197,7 +202,8 @@ public void convertCurrencyShouldReturnConvertedBySingleDigitMultiplierPrice() { @Test public void convertCurrencyShouldUseLatestRatesIfRequestRatesIsNull() { // when - final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, null, EUR, GBP, false); + final BigDecimal price = currencyService.convertCurrency( + BigDecimal.ONE, givenBidRequestWithCurrencies(null, false), EUR, GBP); // then assertThat(price.compareTo(BigDecimal.valueOf(1.149))).isEqualTo(0); @@ -210,8 +216,8 @@ public void convertCurrencyShouldUseConversionRateFromServerIfusepbsratesIsTrue( requestConversionRates.put(EUR, singletonMap(USD, BigDecimal.valueOf(0.6))); // when - final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, GBP, - true); + final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(requestConversionRates, true), EUR, GBP); // then assertThat(price.compareTo(BigDecimal.valueOf(1.149))).isEqualTo(0); @@ -224,8 +230,8 @@ public void convertCurrencyShouldUseConversionRateFromRequestIfusepbsratesIsFals BigDecimal.valueOf(0.6))); // when - final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, USD, - false); + final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(requestConversionRates, false), EUR, USD); // then assertThat(price.compareTo(BigDecimal.valueOf(1.667))).isEqualTo(0); @@ -238,8 +244,8 @@ public void convertCurrencyShouldUseLatestRatesIfMultiplierWasNotFoundInRequestR singletonMap(EUR, BigDecimal.valueOf(0.8434))); // when - final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, UAH, - false); + final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(requestConversionRates, false), EUR, UAH); // then assertThat(price.compareTo(BigDecimal.valueOf(1.156))).isEqualTo(0); @@ -248,7 +254,8 @@ public void convertCurrencyShouldUseLatestRatesIfMultiplierWasNotFoundInRequestR @Test public void convertCurrencyShouldReturnSamePriceIfBidCurrencyIsNullAndServerCurrencyUSD() { // when - final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, emptyMap(), USD, null, false); + final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(emptyMap(), false), USD, null); // then assertThat(price.compareTo(BigDecimal.ONE)).isEqualTo(0); @@ -261,8 +268,8 @@ public void convertCurrencyShouldFailWhenRequestRatesIsNullAndNoExternalRatesPro // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> currencyConversionService.convertCurrency(BigDecimal.ONE, null, EUR, GBP, - false)) + .isThrownBy(() -> currencyConversionService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(null, false), EUR, GBP)) .withMessage("Unable to convert bid currency GBP to desired ad server currency EUR"); } @@ -270,7 +277,8 @@ public void convertCurrencyShouldFailWhenRequestRatesIsNullAndNoExternalRatesPro public void convertCurrencyShouldThrowPrebidExceptionIfServerAndRequestRatesAreNull() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, null, USD, EUR, false)) + .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(null, false), USD, EUR)) .withMessage("Unable to convert bid currency EUR to desired ad server currency USD"); } @@ -287,8 +295,8 @@ public void convertCurrencyShouldThrowPrebidExceptionIfMultiplierWasNotFoundFrom // then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, AUD, - false)) + .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(requestConversionRates, false), EUR, AUD)) .withMessage("Unable to convert bid currency AUD to desired ad server currency EUR"); } @@ -302,7 +310,8 @@ public void convertCurrencyShouldThrowExceptionWhenCurrencyServerResponseStatusN // then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, null, UAH, AUD, false)) + .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(null, false), UAH, AUD)) .withMessage("Unable to convert bid currency AUD to desired ad server currency UAH"); } @@ -316,7 +325,8 @@ public void convertCurrencyShouldThrowExceptionWhenCurrencyServerResponseContain // then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, null, UAH, AUD, false)) + .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, + givenBidRequestWithCurrencies(null, false), UAH, AUD)) .withMessage("Unable to convert bid currency AUD to desired ad server currency UAH"); } @@ -369,4 +379,12 @@ private static void givenHttpClientReturnsResponse(HttpClient httpClient, int st given(httpClient.get(anyString(), anyLong())) .willReturn(Future.succeededFuture(httpClientResponse)); } + + private BidRequest givenBidRequestWithCurrencies(Map> requestCurrencies, + Boolean usepbsrates) { + return BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .currency(ExtRequestCurrency.of(requestCurrencies, usepbsrates)).build())) + .build(); + } } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 6fb43852308..fae65f3d4d0 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -191,7 +191,7 @@ public void setUp() { given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); given(usersyncer.getCookieFamilyName()).willReturn("cookieFamily"); - given(currencyService.convertCurrency(any(), any(), any(), any(), any())) + given(currencyService.convertCurrency(any(), any(), any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); given(storedResponseProcessor.getStoredResponseResult(any(), any(), any())) @@ -1876,7 +1876,7 @@ public void shouldReturnBidsWithUpdatedPriceCurrencyConversion() { identity()); final BigDecimal updatedPrice = BigDecimal.valueOf(5.0); - given(currencyService.convertCurrency(any(), any(), any(), any(), anyBoolean())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); givenBidResponseCreator(singletonList(Bid.builder().price(updatedPrice).build())); @@ -1900,7 +1900,7 @@ public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { identity()); // returns the same price as in argument - given(currencyService.convertCurrency(any(), any(), any(), any(), anyBoolean())) + given(currencyService.convertCurrency(any(), any(), any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); // when @@ -1923,7 +1923,7 @@ public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); - given(currencyService.convertCurrency(any(), any(), any(), any(), any())) + given(currencyService.convertCurrency(any(), any(), any(), any())) .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); // when @@ -1957,7 +1957,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactor() .auctiontimestamp(1000L) .build()))); - given(currencyService.convertCurrency(any(), any(), any(), any(), any())) + given(currencyService.convertCurrency(any(), any(), any(), any())) .willReturn(BigDecimal.valueOf(10)); // when @@ -1992,7 +1992,7 @@ public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForS identity()); final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any(), any())).willReturn(updatedPrice) + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice) .willThrow( new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD")); @@ -2002,8 +2002,8 @@ public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForS // then final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean()); - verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(null), any(), eq("CUR1"), eq(null)); - verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(null), any(), eq("CUR2"), eq(null)); + verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), any(), eq("CUR1")); + verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), any(), eq("CUR2")); assertThat(argumentCaptor.getValue()).hasSize(1); @@ -2033,8 +2033,8 @@ public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupported identity()))).build(); final BigDecimal updatedPrice = BigDecimal.valueOf(20); - given(currencyService.convertCurrency(any(), any(), any(), any(), any())).willReturn(updatedPrice); - given(currencyService.convertCurrency(any(), any(), eq("BAD"), eq("CUR"), any())) + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), eq("BAD"), eq("CUR"))) .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency BAD")); // when @@ -2043,8 +2043,8 @@ public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupported // then final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean()); - verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(null), eq("BAD"), eq("USD"), eq(null)); - verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(null), eq("BAD"), eq("CUR"), eq(null)); + verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("BAD"), eq("USD")); + verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("BAD"), eq("CUR")); assertThat(argumentCaptor.getValue()).hasSize(2); @@ -2076,7 +2076,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndAddErrorAboutMultipleCu builder -> builder.cur(asList("CUR1", "CUR2", "CUR2"))); final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any(), any())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); // when exchangeService.holdAuction(givenRequestContext(bidRequest)).result(); @@ -2084,7 +2084,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndAddErrorAboutMultipleCu // then final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean()); - verify(currencyService).convertCurrency(eq(bidderPrice), eq(null), eq("CUR1"), eq("USD"), eq(null)); + verify(currencyService).convertCurrency(eq(bidderPrice), eq(bidRequest), eq("CUR1"), eq("USD")); assertThat(argumentCaptor.getValue()).hasSize(1); @@ -2120,8 +2120,8 @@ public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { singletonList(givenImp(impBidders, identity())), builder -> builder.cur(singletonList("USD"))); final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any(), any())).willReturn(updatedPrice); - given(currencyService.convertCurrency(any(), any(), any(), eq("USD"), any())).willReturn(bidder3Price); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), any(), eq("USD"))).willReturn(bidder3Price); // when exchangeService.holdAuction(givenRequestContext(bidRequest)).result(); @@ -2129,9 +2129,9 @@ public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { // then final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean()); - verify(currencyService).convertCurrency(eq(bidder1Price), eq(null), eq("USD"), eq("EUR"), eq(null)); - verify(currencyService).convertCurrency(eq(bidder2Price), eq(null), eq("USD"), eq("GBP"), eq(null)); - verify(currencyService).convertCurrency(eq(bidder3Price), eq(null), eq("USD"), eq("USD"), eq(null)); + verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("USD"), eq("EUR")); + verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("USD"), eq("GBP")); + verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD")); verifyNoMoreInteractions(currencyService); assertThat(argumentCaptor.getValue()) From c1162fa5f623ea44b78ce5adb13a9017995bedb9 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Fri, 15 Jan 2021 16:13:18 +0200 Subject: [PATCH 083/129] Add errorWithKey for Conditional logger (#1099) --- src/main/java/org/prebid/server/log/ConditionalLogger.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/prebid/server/log/ConditionalLogger.java b/src/main/java/org/prebid/server/log/ConditionalLogger.java index 0a2fdd59d8d..0b1f9d5c4cd 100644 --- a/src/main/java/org/prebid/server/log/ConditionalLogger.java +++ b/src/main/java/org/prebid/server/log/ConditionalLogger.java @@ -56,6 +56,10 @@ public void info(String message, long duration, TimeUnit unit) { log(message, duration, unit, logger -> logger.info(message)); } + public void errorWithKey(String key, String message, int limit) { + log(key, limit, logger -> logger.error(message)); + } + public void error(String message, int limit) { log(message, limit, logger -> logger.error(message)); } From 1e8e9f273ae654c19c6bc93c7f444dfabdb0f864 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Mon, 18 Jan 2021 12:23:56 +0200 Subject: [PATCH 084/129] Fix undefined Account status fetching (#1101) --- .../settings/JdbcApplicationSettings.java | 3 +++ .../settings/JdbcApplicationSettingsTest.java | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java index 85bbf93b74f..952d11e24a7 100644 --- a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java @@ -175,6 +175,9 @@ private T toModel(String source, Class targetClass) { } private static AccountStatus toAccountStatus(String status) { + if (status == null) { + return null; + } try { return AccountStatus.valueOf(status); } catch (IllegalArgumentException e) { diff --git a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java index b196f717ec6..804791b7a9a 100644 --- a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java @@ -232,6 +232,26 @@ public void getAccountByIdShouldFailIfAccountNotFound(TestContext context) { })); } + @Test + public void getAccountByIdShouldTolerateAccountWithoutStatusDefined(TestContext context) throws SQLException { + // given + connection.createStatement() + .execute("insert into accounts_account (uuid, status) values ('1002', NULL);"); + + // when + final Future future = jdbcApplicationSettings.getAccountById("1002", timeout); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(account -> { + assertThat(account).isEqualTo(Account.builder() + .id("1002") + .status(null) + .build()); + async.complete(); + })); + } + @Test public void getAdUnitConfigByIdShouldReturnConfig(TestContext context) { // when From 9a86826a9b5cc4251cd85d544051d8b2736e28d3 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Mon, 18 Jan 2021 12:24:21 +0200 Subject: [PATCH 085/129] Update IX bidder to support all MediaTypes. (#1098) * Update IX bidder to support all MediaTypes. Refactored code. Url change is not needed. Left code that fixes empty bid.w and bid.h * Fix import * Change method names --- .../prebid/server/bidder/ix/IxAdapter.java | 12 +- .../org/prebid/server/bidder/ix/IxBidder.java | 171 ++++++++++-------- .../openrtb/ext/request/ix/ExtImpIx.java | 4 + src/main/resources/bidder-config/ix.yaml | 7 + .../resources/static/bidder-params/ix.json | 9 + .../server/bidder/ix/IxAdapterTest.java | 13 -- .../prebid/server/bidder/ix/IxBidderTest.java | 114 +++--------- .../openrtb2/ix/test-auction-ix-request.json | 4 +- .../it/openrtb2/ix/test-ix-bid-request-1.json | 1 - .../it/openrtb2/ix/test-ix-bid-request-2.json | 1 - 10 files changed, 148 insertions(+), 188 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/ix/IxAdapter.java b/src/main/java/org/prebid/server/bidder/ix/IxAdapter.java index fe7b84265fb..b8972f8aa15 100644 --- a/src/main/java/org/prebid/server/bidder/ix/IxAdapter.java +++ b/src/main/java/org/prebid/server/bidder/ix/IxAdapter.java @@ -55,8 +55,6 @@ public IxAdapter(String cookieFamilyName, String endpointUrl, JacksonMapper mapp @Override public List> makeHttpRequests(AdapterRequest adapterRequest, PreBidRequestContext preBidRequestContext) { - validatePreBidRequest(preBidRequestContext.getPreBidRequest()); - final List adUnitBids = adapterRequest.getAdUnitBids(); validateAdUnitBidsMediaTypes(adUnitBids, ALLOWED_MEDIA_TYPES); @@ -70,12 +68,6 @@ public List> makeHttpRequests(AdapterRequest adap .collect(Collectors.toList()); } - private static void validatePreBidRequest(PreBidRequest preBidRequest) { - if (preBidRequest.getApp() != null) { - throw new PreBidException("ix doesn't support apps"); - } - } - private List makeRequests(List adUnitBids, PreBidRequestContext preBidRequestContext) { final List prioritizedRequests = new ArrayList<>(); final List regularRequests = new ArrayList<>(); @@ -127,7 +119,9 @@ private IxParams parseAndValidateParams(AdUnitBid adUnitBid) { return params; } - private BidRequest createBidRequest(AdUnitBid adUnitBid, IxParams ixParams, Format size, + private BidRequest createBidRequest(AdUnitBid adUnitBid, + IxParams ixParams, + Format size, PreBidRequestContext preBidRequestContext) { final Imp imp = makeImp(copyAdUnitBidWithSingleSize(adUnitBid, size), preBidRequestContext); diff --git a/src/main/java/org/prebid/server/bidder/ix/IxBidder.java b/src/main/java/org/prebid/server/bidder/ix/IxBidder.java index f06d50d5f7c..aa222cc82d9 100644 --- a/src/main/java/org/prebid/server/bidder/ix/IxBidder.java +++ b/src/main/java/org/prebid/server/bidder/ix/IxBidder.java @@ -12,7 +12,6 @@ import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -57,28 +56,34 @@ public IxBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest bidRequest) { - if (bidRequest.getApp() != null) { - return Result.withError(BidderError.badInput("ix doesn't support apps")); - } final List errors = new ArrayList<>(); + + // First Banner.Format in every Imp have priority final List prioritizedRequests = new ArrayList<>(); - final List regularRequests = new ArrayList<>(); + final List multiSizeRequests = new ArrayList<>(); + for (Imp imp : bidRequest.getImp()) { - if (prioritizedRequests.size() == REQUEST_LIMIT) { - break; - } try { - validateImp(imp); - makeRequests(bidRequest, imp, prioritizedRequests, regularRequests); + final ExtImpIx extImpIx = parseImpExt(imp); + final List bidRequests = makeBidRequests(bidRequest, imp, extImpIx); + + prioritizedRequests.add(bidRequests.get(0)); + multiSizeRequests.addAll(bidRequests.subList(1, bidRequests.size())); + + if (prioritizedRequests.size() == REQUEST_LIMIT) { + break; + } } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } } - final List modifiedRequests = Stream.concat(prioritizedRequests.stream(), regularRequests.stream()) + final List modifiedRequests = Stream.concat( + prioritizedRequests.stream(), multiSizeRequests.stream()) .limit(REQUEST_LIMIT) .collect(Collectors.toList()); + if (modifiedRequests.isEmpty()) { errors.add(BidderError.badInput("No valid impressions in the bid request")); return Result.withErrors(errors); @@ -97,84 +102,85 @@ public Result>> makeHttpRequests(BidRequest bidRequ return Result.of(httpRequests, errors); } - private static void validateImp(Imp imp) { - if (imp.getBanner() == null) { - throw new PreBidException(String.format("Invalid MediaType. Ix supports only Banner type. " - + "Ignoring ImpID=%s", imp.getId())); + private ExtImpIx parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), IX_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage(), e); } } - private void makeRequests(BidRequest bidRequest, Imp imp, List prioritizedRequests, - List regularRequests) { - List formats = imp.getBanner().getFormat(); - if (CollectionUtils.isEmpty(formats)) { - formats = makeFormatFromBannerWidthAndHeight(imp); - } - final ExtImpIx extImpIx = parseAndValidateImpExt(imp); - final List modifiedBidRequests = createBidRequest(bidRequest, imp, formats, extImpIx); - if (!modifiedBidRequests.isEmpty()) { - if (modifiedBidRequests.size() == 1) { - prioritizedRequests.addAll(modifiedBidRequests); - } else { - prioritizedRequests.add(modifiedBidRequests.get(0)); - regularRequests.addAll(modifiedBidRequests.subList(1, modifiedBidRequests.size())); - } + private static List makeBidRequests(BidRequest bidRequest, + Imp imp, + ExtImpIx extImpIx) { + final ArrayList modifiedBidRequests = new ArrayList<>(); + final Site modifiedSite = modifySite(bidRequest.getSite(), extImpIx); + + final List modifiedImps = modifyImps(imp); + for (Imp modifiedImp : modifiedImps) { + final BidRequest modifiedBidRequest = bidRequest.toBuilder() + .site(modifiedSite) + .imp(Collections.singletonList(modifiedImp)) + .build(); + modifiedBidRequests.add(modifiedBidRequest); } + + return modifiedBidRequests; } - private static List makeFormatFromBannerWidthAndHeight(Imp imp) { - final Banner banner = imp.getBanner(); - return Collections.singletonList( - Format.builder().w(banner.getW()).h(banner.getH()).build()); + private static Site modifySite(Site site, ExtImpIx extImpIx) { + return site == null + ? null + : site.toBuilder() + .publisher(modifyPublisher(site.getPublisher(), extImpIx.getSiteId())) + .build(); } - private ExtImpIx parseAndValidateImpExt(Imp imp) { - final ExtImpIx extImpIx; - try { - extImpIx = mapper.mapper().convertValue(imp.getExt(), - IX_EXT_TYPE_REFERENCE).getBidder(); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); - } + private static Publisher modifyPublisher(Publisher publisher, String siteId) { + return publisher == null + ? Publisher.builder().id(siteId).build() + : publisher.toBuilder().id(siteId).build(); + } - if (StringUtils.isBlank(extImpIx.getSiteId())) { - throw new PreBidException("Missing siteId param"); + private static List modifyImps(Imp imp) { + final Banner impBanner = imp.getBanner(); + if (impBanner == null) { + return Collections.singletonList(imp); } - return extImpIx; - } - private static List createBidRequest(BidRequest bidRequest, Imp imp, - List formats, ExtImpIx extImpIx) { - final List limitedFormats = formats.size() > REQUEST_LIMIT - ? formats.subList(0, REQUEST_LIMIT) - : formats; + return modifyBanners(impBanner).stream() + .map(banner -> imp.toBuilder().banner(banner).build()) + .collect(Collectors.toList()); + } - final List requests = new ArrayList<>(); - for (Format format : limitedFormats) { - final Banner updatedBanner = imp.getBanner().toBuilder() + private static List modifyBanners(Banner banner) { + final ArrayList modifiedBanners = new ArrayList<>(); + final List formats = getFormats(banner); + for (Format format : formats) { + final Banner modifiedBanner = banner.toBuilder() .format(Collections.singletonList(format)) .w(format.getW()) .h(format.getH()) .build(); - - final Imp updatedImp = imp.toBuilder() - .banner(updatedBanner) - .tagid(imp.getId()) - .build(); - - requests.add(bidRequest.toBuilder() - .site(modifySite(bidRequest, extImpIx)) - .imp(Collections.singletonList(updatedImp)) - .build()); + modifiedBanners.add(modifiedBanner); } - return requests; + + return modifiedBanners; } - private static Site modifySite(BidRequest bidRequest, ExtImpIx extImpIx) { - final Site site = bidRequest.getSite(); - final Site.SiteBuilder siteBuilder = site == null ? Site.builder() : site.toBuilder(); - return siteBuilder.publisher(Publisher.builder() - .id(extImpIx.getSiteId()).build()).build(); + // Cant be empty because of request validation + private static List getFormats(Banner banner) { + final Integer bannerW = banner.getW(); + final Integer bannerH = banner.getH(); + final List bannerFormats = banner.getFormat(); + if (CollectionUtils.isEmpty(bannerFormats) && bannerW != null && bannerH != null) { + final Format format = Format.builder() + .w(bannerW) + .h(bannerH) + .build(); + return Collections.singletonList(format); + } + return bannerFormats; } @Override @@ -199,14 +205,31 @@ private static List bidsFromResponse(BidResponse bidResponse, BidRequ .map(SeatBid::getBid) .flatMap(Collection::stream) .map(bid -> prepareBid(bid, bidRequest)) - .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); } + private static BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } else if (imp.getAudio() != null) { + return BidType.audio; + } + } + } + throw new PreBidException(String.format("Unmatched impression id %s", impId)); + } + private static Bid prepareBid(Bid bid, BidRequest bidRequest) { - if (bid.getH() == null || bid.getW() == null) { - // Current implementation ensure that we have one imp with banner - final Banner banner = bidRequest.getImp().get(0).getBanner(); + // Current implementation ensure that we have one imp + final Banner banner = bidRequest.getImp().get(0).getBanner(); + if (bid.getH() == null || bid.getW() == null && banner != null) { return bid.toBuilder() .w(banner.getW()) .h(banner.getH()) diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java index 490dbd2bea9..e7a8faee424 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java @@ -4,10 +4,14 @@ import lombok.AllArgsConstructor; import lombok.Value; +import java.util.List; + @AllArgsConstructor(staticName = "of") @Value public class ExtImpIx { @JsonProperty("siteId") String siteId; + + List size; } diff --git a/src/main/resources/bidder-config/ix.yaml b/src/main/resources/bidder-config/ix.yaml index 6bf899e5284..2c8d2a458e8 100644 --- a/src/main/resources/bidder-config/ix.yaml +++ b/src/main/resources/bidder-config/ix.yaml @@ -10,8 +10,15 @@ adapters: meta-info: maintainer-email: pdu-supply-prebid@indexexchange.com app-media-types: + - banner + - video + - native + - audio site-media-types: - banner + - video + - native + - audio supported-vendors: vendor-id: 10 usersync: diff --git a/src/main/resources/static/bidder-params/ix.json b/src/main/resources/static/bidder-params/ix.json index 7ed56deef46..da22db69803 100644 --- a/src/main/resources/static/bidder-params/ix.json +++ b/src/main/resources/static/bidder-params/ix.json @@ -7,6 +7,15 @@ "siteId": { "type": "string", "description": "An ID which identifies the site selling the impression" + }, + "size": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 2, + "maxItems": 2, + "description": "An array of two integer containing the dimension" } }, "required": [ diff --git a/src/test/java/org/prebid/server/bidder/ix/IxAdapterTest.java b/src/test/java/org/prebid/server/bidder/ix/IxAdapterTest.java index 4832fe493c2..ad5c3b4175e 100644 --- a/src/test/java/org/prebid/server/bidder/ix/IxAdapterTest.java +++ b/src/test/java/org/prebid/server/bidder/ix/IxAdapterTest.java @@ -1,6 +1,5 @@ package org.prebid.server.bidder.ix; -import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.BidRequest.BidRequestBuilder; @@ -108,18 +107,6 @@ public void makeHttpRequestsShouldReturnRequestsWithExpectedHeaders() { tuple("Accept", "application/json")); } - @Test - public void makeHttpRequestsShouldFailIfAppIsPresentInPreBidRequest() { - // given - preBidRequestContext = givenPreBidRequestContext(identity(), builder -> builder - .app(App.builder().build())); - - // when and then - assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext)) - .isExactlyInstanceOf(PreBidException.class) - .hasMessage("ix doesn't support apps"); - } - @Test public void makeHttpRequestsShouldFailIfParamsMissingInAtLeastOneAdUnitBid() { // given diff --git a/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java b/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java index b269524b212..0fc5e634268 100644 --- a/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java @@ -1,7 +1,6 @@ package org.prebid.server.bidder.ix; import com.fasterxml.jackson.core.JsonProcessingException; -import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Format; @@ -34,12 +33,12 @@ import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; public class IxBidderTest extends VertxTest { private static final String ENDPOINT_URL = "http://exchange.org/"; private static final int REQUEST_LIMIT = 20; + private static final String SITE_ID = "site id"; private IxBidder ixBidder; @@ -53,39 +52,6 @@ public void creationShouldFailOnInvalidEndpointUrl() { assertThatIllegalArgumentException().isThrownBy(() -> new IxBidder("invalid_url", jacksonMapper)); } - @Test - public void makeHttpRequestsShouldReturnErrorIfRequestHasApp() { - // given - final BidRequest bidRequest = BidRequest.builder() - .app(App.builder().build()) - .build(); - - // when - final Result>> result = ixBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("ix doesn't support apps")); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void makeHttpRequestsShouldReturnErrorIfImpHasNoBanner() { - // given - final BidRequest bidRequest = givenBidRequest( - impBuilder -> impBuilder.banner(null).video(Video.builder().build())); - - // when - final Result>> result = ixBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(2) - .containsOnly( - BidderError.badInput("Invalid MediaType. Ix supports only Banner type. Ignoring ImpID=123"), - BidderError.badInput("No valid impressions in the bid request")); - assertThat(result.getValue()).isEmpty(); - } - @Test public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // given @@ -103,30 +69,25 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { } @Test - public void makeHttpRequestsShouldReturnErrorIfImpExtSiteIdIsNullOrBlank() { + public void makeHttpRequestsShouldReturnWhenImpHasNoBanner() { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(asList( - givenImp(impBuilder -> impBuilder.ext( - mapper.valueToTree(ExtPrebid.of(null, ExtImpIx.of(null))))), - givenImp(impBuilder -> impBuilder.ext( - mapper.valueToTree(ExtPrebid.of(null, ExtImpIx.of(""))))))) - .build(); + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.banner(null).video(Video.builder().build())); // when final Result>> result = ixBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(3) - .containsOnly(BidderError.badInput("Missing siteId param"), - BidderError.badInput("No valid impressions in the bid request")); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); } @Test - public void makeHttpRequestsShouldSetSitePublisherIdFromImpExt() { + public void makeHttpRequestsShouldSetSitePublisherIdFromImpExtWhenSitePresent() { // given - final BidRequest bidRequest = givenBidRequest(identity()); + final Banner banner = Banner.builder().w(100).h(200).build(); + final BidRequest bidRequest = givenBidRequest( + builder -> builder.site(Site.builder().build()), + impBuilder -> impBuilder.banner(banner)); // when final Result>> result = ixBidder.makeHttpRequests(bidRequest); @@ -138,28 +99,7 @@ public void makeHttpRequestsShouldSetSitePublisherIdFromImpExt() { .extracting(BidRequest::getSite) .extracting(Site::getPublisher) .extracting(Publisher::getId) - .containsOnly("site id"); - } - - @Test - public void makeHttpRequestsShouldSetImpTagIdFromImpId() { - // given - final BidRequest bidRequest = givenBidRequest( - impBuilder -> impBuilder.banner(Banner.builder() - .id("123") - .format(singletonList(Format.builder().w(300).h(200).build())) - .build())); - - // when - final Result>> result = ixBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getTagid) - .containsOnly("123"); + .containsOnly(SITE_ID); } @Test @@ -213,6 +153,7 @@ public void makeHttpRequestsShouldCreateOneRequestPerImp() { final BidRequest bidRequest = BidRequest.builder() .imp(asList( givenImp(impBuilder -> impBuilder + .id("123") .banner(Banner.builder() .format(singletonList(Format.builder().w(300).h(200).build())).build())), givenImp(impBuilder -> impBuilder @@ -238,7 +179,8 @@ public void makeHttpRequestsShouldCreateOneRequestPerBannerFormat() { // given final BidRequest bidRequest = givenBidRequest( impBuilder -> impBuilder.banner(Banner.builder() - .format(asList(Format.builder().w(300).h(200).build(), + .format(asList( + Format.builder().w(300).h(200).build(), Format.builder().w(600).h(400).build())) .build())); @@ -249,11 +191,8 @@ public void makeHttpRequestsShouldCreateOneRequestPerBannerFormat() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(2) .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .extracting(BidRequest::getSite) - .extracting(Site::getPublisher) - .extracting(Publisher::getId) - // both from same imp (same imp.ext.siteId) - .containsOnly("site id"); + .flatExtracting(BidRequest::getImp) + .hasSize(2); } @Test @@ -285,8 +224,7 @@ public void makeHttpRequestsShouldLimitImpsAmount() { imps.add(givenImp(impBuilder -> impBuilder.banner(Banner.builder() .format(singletonList(Format.builder().w(value).h(value).build())).build()))); } - final BidRequest bidRequest = givenBidRequest(requestBuilder -> requestBuilder.imp(imps), - identity()); + final BidRequest bidRequest = givenBidRequest(requestBuilder -> requestBuilder.imp(imps), identity()); // when final Result>> result = ixBidder.makeHttpRequests(bidRequest); @@ -303,7 +241,8 @@ public void makeHttpRequestsShouldLimitTotalAmountOfRequests() { for (int i = 0; i < 21; i++) { int value = i; imps.add(givenImp(impBuilder -> impBuilder.banner(Banner.builder() - .format(asList(Format.builder().w(value).h(value).build(), + .format(asList( + Format.builder().w(value).h(value).build(), Format.builder().w(value + 1).h(value).build())) .build()))); } @@ -322,16 +261,16 @@ public void makeHttpRequestsShouldLimitTotalAmountOfRequests() { public void makeHttpRequestsShouldPrioritizeFirstFormatPerImpOverOtherFormats() { // given final List imps = new ArrayList<>(); - for (int i = 0; i < 21; i++) { + for (int i = 0; i <= REQUEST_LIMIT; i++) { int priority = i; int other = i + 25; imps.add(givenImp(impBuilder -> impBuilder.banner(Banner.builder() - .format(asList(Format.builder().w(priority).h(priority).build(), + .format(asList( + Format.builder().w(priority).h(priority).build(), Format.builder().w(other).h(other).build())) .build()))); } - final BidRequest bidRequest = givenBidRequest(requestBuilder -> requestBuilder.imp(imps), - identity()); + final BidRequest bidRequest = givenBidRequest(requestBuilder -> requestBuilder.imp(imps), identity()); // when final Result>> result = ixBidder.makeHttpRequests(bidRequest); @@ -398,9 +337,10 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso @Test public void makeBidsShouldReturnBannerBidWithOriginalBidSize() throws JsonProcessingException { // given + final Video video = Video.builder().build(); final HttpCall httpCall = givenHttpCall( BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").build())) + .imp(singletonList(Imp.builder().id("123").video(video).build())) .build(), mapper.writeValueAsString( givenBidResponse(bidBuilder -> bidBuilder.impid("123").w(300).h(200)))); @@ -411,7 +351,7 @@ public void makeBidsShouldReturnBannerBidWithOriginalBidSize() throws JsonProces // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").w(300).h(200).build(), banner, "EUR")); + .containsOnly(BidderBid.of(Bid.builder().impid("123").w(300).h(200).build(), BidType.video, "EUR")); } @Test @@ -450,7 +390,7 @@ private static Imp givenImp(Function impCustomiz return impCustomizer.apply(Imp.builder() .id("123") .banner(Banner.builder().build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpIx.of("site id"))))) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpIx.of(SITE_ID, null))))) .build(); } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json index 649ff43cfc1..6166d299b78 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json @@ -13,9 +13,7 @@ "w": 600, "h": 480 } - ], - "w": 300, - "h": 250 + ] }, "ext": { "ix": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-1.json index 01cfbd3b728..ca28223533b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-1.json @@ -13,7 +13,6 @@ "w": 300, "h": 250 }, - "tagid": "impId6", "ext": { "bidder": { "siteId": "10002" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-2.json index f75ca1d4437..47cfe63d4bb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request-2.json @@ -13,7 +13,6 @@ "w": 600, "h": 480 }, - "tagid": "impId6", "ext": { "bidder": { "siteId": "10002" From d5ea0dee0a74835b6468e52563a9176cdf42a62f Mon Sep 17 00:00:00 2001 From: Braslavskyi Andrii Date: Mon, 18 Jan 2021 16:31:12 +0200 Subject: [PATCH 086/129] Update PBS to generate bid ids for cache and events (#1100) * Update PBS to generate bid ids for cache and events * Fix checkstyle error * fix pull request comments --- docs/config-app.md | 2 +- .../server/auction/AuctionRequestFactory.java | 3 - .../server/auction/BidResponseCreator.java | 79 +++++--- .../server/auction/model/GeneratedBidIds.java | 81 ++++++++ .../org/prebid/server/cache/CacheService.java | 32 ++- .../server/cache/model/CacheContext.java | 8 +- .../prebid/server/identity/IdGenerator.java | 2 + .../server/identity/NoneIdGenerator.java | 5 + .../server/identity/UUIDIdGenerator.java | 5 + .../spring/config/ServiceConfiguration.java | 20 +- src/main/resources/application.yaml | 2 +- .../auction/BidResponseCreatorTest.java | 167 ++++++++++++---- .../auction/model/GeneratedBidIdsTest.java | 157 +++++++++++++++ .../prebid/server/cache/CacheServiceTest.java | 183 ++++++++++++++---- .../server/it/test-application.properties | 2 +- 15 files changed, 608 insertions(+), 140 deletions(-) create mode 100644 src/main/java/org/prebid/server/auction/model/GeneratedBidIds.java create mode 100644 src/test/java/org/prebid/server/auction/model/GeneratedBidIdsTest.java diff --git a/docs/config-app.md b/docs/config-app.md index 8557a6a3d67..229d8bba283 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -78,7 +78,7 @@ Removes and downloads file again if depending service cant process probably corr - `auction.cache.expected-request-time-ms` - approximate value in milliseconds for Cache Service interacting. This time will be subtracted from global timeout. - `auction.cache.only-winning-bids` - if equals to `true` only the winning bids would be cached. Has lower priority than request-specific flags. - `auction.generate-bid-id` - whether to generate seatbid[].bid[].ext.prebid.bidid in the OpenRTB response. -- `auction.id-generator-type` - if generate-bid-id is on, then this defines how the ID should be generated. Currently onlye `uuid` is supported. +- `auction.generate-source-tid` - whether to generate bidrequest.source.tid in the OpenRTB request. - `auction.validations.banner-creative-max-size` - enables creative max size validation for banners. Possible values: `skip`, `enforce`, `warn`. Default is `skip`. - `auction.validations.secure-markup` - enables secure markup validation. Possible values: `skip`, `enforce`, `warn`. Default is `skip`. diff --git a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java index 7160ae670ee..f3826db6735 100644 --- a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java @@ -11,7 +11,6 @@ import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Source; -import com.iab.openrtb.request.User; import io.netty.buffer.ByteBufInputStream; import io.vertx.core.Future; import io.vertx.core.buffer.Buffer; @@ -276,8 +275,6 @@ BidRequest fillImplicitParameters(BidRequest bidRequest, RoutingContext context, final Site site = bidRequest.getSite(); final Site populatedSite = bidRequest.getApp() != null ? null : populateSite(site, request); - final User user = bidRequest.getUser(); - final Source source = bidRequest.getSource(); final Source populatedSource = populateSource(source); diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index ff7e0c0fa35..4ed980afb56 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -26,6 +26,7 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidRequestCacheInfo; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.auction.model.GeneratedBidIds; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -40,6 +41,8 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; +import org.prebid.server.identity.IdGenerator; +import org.prebid.server.identity.IdGeneratorType; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; @@ -81,7 +84,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -100,7 +102,7 @@ public class BidResponseCreator { private final BidderCatalog bidderCatalog; private final EventsService eventsService; private final StoredRequestProcessor storedRequestProcessor; - private final boolean generateBidId; + private final IdGenerator idGenerator; private final int truncateAttrChars; private final Clock clock; private final JacksonMapper mapper; @@ -113,7 +115,7 @@ public BidResponseCreator(CacheService cacheService, BidderCatalog bidderCatalog, EventsService eventsService, StoredRequestProcessor storedRequestProcessor, - boolean generateBidId, + IdGenerator idGenerator, int truncateAttrChars, Clock clock, JacksonMapper mapper) { @@ -122,7 +124,7 @@ public BidResponseCreator(CacheService cacheService, this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.eventsService = Objects.requireNonNull(eventsService); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); - this.generateBidId = generateBidId; + this.idGenerator = Objects.requireNonNull(idGenerator); this.truncateAttrChars = validateTruncateAttrChars(truncateAttrChars); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); @@ -200,6 +202,11 @@ private Future cacheBidsAndCreateResponse(List bidd final Set winningBids = newOrEmptySet(targeting); final Set winningBidsByBidder = newOrEmptySet(targeting); + final GeneratedBidIds generatedBidIds = GeneratedBidIds.of(bidderResponses, + (ignored, bid) -> idGenerator.getType() != IdGeneratorType.none + ? idGenerator.generateId() + : bid.getId()); + // determine winning bids only if targeting is present if (targeting != null) { populateWinningBids(bidderResponses, winningBids, winningBidsByBidder); @@ -219,6 +226,7 @@ private Future cacheBidsAndCreateResponse(List bidd return toBidsWithCacheIds( bidderResponses, bidsToCache, + generatedBidIds, auctionContext, cacheInfo, eventsContext) @@ -231,6 +239,7 @@ private Future cacheBidsAndCreateResponse(List bidd winningBidsByBidder, cacheInfo, cacheResult, + generatedBidIds, videoStoredDataResult, eventsContext, auctionTimestamp, @@ -399,6 +408,7 @@ private static Stream getBids(BidderResponse bidderResponse) { */ private Future toBidsWithCacheIds(List bidderResponses, Set bidsToCache, + GeneratedBidIds generatedBidIds, AuctionContext auctionContext, BidRequestCacheInfo cacheInfo, EventsContext eventsContext) { @@ -414,22 +424,19 @@ private Future toBidsWithCacheIds(List bidde final boolean shouldCacheVideoBids = cacheInfo.isShouldCacheVideoBids(); - final Map> bidderToVideoBidIdsToModify = + final GeneratedBidIds bidderToVideoGeneratedBidIdsToModify = shouldCacheVideoBids && eventsEnabledForAccount(auctionContext) - ? getBidderAndVideoBidIdsToModify(bidderResponses, auctionContext.getBidRequest().getImp()) - : Collections.emptyMap(); - final Map> bidderToBidIds = bidderResponses.stream() - .collect(Collectors.toMap(BidderResponse::getBidder, bidderResponse -> getBids(bidderResponse) - .map(Bid::getId) - .collect(Collectors.toList()))); + ? getGeneratedVideoBidIds(bidderResponses, generatedBidIds, + auctionContext.getBidRequest().getImp()) + : GeneratedBidIds.empty(); final CacheContext cacheContext = CacheContext.builder() .cacheBidsTtl(cacheInfo.getCacheBidsTtl()) .cacheVideoBidsTtl(cacheInfo.getCacheVideoBidsTtl()) .shouldCacheBids(cacheInfo.isShouldCacheBids()) .shouldCacheVideoBids(shouldCacheVideoBids) - .bidderToVideoBidIdsToModify(bidderToVideoBidIdsToModify) - .bidderToBidIds(bidderToBidIds) + .bidderToVideoGeneratedBidIdsToModify(bidderToVideoGeneratedBidIdsToModify) + .bidderToBidsToGeneratedIds(generatedBidIds) .build(); return cacheService.cacheBidsOpenrtb(bidsValidToBeCached, auctionContext, cacheContext, eventsContext) @@ -441,22 +448,38 @@ private static boolean isValidForCaching(Bid bid) { return bid.getDealid() != null ? price.compareTo(BigDecimal.ZERO) >= 0 : price.compareTo(BigDecimal.ZERO) > 0; } - private Map> getBidderAndVideoBidIdsToModify(List bidderResponses, - List imps) { + private GeneratedBidIds getGeneratedVideoBidIds( + List bidderResponses, + GeneratedBidIds generatedBidIds, + List imps) { - return bidderResponses.stream() + final List vastModifyAllowedResponses = bidderResponses.stream() .filter(bidderResponse -> bidderCatalog.isModifyingVastXmlAllowed(bidderResponse.getBidder())) - .collect(Collectors.toMap(BidderResponse::getBidder, bidderResponse -> getBids(bidderResponse) - .filter(bid -> isVideoBid(bid, imps)) - .map(Bid::getId) - .collect(Collectors.toList()))); + .map(bidderResponse -> makeVideoBidsBidderResponse(bidderResponse, imps)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return GeneratedBidIds.of(vastModifyAllowedResponses, + (bidder, bid) -> generatedBidIds.getGeneratedId(bidder, bid.getId(), bid.getImpid())); + } + + private static BidderResponse makeVideoBidsBidderResponse(BidderResponse bidderResponse, List imps) { + final List videoBidderBids = bidderResponse.getSeatBid().getBids().stream() + .filter(bidderBid -> isVideoBid(bidderBid, imps)) + .collect(Collectors.toList()); + final BidderSeatBid bidderSeatBid = bidderResponse.getSeatBid(); + return CollectionUtils.isNotEmpty(videoBidderBids) + ? BidderResponse.of( + bidderResponse.getBidder(), + BidderSeatBid.of(videoBidderBids, bidderSeatBid.getHttpCalls(), bidderSeatBid.getErrors()), + bidderResponse.getResponseTime()) + : null; } - private static boolean isVideoBid(Bid bid, List imps) { + private static boolean isVideoBid(BidderBid bidderBid, List imps) { return imps.stream() .filter(imp -> imp.getVideo() != null) .map(Imp::getId) - .anyMatch(impId -> bid.getImpid().equals(impId)); + .anyMatch(impId -> bidderBid.getBid().getImpid().equals(impId)); } /** @@ -661,6 +684,7 @@ private BidResponse toBidResponse(List bidderResponses, Set winningBidsByBidder, BidRequestCacheInfo requestCacheInfo, CacheServiceResult cacheResult, + GeneratedBidIds generatedBidIds, VideoStoredDataResult videoStoredDataResult, EventsContext eventsContext, long auctionTimestamp, @@ -680,6 +704,7 @@ private BidResponse toBidResponse(List bidderResponses, winningBidsByBidder, requestCacheInfo, cacheResult.getCacheBids(), + generatedBidIds, videoStoredDataResult, account, bidErrors, @@ -756,6 +781,7 @@ private SeatBid toSeatBid(BidderResponse bidderResponse, Set winningBidsByBidder, BidRequestCacheInfo requestCacheInfo, Map bidToCacheInfo, + GeneratedBidIds generatedBidIds, VideoStoredDataResult videoStoredDataResult, Account account, Map> bidErrors, @@ -773,6 +799,7 @@ private SeatBid toSeatBid(BidderResponse bidderResponse, winningBidsByBidder, requestCacheInfo, bidToCacheInfo, + generatedBidIds, videoStoredDataResult.getImpIdToStoredVideo(), account, eventsContext, @@ -798,6 +825,7 @@ private Bid toBid(BidderBid bidderBid, Set winningBidsByBidder, BidRequestCacheInfo requestCacheInfo, Map bidsWithCacheIds, + GeneratedBidIds generatedBidIds, Map impIdToStoredVideo, Account account, EventsContext eventsContext, @@ -846,14 +874,13 @@ private Bid toBid(BidderBid bidderBid, final CacheAsset vastXml = videoCacheId != null ? toCacheAsset(videoCacheId) : null; final ExtResponseCache cache = bids != null || vastXml != null ? ExtResponseCache.of(bids, vastXml) : null; - final String generatedBidId = generateBidId ? UUID.randomUUID().toString() : null; - final String eventBidId = ObjectUtils.defaultIfNull(generatedBidId, bid.getId()); + final String generatedBidId = generatedBidIds.getGeneratedId(bidder, bid.getId(), bid.getImpid()); final Video storedVideo = impIdToStoredVideo.get(bid.getImpid()); - final Events events = createEvents(bidder, account, eventBidId, eventsContext); + final Events events = createEvents(bidder, account, generatedBidId, eventsContext); final ExtBidPrebidVideo extBidPrebidVideo = getExtBidPrebidVideo(bid.getExt()); final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder() - .bidid(generatedBidId) + .bidid(idGenerator.getType() != IdGeneratorType.none ? generatedBidId : null) .type(bidType) .targeting(targetingKeywords) .cache(cache) diff --git a/src/main/java/org/prebid/server/auction/model/GeneratedBidIds.java b/src/main/java/org/prebid/server/auction/model/GeneratedBidIds.java new file mode 100644 index 00000000000..4eb6e2f6603 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/GeneratedBidIds.java @@ -0,0 +1,81 @@ +package org.prebid.server.auction.model; + +import com.iab.openrtb.response.Bid; +import lombok.AllArgsConstructor; +import org.prebid.server.bidder.model.BidderBid; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + + +/** + * Class creates and holds generated bid ids per bidder per imp. + */ +@AllArgsConstructor +public class GeneratedBidIds { + + private static final String BID_IMP_ID_PATTERN = "%s-%s"; + + private final Map> bidderToBidIds; + + /** + * Creates an object of {@link GeneratedBidIds} with {@link Map} where key is a bidder and value is another map, + * that have pair of bidId-impId to uniquely identify the bid withing the bidder and as value has bid Id generated + * by idGenerationStrategy parameter. + */ + public static GeneratedBidIds of(List bidderResponses, + BiFunction idGenerationStrategy) { + final Map> generatedBidIds = bidderResponses.stream() + .collect(Collectors.toMap(BidderResponse::getBidder, + bidderResponse -> bidderResponse.getSeatBid() + .getBids().stream() + .map(BidderBid::getBid) + .collect(Collectors.toMap(bid -> makeUniqueBidId(bid.getId(), bid.getImpid()), + bid -> idGenerationStrategy.apply(bidderResponse.getBidder(), bid))))); + return new GeneratedBidIds(generatedBidIds); + } + + /** + * Returns empty {@link GeneratedBidIds}. + */ + public static GeneratedBidIds empty() { + return new GeneratedBidIds(Collections.emptyMap()); + } + + /** + * Creates unique identifier for bid that consist from pair bidId-impId. + */ + private static String makeUniqueBidId(String bidId, String impId) { + return String.format(BID_IMP_ID_PATTERN, bidId, impId); + } + + /** + * Returns bidder for bidId and impId parameters. + */ + public Optional getBidderForBid(String bidId, String impId) { + final String uniqueBidId = makeUniqueBidId(bidId, impId); + return bidderToBidIds.entrySet().stream() + .filter(bidIdToGeneratedId -> bidIdToGeneratedId.getValue().containsKey(uniqueBidId)) + .findFirst() + .map(Map.Entry::getKey); + } + + /** + * Returns generated id for bid identified by bidder, bidId and impId parameters. + */ + public String getGeneratedId(String bidder, String bidId, String impId) { + if (bidderToBidIds.containsKey(bidder)) { + return bidderToBidIds.get(bidder).get(makeUniqueBidId(bidId, impId)); + } else { + return null; + } + } + + public Map> getBidderToBidIds() { + return bidderToBidIds; + } +} diff --git a/src/main/java/org/prebid/server/cache/CacheService.java b/src/main/java/org/prebid/server/cache/CacheService.java index f28903dd638..23d3c68e556 100644 --- a/src/main/java/org/prebid/server/cache/CacheService.java +++ b/src/main/java/org/prebid/server/cache/CacheService.java @@ -11,6 +11,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.GeneratedBidIds; import org.prebid.server.cache.model.CacheBid; import org.prebid.server.cache.model.CacheContext; import org.prebid.server.cache.model.CacheHttpRequest; @@ -48,7 +49,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.function.Function; @@ -274,8 +274,8 @@ public Future cacheBidsOpenrtb(List doCacheOpenrtb(List bids, List videoBids, AuctionContext auctionContext, - Map> bidderToVideoBidIdsToModify, - Map> biddersToCacheBidIds, + GeneratedBidIds bidderToVideoBidIdsToModify, + GeneratedBidIds biddersToCacheBidIds, EventsContext eventsContext) { final Account account = auctionContext.getAccount(); @@ -488,7 +488,7 @@ private static CachedCreative createPutObjectVideoOnly(Bid bid) { * Used for OpenRTB auction request. Also, adds win url to result object if events are enabled. */ private CachedCreative createJsonPutObjectOpenrtb(CacheBid cacheBid, - Map> biddersToCacheBidIds, + GeneratedBidIds biddersToCacheBidIds, Account account, EventsContext eventsContext) { @@ -513,7 +513,7 @@ private CachedCreative createJsonPutObjectOpenrtb(CacheBid cacheBid, * Makes XML type {@link PutObject} from {@link com.iab.openrtb.response.Bid}. Used for OpenRTB auction request. */ private CachedCreative createXmlPutObjectOpenrtb(CacheBid cacheBid, - Map> bidderToVideoBidIdsToModify, + GeneratedBidIds bidderToVideoBidIdsToModify, Account account, EventsContext eventsContext) { @@ -544,16 +544,15 @@ private static String resolveVastXmlFrom(com.iab.openrtb.response.Bid bid) { return bid.getAdm(); } - private String generateWinUrl(Map> biddersToCacheBidIds, + private String generateWinUrl(GeneratedBidIds biddersToCacheBidIds, com.iab.openrtb.response.Bid bid, Account account, EventsContext eventsContext) { if (eventsContext.isEnabledForAccount() && eventsContext.isEnabledForRequest()) { - final String bidId = bid.getId(); - return findBidderForBidId(biddersToCacheBidIds, bidId) + return biddersToCacheBidIds.getBidderForBid(bid.getId(), bid.getImpid()) .map(bidder -> eventsService.winUrl( - bidId, + bid.getId(), bidder, account.getId(), eventsContext.getAuctionTimestamp(), @@ -564,14 +563,14 @@ private String generateWinUrl(Map> biddersToCacheBidIds, return null; } - private String generateVastUrlTracking(Map> bidderToVideoBidIdsToModify, + private String generateVastUrlTracking(GeneratedBidIds bidderToVideoBidIdsToModify, com.iab.openrtb.response.Bid bid, Account account, EventsContext eventsContext) { if (eventsContext.isEnabledForAccount()) { final String bidId = bid.getId(); - return findBidderForBidId(bidderToVideoBidIdsToModify, bidId) + return bidderToVideoBidIdsToModify.getBidderForBid(bid.getId(), bid.getImpid()) .map(bidder -> eventsService.vastUrlTracking( bidId, bidder, @@ -584,13 +583,6 @@ private String generateVastUrlTracking(Map> bidderToVideoBi return null; } - private static Optional findBidderForBidId(Map> biddersToCacheBidIds, String bidId) { - return biddersToCacheBidIds.entrySet().stream() - .filter(biddersAndBidIds -> biddersAndBidIds.getValue().contains(bidId)) - .findFirst() - .map(Map.Entry::getKey); - } - private String appendTrackingUrlToVastXml(String vastXml, String vastUrlTracking) { final String closeTag = ""; final int closeTagIndex = vastXml.indexOf(closeTag); diff --git a/src/main/java/org/prebid/server/cache/model/CacheContext.java b/src/main/java/org/prebid/server/cache/model/CacheContext.java index f26c975c212..7c418808387 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheContext.java +++ b/src/main/java/org/prebid/server/cache/model/CacheContext.java @@ -2,9 +2,7 @@ import lombok.Builder; import lombok.Value; - -import java.util.List; -import java.util.Map; +import org.prebid.server.auction.model.GeneratedBidIds; /** * Holds the state needed to perform caching response bids. @@ -21,7 +19,7 @@ public class CacheContext { Integer cacheVideoBidsTtl; - Map> bidderToVideoBidIdsToModify; + GeneratedBidIds bidderToVideoGeneratedBidIdsToModify; - Map> bidderToBidIds; + GeneratedBidIds bidderToBidsToGeneratedIds; } diff --git a/src/main/java/org/prebid/server/identity/IdGenerator.java b/src/main/java/org/prebid/server/identity/IdGenerator.java index 4b75fe47ebc..ea34457d98c 100644 --- a/src/main/java/org/prebid/server/identity/IdGenerator.java +++ b/src/main/java/org/prebid/server/identity/IdGenerator.java @@ -6,4 +6,6 @@ public interface IdGenerator { String generateId(); + + IdGeneratorType getType(); } diff --git a/src/main/java/org/prebid/server/identity/NoneIdGenerator.java b/src/main/java/org/prebid/server/identity/NoneIdGenerator.java index 5e458bf1875..14aaba41886 100644 --- a/src/main/java/org/prebid/server/identity/NoneIdGenerator.java +++ b/src/main/java/org/prebid/server/identity/NoneIdGenerator.java @@ -9,4 +9,9 @@ public class NoneIdGenerator implements IdGenerator { public String generateId() { return null; } + + @Override + public IdGeneratorType getType() { + return IdGeneratorType.none; + } } diff --git a/src/main/java/org/prebid/server/identity/UUIDIdGenerator.java b/src/main/java/org/prebid/server/identity/UUIDIdGenerator.java index 927c0255cc5..7f6e95ef371 100644 --- a/src/main/java/org/prebid/server/identity/UUIDIdGenerator.java +++ b/src/main/java/org/prebid/server/identity/UUIDIdGenerator.java @@ -11,4 +11,9 @@ public class UUIDIdGenerator implements IdGenerator { public String generateId() { return UUID.randomUUID().toString(); } + + @Override + public IdGeneratorType getType() { + return IdGeneratorType.uuid; + } } diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index ba7957c8d80..6e1082bd27c 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -38,7 +38,6 @@ import org.prebid.server.events.EventsService; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.identity.IdGenerator; -import org.prebid.server.identity.IdGeneratorType; import org.prebid.server.identity.NoneIdGenerator; import org.prebid.server.identity.UUIDIdGenerator; import org.prebid.server.json.JacksonMapper; @@ -201,7 +200,7 @@ AuctionRequestFactory auctionRequestFactory( TimeoutFactory timeoutFactory, ApplicationSettings applicationSettings, PrivacyEnforcementService privacyEnforcementService, - IdGenerator idGenerator, + IdGenerator sourceIdGenerator, JacksonMapper mapper) { final List blacklistedApps = splitToList(blacklistedAppsString); @@ -225,14 +224,21 @@ AuctionRequestFactory auctionRequestFactory( timeoutResolver, timeoutFactory, applicationSettings, - idGenerator, + sourceIdGenerator, privacyEnforcementService, mapper); } @Bean - IdGenerator idGenerator(@Value("${auction.id-generator-type}") IdGeneratorType idGeneratorType) { - return idGeneratorType == IdGeneratorType.uuid + IdGenerator bidIdGenerator(@Value("${auction.generate-bid-id}") boolean generateBidId) { + return generateBidId + ? new UUIDIdGenerator() + : new NoneIdGenerator(); + } + + @Bean + IdGenerator sourceIdGenerator(@Value("${auction.generate-source-tid}") boolean generateSourceTid) { + return generateSourceTid ? new UUIDIdGenerator() : new NoneIdGenerator(); } @@ -454,7 +460,7 @@ BidResponseCreator bidResponseCreator( BidderCatalog bidderCatalog, EventsService eventsService, StoredRequestProcessor storedRequestProcessor, - @Value("${auction.generate-bid-id}") boolean generateBidId, + IdGenerator bidIdGenerator, @Value("${settings.targeting.truncate-attr-chars}") int truncateAttrChars, Clock clock, JacksonMapper mapper) { @@ -464,7 +470,7 @@ BidResponseCreator bidResponseCreator( bidderCatalog, eventsService, storedRequestProcessor, - generateBidId, + bidIdGenerator, truncateAttrChars, clock, mapper); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index bd7c71ccbd9..233ec848976 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -82,7 +82,7 @@ auction: log-failure-only: false log-sampling-rate: 0.0 max-request-size: 262144 - id-generator-type: uuid + generate-source-tid: true generate-bid-id: false cache: expected-request-time-ms: 10 diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index f9f536ca465..6a47dd327b1 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -41,6 +41,8 @@ import org.prebid.server.events.EventsService; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.identity.IdGenerator; +import org.prebid.server.identity.IdGeneratorType; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; import org.prebid.server.proto.openrtb.ext.request.ExtImp; @@ -74,11 +76,9 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.function.UnaryOperator; import java.util.stream.Collectors; @@ -121,6 +121,9 @@ public class BidResponseCreatorTest extends VertxTest { private EventsService eventsService; @Mock private StoredRequestProcessor storedRequestProcessor; + @Mock + private IdGenerator idGenerator; + private Clock clock; private Timeout timeout; @@ -135,6 +138,7 @@ public void setUp() { given(storedRequestProcessor.videoStoredDataResult(any(), anyList(), anyList(), any())) .willReturn(Future.succeededFuture(VideoStoredDataResult.empty())); + given(idGenerator.getType()).willReturn(IdGeneratorType.none); clock = Clock.fixed(Instant.ofEpochMilli(1000L), ZoneOffset.UTC); @@ -143,7 +147,7 @@ public void setUp() { bidderCatalog, eventsService, storedRequestProcessor, - false, + idGenerator, 0, clock, jacksonMapper); @@ -223,17 +227,19 @@ public void shouldRequestCacheServiceWithExpectedArguments() { .auctionTimestamp(1000L) .build())); - Map> biddersToCacheBidIds = new HashMap<>(); - biddersToCacheBidIds.put("bidder1", asList("bidId1", "bidId2")); - biddersToCacheBidIds.put("bidder2", asList("bidId3", "bidId4")); + Map> biddersToCacheBidIds = doubleMap( + "bidder1", doubleMap("bidId1-impId1", "bidId1", "bidId2-impId2", "bidId2"), + "bidder2", doubleMap("bidId3-impId1", "bidId3", "bidId4-impId2", "bidId4")); + assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); assertThat(context.isShouldCacheVideoBids()).isTrue(); assertThat(context.getCacheBidsTtl()).isEqualTo(99); assertThat(context.getCacheVideoBidsTtl()).isEqualTo(101); - assertMapWithUnorderedList(context.getBidderToBidIds(), biddersToCacheBidIds); - assertThat(context.getBidderToVideoBidIdsToModify()).isEmpty(); + assertThat(contextArgumentCaptor.getValue().getBidderToBidsToGeneratedIds().getBidderToBidIds()) + .containsAllEntriesOf(biddersToCacheBidIds); + assertThat(context.getBidderToVideoGeneratedBidIdsToModify().getBidderToBidIds()).isEmpty(); }); } @@ -271,13 +277,15 @@ public void shouldRequestCacheServiceWithWinningBidsOnlyWhenWinningonlyIsTrue() contextArgumentCaptor.capture(), eq(EventsContext.builder().auctionTimestamp(1000L).build())); - Map> biddersToCacheBidIds = new HashMap<>(); - biddersToCacheBidIds.put("bidder1", asList("bidId1", "bidId2")); - biddersToCacheBidIds.put("bidder2", asList("bidId3", "bidId4")); + final Map> biddersToCacheBidIds = doubleMap( + "bidder1", doubleMap("bidId1-impId1", "bidId1", "bidId2-impId2", "bidId2"), + "bidder2", doubleMap("bidId3-impId1", "bidId3", "bidId4-impId2", "bidId4")); + assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { - assertMapWithUnorderedList(context.getBidderToBidIds(), biddersToCacheBidIds); - assertThat(context.getBidderToVideoBidIdsToModify()).isEmpty(); + assertThat(contextArgumentCaptor.getValue().getBidderToBidsToGeneratedIds().getBidderToBidIds()) + .containsAllEntriesOf(biddersToCacheBidIds); + assertThat(context.getBidderToVideoGeneratedBidIdsToModify().getBidderToBidIds()).isEmpty(); }); } @@ -316,22 +324,30 @@ public void shouldRequestCacheServiceWithVideoBidsToModifyWhenEventsEnabledAndFo bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false); // then - Map> biddersToCacheBidIds = new HashMap<>(); - biddersToCacheBidIds.put("bidder1", Collections.singletonList("bidId1")); - biddersToCacheBidIds.put("bidder2", Collections.singletonList("bidId2")); + final Map> biddersToCacheBidIds = doubleMap( + "bidder1", singletonMap("bidId1-impId1", "bidId1"), + "bidder2", singletonMap("bidId2-impId2", "bidId2")); + + final ArgumentCaptor cacheContextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + verify(cacheService).cacheBidsOpenrtb( argThat(t -> t.containsAll(asList(bid1, bid2))), same(auctionContext), - eq(CacheContext.builder() - .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder1", singletonList("bidId1"))) - .bidderToBidIds(biddersToCacheBidIds) - .build()), + cacheContextArgumentCaptor.capture(), eq(EventsContext.builder() .enabledForAccount(true) .enabledForRequest(true) .auctionTimestamp(1000L) .build())); + + assertThat(cacheContextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.getBidderToVideoGeneratedBidIdsToModify().getBidderToBidIds()) + .containsAllEntriesOf(singletonMap("bidder1", singletonMap("bidId1-impId1", "bidId1"))); + assertThat(context.getBidderToBidsToGeneratedIds().getBidderToBidIds()) + .containsAllEntriesOf(biddersToCacheBidIds); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); } @Test @@ -351,14 +367,19 @@ public void shouldCallCacheServiceEvenRoundedCpmIsZero() { bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false); // then + final ArgumentCaptor cacheContextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); verify(cacheService).cacheBidsOpenrtb( argThat(bids -> bids.contains(bid1)), same(auctionContext), - eq(CacheContext.builder() - .bidderToVideoBidIdsToModify(emptyMap()) - .bidderToBidIds(singletonMap("bidder1", Collections.singletonList("bidId1"))) - .build()), + cacheContextArgumentCaptor.capture(), eq(EventsContext.builder().auctionTimestamp(1000L).build())); + + assertThat(cacheContextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.getBidderToVideoGeneratedBidIdsToModify().getBidderToBidIds()).isEmpty(); + assertThat(context.getBidderToBidsToGeneratedIds().getBidderToBidIds()) + .containsAllEntriesOf(singletonMap("bidder1", singletonMap("bidId1-impId1", "bidId1"))); + }); } @Test @@ -426,7 +447,7 @@ public void shouldSetNbrNullAndPopulateSeatbidWhenAtLeastOneBidIsPresent() { // given final AuctionContext auctionContext = givenAuctionContext(givenBidRequest()); - final Bid bid = Bid.builder().impid("imp1").build(); + final Bid bid = Bid.builder().impid("imp1").id("bidId").build(); final List bidderResponses = singletonList(BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, null, null)), 100)); @@ -446,7 +467,7 @@ public void shouldSkipBidderResponsesWhereSeatBidContainEmptyBids() { // given final AuctionContext auctionContext = givenAuctionContext(givenBidRequest()); - final Bid bid = Bid.builder().impid("imp1").build(); + final Bid bid = Bid.builder().impid("imp1").id("bidId").build(); final List bidderResponses = asList( BidderResponse.of("bidder1", givenSeatBid(), 0), BidderResponse.of("bidder2", givenSeatBid(BidderBid.of(bid, banner, "USD")), 0)); @@ -473,13 +494,15 @@ public void shouldOverrideBidIdWhenGenerateBidIdIsTurnedOn() { .build(); final List bidderResponses = singletonList( BidderResponse.of("bidder2", givenSeatBid(BidderBid.of(bid, banner, "USD")), 0)); + given(idGenerator.getType()).willReturn(IdGeneratorType.uuid); + given(idGenerator.generateId()).willReturn("de7fc739-0a6e-41ad-8961-701c30c82166"); final BidResponseCreator bidResponseCreator = new BidResponseCreator( cacheService, bidderCatalog, eventsService, storedRequestProcessor, - true, + idGenerator, 0, clock, jacksonMapper); @@ -497,11 +520,72 @@ public void shouldOverrideBidIdWhenGenerateBidIdIsTurnedOn() { .extracting(ExtBidPrebid::getBidid) .hasSize(1) .first() - .satisfies(bidId -> assertThat(UUID.fromString(bidId)).isInstanceOf(UUID.class)); + .isEqualTo("de7fc739-0a6e-41ad-8961-701c30c82166"); verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any()); } + @Test + public void shouldUseGeneratedBidIdForEventAndCacheWhenGeneratedIdIsTurnedOn() { + // given + final Account account = Account.builder().id("accountId").eventsEnabled(true).build(); + final BidRequest bidRequest = givenBidRequest( + identity(), + extBuilder -> extBuilder + .events(mapper.createObjectNode()) + .integration("pbjs")); + final AuctionContext auctionContext = givenAuctionContext( + bidRequest, + contextBuilder -> contextBuilder.account(account)); + final ExtPrebid prebid = ExtPrebid.of(ExtBidPrebid.builder().type(banner).build(), null); + final Bid bid = Bid.builder() + .id("bidId1") + .impid("impId1") + .price(BigDecimal.ONE) + .ext(mapper.valueToTree(prebid)) + .build(); + final List bidderResponses = singletonList( + BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 0)); + given(idGenerator.getType()).willReturn(IdGeneratorType.uuid); + given(idGenerator.generateId()).willReturn("de7fc739-0a6e-41ad-8961-701c30c82166"); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build(); + givenCacheServiceResult(singletonMap(bid, CacheInfo.of("id", null, null, null))); + + final Events events = Events.of("http://event-type-win", "http://event-type-view"); + given(eventsService.createEvent(anyString(), anyString(), anyString(), anyLong(), anyString())) + .willReturn(events); + + final BidResponseCreator bidResponseCreator = new BidResponseCreator( + cacheService, + bidderCatalog, + eventsService, + storedRequestProcessor, + idGenerator, + 0, + clock, + jacksonMapper); + + // when + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + + // then + final ArgumentCaptor cacheContextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + verify(cacheService).cacheBidsOpenrtb(any(), any(), cacheContextArgumentCaptor.capture(), any()); + + assertThat(cacheContextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheVideoBids()).isFalse(); + assertThat(context.getBidderToVideoGeneratedBidIdsToModify().getBidderToBidIds()).isEmpty(); + assertThat(context.getBidderToBidsToGeneratedIds().getBidderToBidIds()) + .containsAllEntriesOf(singletonMap("bidder1", + singletonMap("bidId1-impId1", "de7fc739-0a6e-41ad-8961-701c30c82166"))); + }); + + verify(eventsService).createEvent(eq("de7fc739-0a6e-41ad-8961-701c30c82166"), + anyString(), anyString(), anyLong(), anyString()); + } + @Test public void shouldSetExpectedResponseSeatBidAndBidFields() { // given @@ -736,7 +820,7 @@ public void shouldSetBidAdmToNullIfCacheIdIsPresentAndReturnCreativeBidsIsFalse( identity(), extBuilder -> extBuilder.targeting(givenTargeting()))); - final Bid bid = Bid.builder().price(BigDecimal.ONE).adm("adm").impid("i1").build(); + final Bid bid = Bid.builder().price(BigDecimal.ONE).adm("adm").id("bidId").impid("i1").build(); final List bidderResponses = singletonList(BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100)); @@ -764,7 +848,7 @@ public void shouldSetBidAdmToNullIfVideoCacheIdIsPresentAndReturnCreativeVideoBi identity(), extBuilder -> extBuilder.targeting(givenTargeting()))); - final Bid bid = Bid.builder().price(BigDecimal.ONE).adm("adm").impid("i1").build(); + final Bid bid = Bid.builder().price(BigDecimal.ONE).adm("adm").id("bidId").impid("i1").build(); final List bidderResponses = singletonList(BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100)); @@ -792,7 +876,7 @@ public void shouldSetBidExpWhenCacheIdIsMatched() { identity(), extBuilder -> extBuilder.targeting(givenTargeting()))); - final Bid bid = Bid.builder().price(BigDecimal.ONE).impid("i1").build(); + final Bid bid = Bid.builder().price(BigDecimal.ONE).impid("i1").id("bidId").build(); final List bidderResponses = singletonList(BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100)); @@ -820,7 +904,7 @@ public void shouldSetBidExpMaxTtlWhenCacheIdIsMatchedAndBothTtlIsSet() { identity(), extBuilder -> extBuilder.targeting(givenTargeting()))); - final Bid bid = Bid.builder().price(BigDecimal.ONE).impid("i1").build(); + final Bid bid = Bid.builder().price(BigDecimal.ONE).impid("i1").id("bidId").build(); final List bidderResponses = singletonList(BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100)); @@ -913,7 +997,7 @@ public void shouldTruncateTargetingKeywordsByGlobalConfig() { bidderCatalog, eventsService, storedRequestProcessor, - false, + idGenerator, 20, clock, jacksonMapper); @@ -981,7 +1065,7 @@ public void shouldTruncateTargetingKeywordsByRequestPassedValue() { .includewinners(true) .includebidderkeys(true) .includeformat(false) - .truncateattrchars(20) + .truncateattrchars(20) .build())); final AuctionContext auctionContext = givenAuctionContext( bidRequest, @@ -1063,7 +1147,7 @@ public void shouldPopulateTargetingKeywordsFromMediaTypePriceGranularities() { .includewinners(true) .includebidderkeys(true) .includeformat(false) - .build()))); + .build()))); final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("i1").build(); final List bidderResponses = singletonList(BidderResponse.of("bidder1", @@ -1458,7 +1542,7 @@ public void shouldNotPopulateWinningBidTargetingIfIncludeWinnersFlagIsFalse() { .includewinners(false) .includebidderkeys(true) .includeformat(false) - .build()))); + .build()))); final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("i1").build(); final List bidderResponses = singletonList(BidderResponse.of("bidder1", @@ -1500,7 +1584,7 @@ public void shouldNotPopulateBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse() .includewinners(true) .includebidderkeys(false) .includeformat(false) - .build()))); + .build()))); final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("i1").build(); final List bidderResponses = singletonList(BidderResponse.of("bidder1", @@ -1987,6 +2071,13 @@ private static void assertMapWithUnorderedList(Map> map, Map Map doubleMap(K key1, V value1, K key2, V value2) { + final Map result = new HashMap<>(); + result.put(key1, value1); + result.put(key2, value2); + return result; + } + @SafeVarargs private static List mutableList(T... values) { return Arrays.stream(values).collect(Collectors.toList()); diff --git a/src/test/java/org/prebid/server/auction/model/GeneratedBidIdsTest.java b/src/test/java/org/prebid/server/auction/model/GeneratedBidIdsTest.java new file mode 100644 index 00000000000..f704d68d962 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/model/GeneratedBidIdsTest.java @@ -0,0 +1,157 @@ +package org.prebid.server.auction.model; + +import com.iab.openrtb.response.Bid; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.identity.IdGenerator; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +public class GeneratedBidIdsTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private IdGenerator idGenerator; + + private BidderResponse bidderResponse1; + + private BidderResponse bidderResponse2; + + @Before + public void setup() { + bidderResponse1 = givenBidderResponse("bidder1", asList( + givenBidderBid("bidId1", "impId1"), givenBidderBid("bidId1", "impId2"))); + bidderResponse2 = givenBidderResponse("bidder2", asList(givenBidderBid("bidId1", "impId1"), + givenBidderBid("bidId2", "impId2"))); + + given(idGenerator.generateId()) + .willReturn("20dc17a4-4030-4c9f-a2b2-0a95a14af7e9", "a09fb35b-9406-4d1e-9ec3-10c5d33dd249", + "b90a06ad-cfd9-4e6e-bc44-4cf723ef5677", "8269d3fc-3304-4ecd-8221-4e0304bf11c2"); + } + + @Test + public void shouldCreateGeneratedBidIdsTestFromBidderResponses() { + // when + final GeneratedBidIds generatedBidIds = GeneratedBidIds.of(asList(bidderResponse1, bidderResponse2), + (ignored1, ignored2) -> idGenerator.generateId()); + + // then + final Map> expectedResult = + doubleMap("bidder1", + doubleMap("bidId1-impId1", "20dc17a4-4030-4c9f-a2b2-0a95a14af7e9", + "bidId1-impId2", "a09fb35b-9406-4d1e-9ec3-10c5d33dd249"), + "bidder2", + doubleMap("bidId1-impId1", "b90a06ad-cfd9-4e6e-bc44-4cf723ef5677", + "bidId2-impId2", "8269d3fc-3304-4ecd-8221-4e0304bf11c2")); + assertThat(generatedBidIds.getBidderToBidIds()) + .containsAllEntriesOf(expectedResult); + } + + @Test + public void getBidderForBidShouldReturnOptionalWithBidderIfExists() { + // given + final GeneratedBidIds generatedBidIds = GeneratedBidIds.of(asList(bidderResponse1, bidderResponse2), + (ignored1, ignored2) -> idGenerator.generateId()); + + // when + final Optional result = generatedBidIds.getBidderForBid("bidId1", "impId2"); + // then + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo("bidder1"); + } + + @Test + public void getBidderForBidShouldReturnEmptyOptionalIfBidderNotExists() { + // given + final GeneratedBidIds generatedBidIds = GeneratedBidIds.of(asList(bidderResponse1, bidderResponse2), + (ignored1, ignored2) -> idGenerator.generateId()); + + // when + final Optional result = generatedBidIds.getBidderForBid("bidId1", "impId3"); + + // then + assertThat(result).isEmpty(); + } + + @Test + public void getGeneratedIdShouldReturnGeneratedId() { + // given + final GeneratedBidIds generatedBidIds = GeneratedBidIds.of(asList(bidderResponse1, bidderResponse2), + (ignored1, ignored2) -> idGenerator.generateId()); + + // when + final String result = generatedBidIds.getGeneratedId("bidder1", "bidId1", "impId1"); + + // then + assertThat(result).isEqualTo("20dc17a4-4030-4c9f-a2b2-0a95a14af7e9"); + } + + @Test + public void getGeneratedIdShouldReturnNullIfBidWithBidIdAndImpIdPairWasNotFound() { + // given + final GeneratedBidIds generatedBidIds = GeneratedBidIds.of(asList(bidderResponse1, bidderResponse2), + (ignored1, ignored2) -> idGenerator.generateId()); + + // when + final String result = generatedBidIds.getGeneratedId("bidder1", "bidId1", "impId3"); + + // then + assertThat(result).isNull(); + } + + @Test + public void getGeneratedIdShouldReturnNullIfBidderNotFound() { + // given + final GeneratedBidIds generatedBidIds = GeneratedBidIds.of(asList(bidderResponse1, bidderResponse2), + (ignored1, ignored2) -> idGenerator.generateId()); + + // when + final String result = generatedBidIds.getGeneratedId("bidder3", "bidId1", "impId1"); + + // then + assertThat(result).isNull(); + } + + @Test + public void getGeneratedIdShouldReturnNullIfGeneratedIdCreatedFromEmptyMap() { + // given + final GeneratedBidIds generatedBidIds = GeneratedBidIds.empty(); + + // when + final String result = generatedBidIds.getGeneratedId("bidder3", "bidId1", "impId1"); + + // then + assertThat(result).isNull(); + } + + private static BidderBid givenBidderBid(String bidId, String impId) { + return BidderBid.of(Bid.builder().id(bidId).impid(impId).build(), null, null); + } + + private static BidderResponse givenBidderResponse(String bidder, List bidderBids) { + return BidderResponse.of(bidder, BidderSeatBid.of(bidderBids, null, null), 0); + } + + private Map doubleMap(K key1, V value1, K key2, V value2) { + final Map result = new HashMap<>(); + result.put(key1, value1); + result.put(key2, value2); + return result; + } + +} diff --git a/src/test/java/org/prebid/server/cache/CacheServiceTest.java b/src/test/java/org/prebid/server/cache/CacheServiceTest.java index e898666e5d3..71c6f793ebe 100644 --- a/src/test/java/org/prebid/server/cache/CacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CacheServiceTest.java @@ -15,6 +15,7 @@ import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.GeneratedBidIds; import org.prebid.server.cache.model.CacheContext; import org.prebid.server.cache.model.CacheHttpRequest; import org.prebid.server.cache.model.CacheInfo; @@ -47,6 +48,7 @@ import java.time.ZoneId; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeoutException; import java.util.function.UnaryOperator; @@ -55,7 +57,6 @@ import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -83,6 +84,10 @@ public class CacheServiceTest extends VertxTest { private EventsService eventsService; @Mock private Metrics metrics; + @Mock + private GeneratedBidIds allBidIds; + @Mock + private GeneratedBidIds videoCachedBidIds; private Clock clock; @@ -361,13 +366,16 @@ public void cacheBidsOpenrtbShouldNeverCallCacheServiceIfNoBidsPassed() { @Test public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedTimeout() { + // given + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); // when cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(identity())), givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -378,12 +386,14 @@ public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedTimeout() { @Test public void cacheBidsOpenrtbShouldTolerateGlobalTimeoutAlreadyExpired() { // when + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); final Future future = cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(identity())), givenAuctionContext().toBuilder().timeout(expiredTimeout).build(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -398,7 +408,8 @@ public void cacheBidsOpenrtbShouldTolerateGlobalTimeoutAlreadyExpired() { public void cacheBidsOpenrtbShouldStoreWinUrl() { // given final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); - + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); // when cacheService.cacheBidsOpenrtb( singletonList(bid), @@ -406,7 +417,7 @@ public void cacheBidsOpenrtbShouldStoreWinUrl() { .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), EventsContext.builder().enabledForAccount(true).enabledForRequest(true).build()); @@ -418,6 +429,8 @@ public void cacheBidsOpenrtbShouldStoreWinUrl() { public void cacheBidsOpenrtbShouldTolerateReadingHttpResponseFails() throws JsonProcessingException { // given givenHttpClientProducesException(new RuntimeException("Response exception")); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); @@ -428,7 +441,7 @@ public void cacheBidsOpenrtbShouldTolerateReadingHttpResponseFails() throws Json .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -448,6 +461,8 @@ public void cacheBidsOpenrtbShouldTolerateReadingHttpResponseFails() throws Json public void cacheBidsOpenrtbShouldTolerateResponseCodeIsNot200() throws JsonProcessingException { // given givenHttpClientReturnsResponse(503, "response"); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); @@ -458,7 +473,7 @@ public void cacheBidsOpenrtbShouldTolerateResponseCodeIsNot200() throws JsonProc .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -477,7 +492,8 @@ public void cacheBidsOpenrtbShouldTolerateResponseCodeIsNot200() throws JsonProc public void cacheBidsOpenrtbShouldTolerateResponseBodyCouldNotBeParsed() throws JsonProcessingException { // given givenHttpClientReturnsResponse(200, "response"); - + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); // when @@ -487,7 +503,7 @@ public void cacheBidsOpenrtbShouldTolerateResponseBodyCouldNotBeParsed() throws .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -507,6 +523,8 @@ public void cacheBidsOpenrtbShouldTolerateCacheEntriesNumberDoesNotMatchBidsNumb throws JsonProcessingException { // given givenHttpClientReturnsResponse(200, "{}"); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); @@ -517,7 +535,7 @@ public void cacheBidsOpenrtbShouldTolerateCacheEntriesNumberDoesNotMatchBidsNumb .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -537,6 +555,8 @@ public void cacheBidsOpenrtbShouldTolerateCacheEntriesNumberDoesNotMatchBidsNumb public void cacheBidsOpenrtbShouldReturnExpectedDebugInfo() throws JsonProcessingException { // given final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); // when final Future future = cacheService.cacheBidsOpenrtb( @@ -545,7 +565,7 @@ public void cacheBidsOpenrtbShouldReturnExpectedDebugInfo() throws JsonProcessin .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -564,6 +584,8 @@ public void cacheBidsOpenrtbShouldReturnExpectedCacheBids() { // given final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); // when final Future future = cacheService.cacheBidsOpenrtb( singletonList(bid), @@ -571,7 +593,7 @@ public void cacheBidsOpenrtbShouldReturnExpectedCacheBids() { .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -590,6 +612,16 @@ public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedBody() throws IO final Imp imp1 = givenImp(identity()); final Imp imp2 = givenImp(builder -> builder.id("impId2").video(Video.builder().build())); + given(allBidIds.getGeneratedId(any(), eq("bid1"), any())).willReturn("bid1"); + given(allBidIds.getGeneratedId(any(), eq("bid2"), any())).willReturn("bid2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); + + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); + + given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid2"); + given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); + // when cacheService.cacheBidsOpenrtb( asList(bid1, bid2), @@ -599,8 +631,8 @@ public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedBody() throws IO CacheContext.builder() .shouldCacheBids(true) .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder2", singletonList("bid2"))) - .bidderToBidIds(singletonMap("bidder1", asList("bid1", "bid2"))) + .bidderToVideoGeneratedBidIdsToModify(videoCachedBidIds) + .bidderToBidsToGeneratedIds(allBidIds) .build(), EventsContext.builder().auctionTimestamp(1000L).build()); @@ -618,6 +650,10 @@ public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedBody() throws IO @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() throws IOException { + // given + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); + // when final Future future = cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(builder -> builder.impid("impId1").exp(10))), @@ -625,7 +661,7 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBi .imp(singletonList(givenImp(buider -> buider.id("impId1").exp(20))))), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) + .bidderToBidsToGeneratedIds(allBidIds) .cacheBidsTtl(30) .build(), eventsContext); @@ -644,6 +680,10 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBi @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() throws IOException { + // given + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); + // when final Future future = cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(identity())), @@ -651,7 +691,7 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromIm .imp(singletonList(givenImp(buider -> buider.exp(10))))), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) + .bidderToBidsToGeneratedIds(allBidIds) .cacheBidsTtl(20) .build(), eventsContext); @@ -670,13 +710,17 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromIm @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() throws IOException { + // given + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); + // when final Future future = cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(identity())), givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) + .bidderToBidsToGeneratedIds(allBidIds) .cacheBidsTtl(10) .build(), eventsContext); @@ -696,6 +740,10 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRe @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBannerTtl() throws IOException { + // given + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); + // given cacheService = new CacheService( CacheTtl.of(20, null), @@ -715,7 +763,7 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAc identity()), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -734,6 +782,9 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAc @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl() throws IOException { // given + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); + cacheService = new CacheService( CacheTtl.of(10, null), httpClient, @@ -750,7 +801,7 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMe givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -769,6 +820,9 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMe @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithTtlFromMediaTypeWhenAccountIsEmpty() throws IOException { // given + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); + cacheService = new CacheService( CacheTtl.of(10, null), httpClient, @@ -785,7 +839,7 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithTtlFromMediaTypeWhenAccoun givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -803,13 +857,17 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithTtlFromMediaTypeWhenAccoun @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithNoTtlAndSetEmptyTtl() throws IOException { + // given + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); + // when final Future future = cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(identity())), givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -829,6 +887,8 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithNoTtlAndSetEmptyTtl() thro public void cacheBidsOpenrtbShouldReturnExpectedResultForBids() { // given final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(identity()); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); // when final Future future = cacheService.cacheBidsOpenrtb( @@ -836,7 +896,7 @@ public void cacheBidsOpenrtbShouldReturnExpectedResultForBids() { givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -850,6 +910,8 @@ public void cacheBidsOpenrtbShouldReturnExpectedResultForVideoBids() { // given final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.impid("impId1")); final Imp imp = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); // when final Future future = cacheService.cacheBidsOpenrtb( @@ -857,7 +919,7 @@ public void cacheBidsOpenrtbShouldReturnExpectedResultForVideoBids() { givenAuctionContext(bidRequestBuilder -> bidRequestBuilder.imp(singletonList(imp))), CacheContext.builder() .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder1", singletonList("bidId1"))) + .bidderToVideoGeneratedBidIdsToModify(allBidIds) .build(), eventsContext); @@ -872,6 +934,11 @@ public void cacheBidsOpenrtbShouldReturnExpectedResultForBidsAndVideoBids() thro givenHttpClientReturnsResponse(200, mapper.writeValueAsString( BidCacheResponse.of(asList(CacheObject.of("uuid1"), CacheObject.of("uuid2"), CacheObject.of("videoUuid1"), CacheObject.of("videoUuid2"))))); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); + + given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); final com.iab.openrtb.response.Bid bid1 = givenBidOpenrtb(builder -> builder.impid("impId1")); final com.iab.openrtb.response.Bid bid2 = givenBidOpenrtb(builder -> builder.impid("impId2")); @@ -885,8 +952,8 @@ public void cacheBidsOpenrtbShouldReturnExpectedResultForBidsAndVideoBids() thro CacheContext.builder() .shouldCacheBids(true) .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder1", singletonList("bidId1"))) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) + .bidderToVideoGeneratedBidIdsToModify(videoCachedBidIds) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -905,13 +972,16 @@ public void cacheBidsOpenrtbShouldNotCacheVideoBidWithMissingImpId() { final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); final Imp imp2 = givenImp(builder -> builder.id(null).video(Video.builder().build())); + given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); + // when final Future future = cacheService.cacheBidsOpenrtb( asList(bid1, bid2), givenAuctionContext(bidRequestBuilder -> bidRequestBuilder.imp(asList(imp1, imp2))), CacheContext.builder() .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder1", singletonList("bidId1"))) + .bidderToVideoGeneratedBidIdsToModify(videoCachedBidIds) .build(), eventsContext); @@ -929,6 +999,15 @@ public void cacheBidsOpenrtbShouldWrapEmptyAdmFieldUsingNurlFieldValue() throws .nurl("adm2")); final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); + given(allBidIds.getGeneratedId(any(), eq("bid1"), any())).willReturn("bid1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); + + given(allBidIds.getGeneratedId(any(), eq("bid2"), any())).willReturn("bid2"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); + + given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); + given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); + // when cacheService.cacheBidsOpenrtb( asList(bid1, bid2), @@ -936,8 +1015,8 @@ public void cacheBidsOpenrtbShouldWrapEmptyAdmFieldUsingNurlFieldValue() throws CacheContext.builder() .shouldCacheBids(true) .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder1", singletonList("bid1"))) - .bidderToBidIds(singletonMap("bidder1", asList("bid1", "bid2"))) + .bidderToVideoGeneratedBidIdsToModify(videoCachedBidIds) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -961,6 +1040,11 @@ public void cacheBidsOpenrtbShouldNotModifyVastXmlWhenBidIdIsNotInToModifyList() final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bid1").impid("impId1").adm("adm")); final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + + given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid2"); + given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); // when cacheService.cacheBidsOpenrtb( @@ -969,8 +1053,8 @@ public void cacheBidsOpenrtbShouldNotModifyVastXmlWhenBidIdIsNotInToModifyList() CacheContext.builder() .shouldCacheBids(true) .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder", singletonList("bid2"))) - .bidderToBidIds(singletonMap("bidder", singletonList("bid1"))) + .bidderToVideoGeneratedBidIdsToModify(videoCachedBidIds) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -988,6 +1072,11 @@ public void cacheBidsOpenrtbShouldNotAddTrackingImpToBidAdmWhenXmlDoesNotContain final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bid1").impid("impId1").adm("no impression tag")); final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + + given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid2"); + given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); // when cacheService.cacheBidsOpenrtb( @@ -996,8 +1085,8 @@ public void cacheBidsOpenrtbShouldNotAddTrackingImpToBidAdmWhenXmlDoesNotContain CacheContext.builder() .shouldCacheBids(true) .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder", singletonList("bid2"))) - .bidderToBidIds(singletonMap("bidder", singletonList("bid1"))) + .bidderToVideoGeneratedBidIdsToModify(videoCachedBidIds) + .bidderToBidsToGeneratedIds(allBidIds) .build(), eventsContext); @@ -1023,6 +1112,12 @@ public void cacheBidsOpenrtbShouldAddTrackingLinkToImpTagWhenItIsEmpty() throws given(eventsService.vastUrlTracking(anyString(), anyString(), any(), any(), any())) .willReturn("https://test-event.com/event?t=imp&b=bid1&f=b&a=accountId"); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + + given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); + given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + // when cacheService.cacheBidsOpenrtb( singletonList(bid), @@ -1030,8 +1125,8 @@ public void cacheBidsOpenrtbShouldAddTrackingLinkToImpTagWhenItIsEmpty() throws CacheContext.builder() .shouldCacheBids(true) .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder", singletonList("bid1"))) - .bidderToBidIds(singletonMap("bidder", singletonList("bid1"))) + .bidderToVideoGeneratedBidIdsToModify(videoCachedBidIds) + .bidderToBidsToGeneratedIds(allBidIds) .build(), EventsContext.builder().enabledForAccount(true).enabledForRequest(false).build()); @@ -1065,6 +1160,12 @@ public void cacheBidsOpenrtbShouldAddTrackingImpToBidAdmXmlWhenThatBidShouldBeMo given(eventsService.vastUrlTracking(any(), any(), any(), any(), any())) .willReturn("https://test-event.com/event?t=imp&b=bid1&f=b&a=accountId"); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + + given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); + given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + // when cacheService.cacheBidsOpenrtb( singletonList(bid), @@ -1072,8 +1173,8 @@ public void cacheBidsOpenrtbShouldAddTrackingImpToBidAdmXmlWhenThatBidShouldBeMo CacheContext.builder() .shouldCacheBids(true) .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder", singletonList("bid1"))) - .bidderToBidIds(singletonMap("bidder", singletonList("bid1"))) + .bidderToVideoGeneratedBidIdsToModify(videoCachedBidIds) + .bidderToBidsToGeneratedIds(allBidIds) .build(), EventsContext.builder().enabledForAccount(true).enabledForRequest(false).build()); @@ -1104,6 +1205,12 @@ public void cacheBidsOpenrtbShouldNotAddTrackingImpWhenEventsNotEnabled() throws .id("impId1") .video(Video.builder().build())); + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); + given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + + given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); + given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + // when cacheService.cacheBidsOpenrtb( singletonList(bid), @@ -1111,8 +1218,8 @@ public void cacheBidsOpenrtbShouldNotAddTrackingImpWhenEventsNotEnabled() throws CacheContext.builder() .shouldCacheBids(true) .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder", singletonList("bid1"))) - .bidderToBidIds(singletonMap("bidder", singletonList("bid1"))) + .bidderToVideoGeneratedBidIdsToModify(videoCachedBidIds) + .bidderToBidsToGeneratedIds(allBidIds) .build(), EventsContext.builder().enabledForAccount(false).build()); 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 77a14c83601..cb2bd89e92b 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -378,7 +378,7 @@ default-timeout-ms=2000 timeout-adjustment-ms=0 auction.default-timeout-ms=2000 auction.timeout-adjustment-ms=0 -auction.id-generator-type=none +auction.generate-source-tid=false currency-converter.external-rates.enabled=true currency-converter.external-rates.url=http://localhost:8090/currency-rates amp.default-timeout-ms=2000 From 6f15040cc17ef9ed401eb7fdc6857d6706949eff Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Mon, 18 Jan 2021 19:44:13 +0200 Subject: [PATCH 087/129] Fix setting Rubicon size id using video placement (#1103) --- .../prebid/server/auction/BidderAliases.java | 8 ++-- .../server/auction/ExchangeService.java | 4 +- .../server/bidder/rubicon/RubiconBidder.java | 34 ++++++++------ .../bidder/triplelift/TripleliftBidder.java | 2 +- .../org/prebid/server/json/JsonMerger.java | 2 +- .../prebid/server/settings/model/Account.java | 26 +++++------ .../bidder/rubicon/RubiconBidderTest.java | 44 +++++++++++-------- 7 files changed, 66 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/BidderAliases.java b/src/main/java/org/prebid/server/auction/BidderAliases.java index ed05653f02b..0d00adcc392 100644 --- a/src/main/java/org/prebid/server/auction/BidderAliases.java +++ b/src/main/java/org/prebid/server/auction/BidderAliases.java @@ -1,9 +1,9 @@ package org.prebid.server.auction; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.bidder.BidderCatalog; -import java.util.Collections; import java.util.Map; import java.util.Objects; @@ -21,8 +21,8 @@ public class BidderAliases { private BidderAliases( Map aliasToBidder, Map aliasToVendorId, BidderCatalog bidderCatalog) { - this.aliasToBidder = ObjectUtils.firstNonNull(aliasToBidder, Collections.emptyMap()); - this.aliasToVendorId = ObjectUtils.firstNonNull(aliasToVendorId, Collections.emptyMap()); + this.aliasToBidder = MapUtils.emptyIfNull(aliasToBidder); + this.aliasToVendorId = MapUtils.emptyIfNull(aliasToVendorId); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); } @@ -43,7 +43,7 @@ public boolean isAliasDefined(String alias) { public String resolveBidder(String aliasOrBidder) { return aliasToBidder.containsKey(aliasOrBidder) ? aliasToBidder.get(aliasOrBidder) - : ObjectUtils.firstNonNull(resolveBidderViaCatalog(aliasOrBidder), aliasOrBidder); + : ObjectUtils.defaultIfNull(resolveBidderViaCatalog(aliasOrBidder), aliasOrBidder); } public Integer resolveAliasVendorId(String alias) { diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index dbd39b5c921..11f21e978d7 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -405,7 +405,7 @@ private Map prepareUsers(List bidders, final Map bidderToUser = new HashMap<>(); for (String bidder : bidders) { - final ExtBidderConfigFpd fpdConfig = ObjectUtils.firstNonNull(biddersToConfigs.get(bidder), + final ExtBidderConfigFpd fpdConfig = ObjectUtils.defaultIfNull(biddersToConfigs.get(bidder), biddersToConfigs.get(ALL_BIDDERS_CONFIG)); final boolean useFirstPartyData = firstPartyDataBidders == null || firstPartyDataBidders.contains(bidder); @@ -578,7 +578,7 @@ private BidderRequest createBidderRequest(BidderPrivacyResult bidderPrivacyResul final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); final boolean useFirstPartyData = firstPartyDataBidders == null || firstPartyDataBidders.contains(bidder); - final ExtBidderConfigFpd fpdConfig = ObjectUtils.firstNonNull(biddersToConfigs.get(bidder), + final ExtBidderConfigFpd fpdConfig = ObjectUtils.defaultIfNull(biddersToConfigs.get(bidder), biddersToConfigs.get(ALL_BIDDERS_CONFIG)); final Site bidRequestSite = bidRequest.getSite(); 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 6d13717c48e..ac90f63aa8f 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -386,7 +386,7 @@ private Imp makeImp(Imp imp, ExtImpPrebid extPrebid, ExtImpRubicon extRubicon, S if (isVideo(imp)) { builder .banner(null) - .video(makeVideo(imp, site, extRubicon.getVideo(), extPrebid)); + .video(makeVideo(imp, extRubicon.getVideo(), extPrebid, referer(site))); } else { builder .banner(makeBanner(imp, overriddenSizes(extRubicon))) @@ -656,23 +656,29 @@ private static boolean isFullyPopulatedVideo(Video video) { && video.getLinearity() != null && video.getApi() != null; } - private Video makeVideo(Imp imp, Site site, RubiconVideoParams rubiconVideoParams, ExtImpPrebid prebidImpExt) { - final Video video = imp.getVideo(); - final String impId = imp.getId(); - final String referer = site != null ? site.getPage() : null; - final String videoType = prebidImpExt != null && prebidImpExt.getIsRewardedInventory() != null - && prebidImpExt.getIsRewardedInventory() == 1 ? "rewarded" : null; + private static String referer(Site site) { + return site != null ? site.getPage() : null; + } - if (rubiconVideoParams == null && videoType == null) { - return video; - } + private Video makeVideo(Imp imp, RubiconVideoParams rubiconVideoParams, ExtImpPrebid prebidImpExt, String referer) { + final Video video = imp.getVideo(); final Integer skip = rubiconVideoParams != null ? rubiconVideoParams.getSkip() : null; final Integer skipDelay = rubiconVideoParams != null ? rubiconVideoParams.getSkipdelay() : null; final Integer sizeId = rubiconVideoParams != null ? rubiconVideoParams.getSizeId() : null; + final Integer resolvedSizeId = sizeId == null || sizeId == 0 - ? resolveVideoSizeId(video.getPlacement(), imp.getInstl()) : sizeId; - validateVideoSizeId(resolvedSizeId, referer, impId); + ? resolveVideoSizeId(video.getPlacement(), imp.getInstl()) + : sizeId; + validateVideoSizeId(resolvedSizeId, referer, imp.getId()); + + final String videoType = prebidImpExt != null && prebidImpExt.getIsRewardedInventory() != null + && prebidImpExt.getIsRewardedInventory() == 1 ? "rewarded" : null; + + // optimization for empty ext params + if (skip == null && skipDelay == null && resolvedSizeId == null && videoType == null) { + return video; + } return video.toBuilder() .ext(mapper.mapper().valueToTree( @@ -680,7 +686,7 @@ private Video makeVideo(Imp imp, Site site, RubiconVideoParams rubiconVideoParam .build(); } - private void validateVideoSizeId(Integer resolvedSizeId, String referer, String impId) { + private static void validateVideoSizeId(Integer resolvedSizeId, String referer, String impId) { // log only 1% of cases to monitor how often video impressions does not have size id if (resolvedSizeId == null) { MISSING_VIDEO_SIZE_LOGGER.warn(String.format("RP adapter: video request with no size_id. Referrer URL = %s," @@ -688,7 +694,7 @@ private void validateVideoSizeId(Integer resolvedSizeId, String referer, String } } - private Integer resolveVideoSizeId(Integer placement, Integer instl) { + private static Integer resolveVideoSizeId(Integer placement, Integer instl) { if (placement != null) { if (placement == 1) { return 201; diff --git a/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java b/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java index 2ce0c27a656..ad099079b3e 100644 --- a/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java +++ b/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java @@ -89,7 +89,7 @@ private Imp modifyImp(Imp imp) throws PreBidException { final ExtImpTriplelift impExt = parseImpExt(imp); return imp.toBuilder() .tagid(impExt.getInventoryCode()) - .bidfloor(ObjectUtils.firstNonNull(impExt.getFloor(), imp.getBidfloor())) + .bidfloor(ObjectUtils.defaultIfNull(impExt.getFloor(), imp.getBidfloor())) .build(); } diff --git a/src/main/java/org/prebid/server/json/JsonMerger.java b/src/main/java/org/prebid/server/json/JsonMerger.java index ee21248df27..b46010eb5a8 100644 --- a/src/main/java/org/prebid/server/json/JsonMerger.java +++ b/src/main/java/org/prebid/server/json/JsonMerger.java @@ -48,7 +48,7 @@ public T merge(T originalObject, String storedData, String id, Class clas public T merge(T originalObject, T mergingObject, Class classToCast) { if (!ObjectUtils.allNotNull(originalObject, mergingObject)) { - return ObjectUtils.firstNonNull(originalObject, mergingObject); + return ObjectUtils.defaultIfNull(originalObject, mergingObject); } final JsonNode originJsonNode = mapper.mapper().valueToTree(originalObject); diff --git a/src/main/java/org/prebid/server/settings/model/Account.java b/src/main/java/org/prebid/server/settings/model/Account.java index 75879559cbe..479c2bc680a 100644 --- a/src/main/java/org/prebid/server/settings/model/Account.java +++ b/src/main/java/org/prebid/server/settings/model/Account.java @@ -36,20 +36,20 @@ public class Account { public Account merge(Account another) { return Account.builder() - .id(ObjectUtils.firstNonNull(id, another.id)) - .priceGranularity(ObjectUtils.firstNonNull(priceGranularity, another.priceGranularity)) - .bannerCacheTtl(ObjectUtils.firstNonNull(bannerCacheTtl, another.bannerCacheTtl)) - .videoCacheTtl(ObjectUtils.firstNonNull(videoCacheTtl, another.videoCacheTtl)) - .eventsEnabled(ObjectUtils.firstNonNull(eventsEnabled, another.eventsEnabled)) - .enforceCcpa(ObjectUtils.firstNonNull(enforceCcpa, another.enforceCcpa)) - .gdpr(ObjectUtils.firstNonNull(gdpr, another.gdpr)) - .analyticsSamplingFactor(ObjectUtils.firstNonNull( + .id(ObjectUtils.defaultIfNull(id, another.id)) + .priceGranularity(ObjectUtils.defaultIfNull(priceGranularity, another.priceGranularity)) + .bannerCacheTtl(ObjectUtils.defaultIfNull(bannerCacheTtl, another.bannerCacheTtl)) + .videoCacheTtl(ObjectUtils.defaultIfNull(videoCacheTtl, another.videoCacheTtl)) + .eventsEnabled(ObjectUtils.defaultIfNull(eventsEnabled, another.eventsEnabled)) + .enforceCcpa(ObjectUtils.defaultIfNull(enforceCcpa, another.enforceCcpa)) + .gdpr(ObjectUtils.defaultIfNull(gdpr, another.gdpr)) + .analyticsSamplingFactor(ObjectUtils.defaultIfNull( analyticsSamplingFactor, another.analyticsSamplingFactor)) - .truncateTargetAttr(ObjectUtils.firstNonNull(truncateTargetAttr, another.truncateTargetAttr)) - .defaultIntegration(ObjectUtils.firstNonNull(defaultIntegration, another.defaultIntegration)) - .analyticsConfig(ObjectUtils.firstNonNull(analyticsConfig, another.analyticsConfig)) - .bidValidations(ObjectUtils.firstNonNull(bidValidations, another.bidValidations)) - .status(ObjectUtils.firstNonNull(status, another.status)) + .truncateTargetAttr(ObjectUtils.defaultIfNull(truncateTargetAttr, another.truncateTargetAttr)) + .defaultIntegration(ObjectUtils.defaultIfNull(defaultIntegration, another.defaultIntegration)) + .analyticsConfig(ObjectUtils.defaultIfNull(analyticsConfig, another.analyticsConfig)) + .bidValidations(ObjectUtils.defaultIfNull(bidValidations, another.bidValidations)) + .status(ObjectUtils.defaultIfNull(status, another.status)) .build(); } 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 2a5b3b7dc05..5d8b50ae993 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -421,11 +421,11 @@ public void makeHttpRequestsShouldCreateVideoRequestIfImpHasBannerAndVideoButAll } @Test - public void shouldSetSizeIdTo201IfplacementIs1IfSizeIdIsNotPresent() { + public void shouldSetSizeIdTo201IfPlacementIs1AndSizeIdIsNotPresent() { // given final BidRequest bidRequest = givenBidRequest( builder -> builder.instl(1).video(Video.builder().placement(1).build()), - builder -> builder.video(RubiconVideoParams.builder().skip(5).skipdelay(10).sizeId(null).build())); + builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build())); // when final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); @@ -433,20 +433,22 @@ public void shouldSetSizeIdTo201IfplacementIs1IfSizeIdIsNotPresent() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1).doesNotContainNull() - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getVideo).doesNotContainNull() - .extracting(Video::getExt).doesNotContainNull() - .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) - .containsOnly(RubiconVideoExt.of(5, 10, RubiconVideoExtRp.of(201), null)); + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getVideo).doesNotContainNull() + .extracting(Video::getExt).doesNotContainNull() + .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) + .extracting(RubiconVideoExt::getRp) + .extracting(RubiconVideoExtRp::getSizeId) + .containsOnly(201); } @Test - public void shouldSetSizeIdTo203IfplacementIs3IfSizeIdIsNotPresent() { + public void shouldSetSizeIdTo203IfPlacementIs3AndSizeIdIsNotPresent() { // given final BidRequest bidRequest = givenBidRequest( builder -> builder.instl(1).video(Video.builder().placement(3).build()), - builder -> builder.video(RubiconVideoParams.builder().skip(5).skipdelay(10).sizeId(null).build())); + builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build())); // when final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); @@ -459,15 +461,17 @@ public void shouldSetSizeIdTo203IfplacementIs3IfSizeIdIsNotPresent() { .extracting(Imp::getVideo).doesNotContainNull() .extracting(Video::getExt).doesNotContainNull() .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) - .containsOnly(RubiconVideoExt.of(5, 10, RubiconVideoExtRp.of(203), null)); + .extracting(RubiconVideoExt::getRp) + .extracting(RubiconVideoExtRp::getSizeId) + .containsOnly(203); } @Test - public void shouldCalculateSizeIdUsingInstlIfPlacementAndSizeIdIsNotPresent() { + public void shouldSetSizeIdTo202UsingInstlFlagIfPlacementAndSizeIdAreNotPresent() { // given final BidRequest bidRequest = givenBidRequest( builder -> builder.instl(1).video(Video.builder().placement(null).build()), - builder -> builder.video(RubiconVideoParams.builder().skip(5).skipdelay(10).sizeId(null).build())); + builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build())); // when final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); @@ -475,12 +479,14 @@ public void shouldCalculateSizeIdUsingInstlIfPlacementAndSizeIdIsNotPresent() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1).doesNotContainNull() - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getVideo).doesNotContainNull() - .extracting(Video::getExt).doesNotContainNull() - .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) - .containsOnly(RubiconVideoExt.of(5, 10, RubiconVideoExtRp.of(202), null)); + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getVideo).doesNotContainNull() + .extracting(Video::getExt).doesNotContainNull() + .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) + .extracting(RubiconVideoExt::getRp) + .extracting(RubiconVideoExtRp::getSizeId) + .containsOnly(202); } @Test From 00354f1bb277ebc6ba90f31c92bc7a67398a8b17 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Tue, 19 Jan 2021 11:59:58 +0200 Subject: [PATCH 088/129] Fixed mismatched winUrl and VastXml (#1104) --- .../server/auction/AuctionRequestFactory.java | 7 +- .../org/prebid/server/cache/CacheService.java | 11 +- .../auction/BidResponseCreatorTest.java | 13 +- .../prebid/server/cache/CacheServiceTest.java | 138 +++--------------- 4 files changed, 43 insertions(+), 126 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java index f3826db6735..c8ddb5cdbe7 100644 --- a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java @@ -776,7 +776,8 @@ private Future accountFrom(BidRequest bidRequest, Timeout timeout, Rout return blankAccountId ? responseForEmptyAccount(routingContext) : applicationSettings.getAccountById(accountId, timeout) - .compose(this::ensureAccountActive, exception -> accountFallback(exception, accountId, routingContext)); + .compose(this::ensureAccountActive, + exception -> accountFallback(exception, accountId, routingContext)); } /** @@ -846,8 +847,8 @@ private Future ensureAccountActive(Account account) { final String accountId = account.getId(); return account.getStatus() == AccountStatus.inactive - ? Future.failedFuture( - new UnauthorizedAccountException(String.format("Account %s is inactive", accountId), accountId)) + ? Future.failedFuture(new UnauthorizedAccountException( + String.format("Account %s is inactive", accountId), accountId)) : Future.succeededFuture(account); } diff --git a/src/main/java/org/prebid/server/cache/CacheService.java b/src/main/java/org/prebid/server/cache/CacheService.java index 23d3c68e556..c6a06a4bd40 100644 --- a/src/main/java/org/prebid/server/cache/CacheService.java +++ b/src/main/java/org/prebid/server/cache/CacheService.java @@ -550,9 +550,11 @@ private String generateWinUrl(GeneratedBidIds biddersToCacheBidIds, EventsContext eventsContext) { if (eventsContext.isEnabledForAccount() && eventsContext.isEnabledForRequest()) { - return biddersToCacheBidIds.getBidderForBid(bid.getId(), bid.getImpid()) + final String bidId = bid.getId(); + final String impId = bid.getImpid(); + return biddersToCacheBidIds.getBidderForBid(bidId, impId) .map(bidder -> eventsService.winUrl( - bid.getId(), + biddersToCacheBidIds.getGeneratedId(bidder, bidId, impId), bidder, account.getId(), eventsContext.getAuctionTimestamp(), @@ -570,9 +572,10 @@ private String generateVastUrlTracking(GeneratedBidIds bidderToVideoBidIdsToModi if (eventsContext.isEnabledForAccount()) { final String bidId = bid.getId(); - return bidderToVideoBidIdsToModify.getBidderForBid(bid.getId(), bid.getImpid()) + final String impId = bid.getImpid(); + return bidderToVideoBidIdsToModify.getBidderForBid(bidId, impId) .map(bidder -> eventsService.vastUrlTracking( - bidId, + bidderToVideoBidIdsToModify.getGeneratedId(bidder, bidId, impId), bidder, account.getId(), eventsContext.getAuctionTimestamp(), diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 6a47dd327b1..4e76a72b36f 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -534,9 +534,11 @@ public void shouldUseGeneratedBidIdForEventAndCacheWhenGeneratedIdIsTurnedOn() { extBuilder -> extBuilder .events(mapper.createObjectNode()) .integration("pbjs")); + final AuctionContext auctionContext = givenAuctionContext( bidRequest, contextBuilder -> contextBuilder.account(account)); + final ExtPrebid prebid = ExtPrebid.of(ExtBidPrebid.builder().type(banner).build(), null); final Bid bid = Bid.builder() .id("bidId1") @@ -544,10 +546,13 @@ public void shouldUseGeneratedBidIdForEventAndCacheWhenGeneratedIdIsTurnedOn() { .price(BigDecimal.ONE) .ext(mapper.valueToTree(prebid)) .build(); + final List bidderResponses = singletonList( BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 0)); + + final String generatedBid = "de7fc739-0a6e-41ad-8961-701c30c82166"; given(idGenerator.getType()).willReturn(IdGeneratorType.uuid); - given(idGenerator.generateId()).willReturn("de7fc739-0a6e-41ad-8961-701c30c82166"); + given(idGenerator.generateId()).willReturn(generatedBid); final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build(); givenCacheServiceResult(singletonMap(bid, CacheInfo.of("id", null, null, null))); @@ -578,12 +583,10 @@ public void shouldUseGeneratedBidIdForEventAndCacheWhenGeneratedIdIsTurnedOn() { assertThat(context.isShouldCacheVideoBids()).isFalse(); assertThat(context.getBidderToVideoGeneratedBidIdsToModify().getBidderToBidIds()).isEmpty(); assertThat(context.getBidderToBidsToGeneratedIds().getBidderToBidIds()) - .containsAllEntriesOf(singletonMap("bidder1", - singletonMap("bidId1-impId1", "de7fc739-0a6e-41ad-8961-701c30c82166"))); + .containsAllEntriesOf(singletonMap("bidder1", singletonMap("bidId1-impId1", generatedBid))); }); - verify(eventsService).createEvent(eq("de7fc739-0a6e-41ad-8961-701c30c82166"), - anyString(), anyString(), anyLong(), anyString()); + verify(eventsService).createEvent(eq(generatedBid), anyString(), anyString(), anyLong(), anyString()); } @Test diff --git a/src/test/java/org/prebid/server/cache/CacheServiceTest.java b/src/test/java/org/prebid/server/cache/CacheServiceTest.java index 71c6f793ebe..ba7d8f42c27 100644 --- a/src/test/java/org/prebid/server/cache/CacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CacheServiceTest.java @@ -366,9 +366,6 @@ public void cacheBidsOpenrtbShouldNeverCallCacheServiceIfNoBidsPassed() { @Test public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedTimeout() { - // given - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); // when cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(identity())), @@ -386,8 +383,6 @@ public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedTimeout() { @Test public void cacheBidsOpenrtbShouldTolerateGlobalTimeoutAlreadyExpired() { // when - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); final Future future = cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(identity())), givenAuctionContext().toBuilder().timeout(expiredTimeout).build(), @@ -405,11 +400,13 @@ public void cacheBidsOpenrtbShouldTolerateGlobalTimeoutAlreadyExpired() { } @Test - public void cacheBidsOpenrtbShouldStoreWinUrl() { + public void cacheBidsOpenrtbShouldStoreWinUrlWithGeneratedBidId() { // given final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); + final String generatedBidId = "GeneratedBidId"; + given(allBidIds.getGeneratedId(any(), any(), any())).willReturn(generatedBidId); given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + // when cacheService.cacheBidsOpenrtb( singletonList(bid), @@ -422,15 +419,13 @@ public void cacheBidsOpenrtbShouldStoreWinUrl() { EventsContext.builder().enabledForAccount(true).enabledForRequest(true).build()); // then - verify(eventsService).winUrl(eq("bidId1"), eq("bidder"), eq("accountId"), isNull(), isNull()); + verify(eventsService).winUrl(eq(generatedBidId), eq("bidder"), eq("accountId"), isNull(), isNull()); } @Test public void cacheBidsOpenrtbShouldTolerateReadingHttpResponseFails() throws JsonProcessingException { // given givenHttpClientProducesException(new RuntimeException("Response exception")); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); @@ -461,8 +456,6 @@ public void cacheBidsOpenrtbShouldTolerateReadingHttpResponseFails() throws Json public void cacheBidsOpenrtbShouldTolerateResponseCodeIsNot200() throws JsonProcessingException { // given givenHttpClientReturnsResponse(503, "response"); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); @@ -492,8 +485,6 @@ public void cacheBidsOpenrtbShouldTolerateResponseCodeIsNot200() throws JsonProc public void cacheBidsOpenrtbShouldTolerateResponseBodyCouldNotBeParsed() throws JsonProcessingException { // given givenHttpClientReturnsResponse(200, "response"); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); // when @@ -523,8 +514,6 @@ public void cacheBidsOpenrtbShouldTolerateCacheEntriesNumberDoesNotMatchBidsNumb throws JsonProcessingException { // given givenHttpClientReturnsResponse(200, "{}"); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); @@ -555,8 +544,6 @@ public void cacheBidsOpenrtbShouldTolerateCacheEntriesNumberDoesNotMatchBidsNumb public void cacheBidsOpenrtbShouldReturnExpectedDebugInfo() throws JsonProcessingException { // given final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); // when final Future future = cacheService.cacheBidsOpenrtb( @@ -584,8 +571,6 @@ public void cacheBidsOpenrtbShouldReturnExpectedCacheBids() { // given final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); // when final Future future = cacheService.cacheBidsOpenrtb( singletonList(bid), @@ -612,16 +597,6 @@ public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedBody() throws IO final Imp imp1 = givenImp(identity()); final Imp imp2 = givenImp(builder -> builder.id("impId2").video(Video.builder().build())); - given(allBidIds.getGeneratedId(any(), eq("bid1"), any())).willReturn("bid1"); - given(allBidIds.getGeneratedId(any(), eq("bid2"), any())).willReturn("bid2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); - - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); - - given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid2"); - given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); - // when cacheService.cacheBidsOpenrtb( asList(bid1, bid2), @@ -650,10 +625,6 @@ public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedBody() throws IO @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() throws IOException { - // given - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); - // when final Future future = cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(builder -> builder.impid("impId1").exp(10))), @@ -680,10 +651,6 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBi @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() throws IOException { - // given - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); - // when final Future future = cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(identity())), @@ -710,10 +677,6 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromIm @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() throws IOException { - // given - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); - // when final Future future = cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(identity())), @@ -740,10 +703,6 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRe @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBannerTtl() throws IOException { - // given - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); - // given cacheService = new CacheService( CacheTtl.of(20, null), @@ -781,10 +740,6 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAc @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl() throws IOException { - // given - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); - cacheService = new CacheService( CacheTtl.of(10, null), httpClient, @@ -819,10 +774,6 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMe @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithTtlFromMediaTypeWhenAccountIsEmpty() throws IOException { - // given - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); - cacheService = new CacheService( CacheTtl.of(10, null), httpClient, @@ -857,10 +808,6 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithTtlFromMediaTypeWhenAccoun @Test public void cacheBidsOpenrtbShouldSendCacheRequestWithNoTtlAndSetEmptyTtl() throws IOException { - // given - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); - // when final Future future = cacheService.cacheBidsOpenrtb( singletonList(givenBidOpenrtb(identity())), @@ -887,8 +834,6 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithNoTtlAndSetEmptyTtl() thro public void cacheBidsOpenrtbShouldReturnExpectedResultForBids() { // given final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(identity()); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); // when final Future future = cacheService.cacheBidsOpenrtb( @@ -910,8 +855,6 @@ public void cacheBidsOpenrtbShouldReturnExpectedResultForVideoBids() { // given final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.impid("impId1")); final Imp imp = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); // when final Future future = cacheService.cacheBidsOpenrtb( @@ -934,11 +877,6 @@ public void cacheBidsOpenrtbShouldReturnExpectedResultForBidsAndVideoBids() thro givenHttpClientReturnsResponse(200, mapper.writeValueAsString( BidCacheResponse.of(asList(CacheObject.of("uuid1"), CacheObject.of("uuid2"), CacheObject.of("videoUuid1"), CacheObject.of("videoUuid2"))))); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder2")); - - given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); - given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); final com.iab.openrtb.response.Bid bid1 = givenBidOpenrtb(builder -> builder.impid("impId1")); final com.iab.openrtb.response.Bid bid2 = givenBidOpenrtb(builder -> builder.impid("impId2")); @@ -972,9 +910,6 @@ public void cacheBidsOpenrtbShouldNotCacheVideoBidWithMissingImpId() { final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); final Imp imp2 = givenImp(builder -> builder.id(null).video(Video.builder().build())); - given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bidId1"); - given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); - // when final Future future = cacheService.cacheBidsOpenrtb( asList(bid1, bid2), @@ -999,15 +934,6 @@ public void cacheBidsOpenrtbShouldWrapEmptyAdmFieldUsingNurlFieldValue() throws .nurl("adm2")); final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); - given(allBidIds.getGeneratedId(any(), eq("bid1"), any())).willReturn("bid1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); - - given(allBidIds.getGeneratedId(any(), eq("bid2"), any())).willReturn("bid2"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); - - given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); - given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder1")); - // when cacheService.cacheBidsOpenrtb( asList(bid1, bid2), @@ -1040,11 +966,6 @@ public void cacheBidsOpenrtbShouldNotModifyVastXmlWhenBidIdIsNotInToModifyList() final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bid1").impid("impId1").adm("adm")); final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); - - given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid2"); - given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); // when cacheService.cacheBidsOpenrtb( @@ -1072,11 +993,6 @@ public void cacheBidsOpenrtbShouldNotAddTrackingImpToBidAdmWhenXmlDoesNotContain final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bid1").impid("impId1").adm("no impression tag")); final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); - - given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid2"); - given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); // when cacheService.cacheBidsOpenrtb( @@ -1109,14 +1025,14 @@ public void cacheBidsOpenrtbShouldAddTrackingLinkToImpTagWhenItIsEmpty() throws .id("impId1") .video(Video.builder().build())); - given(eventsService.vastUrlTracking(anyString(), anyString(), any(), any(), any())) - .willReturn("https://test-event.com/event?t=imp&b=bid1&f=b&a=accountId"); - - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + final String generatedBidId = "generatedBidId"; + final String bidder = "bidder"; + given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn(generatedBidId); + given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of(bidder)); - given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); - given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + final String vastUrl = String.format("https://test-event.com/event?t=imp&b=%s&f=b&a=accountId", generatedBidId); + given(eventsService.vastUrlTracking(anyString(), anyString(), any(), any(), any())) + .willReturn(vastUrl); // when cacheService.cacheBidsOpenrtb( @@ -1131,6 +1047,8 @@ public void cacheBidsOpenrtbShouldAddTrackingLinkToImpTagWhenItIsEmpty() throws EventsContext.builder().enabledForAccount(true).enabledForRequest(false).build()); // then + verify(eventsService).vastUrlTracking(eq(generatedBidId), eq(bidder), any(), any(), any()); + final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); assertThat(bidCacheRequest.getPuts()).hasSize(2) .containsOnly( @@ -1140,8 +1058,7 @@ public void cacheBidsOpenrtbShouldAddTrackingLinkToImpTagWhenItIsEmpty() throws .build(), PutObject.builder() .type("xml") - .value(new TextNode("")) + .value(new TextNode("")) .build()); } @@ -1157,14 +1074,14 @@ public void cacheBidsOpenrtbShouldAddTrackingImpToBidAdmXmlWhenThatBidShouldBeMo .id("impId1") .video(Video.builder().build())); - given(eventsService.vastUrlTracking(any(), any(), any(), any(), any())) - .willReturn("https://test-event.com/event?t=imp&b=bid1&f=b&a=accountId"); + final String generatedBidId = "generatedBidId"; + final String bidder = "bidder"; + given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn(generatedBidId); + given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of(bidder)); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); - - given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); - given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); + final String vastUrl = String.format("https://test-event.com/event?t=imp&b=%s&f=b&a=accountId", generatedBidId); + given(eventsService.vastUrlTracking(anyString(), anyString(), any(), any(), any())) + .willReturn(vastUrl); // when cacheService.cacheBidsOpenrtb( @@ -1188,9 +1105,8 @@ public void cacheBidsOpenrtbShouldAddTrackingImpToBidAdmXmlWhenThatBidShouldBeMo .build(), PutObject.builder() .type("xml") - .value(new TextNode("http:/test.com" - + "" - + "")) + .value(new TextNode("http:/test.com" + + "")) .build()); } @@ -1205,12 +1121,6 @@ public void cacheBidsOpenrtbShouldNotAddTrackingImpWhenEventsNotEnabled() throws .id("impId1") .video(Video.builder().build())); - given(allBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); - given(allBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); - - given(videoCachedBidIds.getGeneratedId(any(), any(), any())).willReturn("bid1"); - given(videoCachedBidIds.getBidderForBid(any(), any())).willReturn(Optional.of("bidder")); - // when cacheService.cacheBidsOpenrtb( singletonList(bid), From bb4a22fb9b2c3697a1bee572d04efb0b45cf6b9f Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Wed, 20 Jan 2021 11:56:14 +0200 Subject: [PATCH 089/129] Fix passing ext.prebid.targeting.* fields (#1105) --- .../server/auction/AuctionRequestFactory.java | 35 ++++++++-------- .../server/auction/BidResponseCreator.java | 41 ++++++++++--------- .../auction/AuctionRequestFactoryTest.java | 26 ++++++++++++ .../test-video-appnexus-bid-request-1.json | 11 ++++- .../test-video-appnexus-bid-request-2.json | 13 +++++- 5 files changed, 84 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java index c8ddb5cdbe7..47cf8328a9b 100644 --- a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java @@ -81,8 +81,8 @@ public class AuctionRequestFactory { private static final ConditionalLogger EMPTY_ACCOUNT_LOGGER = new ConditionalLogger("empty_account", logger); private static final ConditionalLogger UNKNOWN_ACCOUNT_LOGGER = new ConditionalLogger("unknown_account", logger); - public static final String WEB_CHANNEL = "web"; - public static final String APP_CHANNEL = "app"; + private static final String WEB_CHANNEL = "web"; + private static final String APP_CHANNEL = "app"; private final long maxRequestSize; private final boolean enforceValidAccount; @@ -100,7 +100,7 @@ public class AuctionRequestFactory { private final TimeoutResolver timeoutResolver; private final TimeoutFactory timeoutFactory; private final ApplicationSettings applicationSettings; - private final IdGenerator idGenerator; + private final IdGenerator sourceIdGenerator; private final PrivacyEnforcementService privacyEnforcementService; private final JacksonMapper mapper; private final OrtbTypesResolver ortbTypesResolver; @@ -122,7 +122,7 @@ public AuctionRequestFactory(long maxRequestSize, TimeoutResolver timeoutResolver, TimeoutFactory timeoutFactory, ApplicationSettings applicationSettings, - IdGenerator idGenerator, + IdGenerator sourceIdGenerator, PrivacyEnforcementService privacyEnforcementService, JacksonMapper mapper) { @@ -143,7 +143,7 @@ public AuctionRequestFactory(long maxRequestSize, this.timeoutResolver = Objects.requireNonNull(timeoutResolver); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.applicationSettings = Objects.requireNonNull(applicationSettings); - this.idGenerator = Objects.requireNonNull(idGenerator); + this.sourceIdGenerator = Objects.requireNonNull(sourceIdGenerator); this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); this.mapper = Objects.requireNonNull(mapper); } @@ -450,7 +450,7 @@ private Site populateSite(Site site, HttpServerRequest request) { private Source populateSource(Source source) { final String tid = source != null ? source.getTid() : null; if (StringUtils.isEmpty(tid)) { - final String generatedId = idGenerator.generateId(); + final String generatedId = sourceIdGenerator.generateId(); if (StringUtils.isNotEmpty(generatedId)) { final Source.SourceBuilder builder = source != null ? source.toBuilder() : Source.builder(); return builder @@ -558,22 +558,17 @@ private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, Set impMediaTypes) { - final JsonNode priceGranularityNode = targeting.getPricegranularity(); + private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting, boolean isPriceGranularityNull, + boolean isPriceGranularityTextual, Set impMediaTypes) { final boolean hasAllMediaTypes = checkExistingMediaTypes(targeting.getMediatypepricegranularity()) .containsAll(impMediaTypes); @@ -601,6 +595,8 @@ private JsonNode populatePriceGranularity(ExtRequestTargeting targeting, boolean if (isPriceGranularityNull && !hasAllMediaTypes) { return mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)); } + + final JsonNode priceGranularityNode = targeting.getPricegranularity(); if (isPriceGranularityTextual) { final PriceGranularity priceGranularity; try { @@ -610,6 +606,7 @@ private JsonNode populatePriceGranularity(ExtRequestTargeting targeting, boolean } return mapper.mapper().valueToTree(ExtPriceGranularity.from(priceGranularity)); } + return priceGranularityNode; } @@ -776,8 +773,8 @@ private Future accountFrom(BidRequest bidRequest, Timeout timeout, Rout return blankAccountId ? responseForEmptyAccount(routingContext) : applicationSettings.getAccountById(accountId, timeout) - .compose(this::ensureAccountActive, - exception -> accountFallback(exception, accountId, routingContext)); + .compose(this::ensureAccountActive, + exception -> accountFallback(exception, accountId, routingContext)); } /** diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index 4ed980afb56..7c56ccb0724 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -102,7 +102,7 @@ public class BidResponseCreator { private final BidderCatalog bidderCatalog; private final EventsService eventsService; private final StoredRequestProcessor storedRequestProcessor; - private final IdGenerator idGenerator; + private final IdGenerator bidIdGenerator; private final int truncateAttrChars; private final Clock clock; private final JacksonMapper mapper; @@ -115,7 +115,7 @@ public BidResponseCreator(CacheService cacheService, BidderCatalog bidderCatalog, EventsService eventsService, StoredRequestProcessor storedRequestProcessor, - IdGenerator idGenerator, + IdGenerator bidIdGenerator, int truncateAttrChars, Clock clock, JacksonMapper mapper) { @@ -124,7 +124,7 @@ public BidResponseCreator(CacheService cacheService, this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.eventsService = Objects.requireNonNull(eventsService); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); - this.idGenerator = Objects.requireNonNull(idGenerator); + this.bidIdGenerator = Objects.requireNonNull(bidIdGenerator); this.truncateAttrChars = validateTruncateAttrChars(truncateAttrChars); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); @@ -175,7 +175,6 @@ private static int validateTruncateAttrChars(int truncateAttrChars) { if (truncateAttrChars < 0 || truncateAttrChars > 255) { throw new IllegalArgumentException("truncateAttrChars must be between 0 and 255"); } - return truncateAttrChars; } @@ -203,8 +202,8 @@ private Future cacheBidsAndCreateResponse(List bidd final Set winningBidsByBidder = newOrEmptySet(targeting); final GeneratedBidIds generatedBidIds = GeneratedBidIds.of(bidderResponses, - (ignored, bid) -> idGenerator.getType() != IdGeneratorType.none - ? idGenerator.generateId() + (ignored, bid) -> bidIdGenerator.getType() != IdGeneratorType.none + ? bidIdGenerator.generateId() : bid.getId()); // determine winning bids only if targeting is present @@ -313,7 +312,7 @@ private static BidderBid mostValuableBid(List bidderBids) { .filter(bidderBid -> StringUtils.isNotBlank(bidderBid.getBid().getDealid())) .collect(Collectors.toList()); - List processedBidderBids = dealBidderBids.isEmpty() ? bidderBids : dealBidderBids; + final List processedBidderBids = dealBidderBids.isEmpty() ? bidderBids : dealBidderBids; return processedBidderBids.stream() .max(Comparator.comparing(bidderBid -> bidderBid.getBid().getPrice(), Comparator.naturalOrder())) @@ -448,16 +447,16 @@ private static boolean isValidForCaching(Bid bid) { return bid.getDealid() != null ? price.compareTo(BigDecimal.ZERO) >= 0 : price.compareTo(BigDecimal.ZERO) > 0; } - private GeneratedBidIds getGeneratedVideoBidIds( - List bidderResponses, - GeneratedBidIds generatedBidIds, - List imps) { + private GeneratedBidIds getGeneratedVideoBidIds(List bidderResponses, + GeneratedBidIds generatedBidIds, + List imps) { final List vastModifyAllowedResponses = bidderResponses.stream() .filter(bidderResponse -> bidderCatalog.isModifyingVastXmlAllowed(bidderResponse.getBidder())) .map(bidderResponse -> makeVideoBidsBidderResponse(bidderResponse, imps)) .filter(Objects::nonNull) .collect(Collectors.toList()); + return GeneratedBidIds.of(vastModifyAllowedResponses, (bidder, bid) -> generatedBidIds.getGeneratedId(bidder, bid.getId(), bid.getImpid())); } @@ -466,7 +465,9 @@ private static BidderResponse makeVideoBidsBidderResponse(BidderResponse bidderR final List videoBidderBids = bidderResponse.getSeatBid().getBids().stream() .filter(bidderBid -> isVideoBid(bidderBid, imps)) .collect(Collectors.toList()); + final BidderSeatBid bidderSeatBid = bidderResponse.getSeatBid(); + return CollectionUtils.isNotEmpty(videoBidderBids) ? BidderResponse.of( bidderResponse.getBidder(), @@ -540,11 +541,10 @@ private Map> toExtBidderErrors(List CacheServiceResult cacheResult, VideoStoredDataResult videoStoredDataResult, Map> bidErrors) { - final BidRequest bidRequest = auctionContext.getBidRequest(); final Map> errors = new HashMap<>(); errors.putAll(extractBidderErrors(bidderResponses)); - errors.putAll(extractDeprecatedBiddersErrors(bidRequest)); + errors.putAll(extractDeprecatedBiddersErrors(auctionContext.getBidRequest())); errors.putAll(extractPrebidErrors(videoStoredDataResult, auctionContext)); errors.putAll(extractCacheErrors(cacheResult)); if (MapUtils.isNotEmpty(bidErrors)) { @@ -880,7 +880,7 @@ private Bid toBid(BidderBid bidderBid, final ExtBidPrebidVideo extBidPrebidVideo = getExtBidPrebidVideo(bid.getExt()); final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder() - .bidid(idGenerator.getType() != IdGeneratorType.none ? generatedBidId : null) + .bidid(bidIdGenerator.getType() != IdGeneratorType.none ? generatedBidId : null) .type(bidType) .targeting(targetingKeywords) .cache(cache) @@ -1014,8 +1014,9 @@ private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, boolean isApp, BidRequest bidRequest, Account account) { - final Map keywordsCreatorByBidType = - keywordsCreatorByBidType(targeting, isApp, bidRequest, account); + + final Map keywordsCreatorByBidType = keywordsCreatorByBidType(targeting, + isApp, bidRequest, account); return keywordsCreatorByBidType.getOrDefault(bidType, keywordsCreator(targeting, isApp, bidRequest, account)); } @@ -1024,8 +1025,10 @@ private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, * Extracts targeting keywords settings from the bid request and creates {@link TargetingKeywordsCreator} * instance if it is present. */ - private TargetingKeywordsCreator keywordsCreator( - ExtRequestTargeting targeting, boolean isApp, BidRequest bidRequest, Account account) { + private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, + boolean isApp, + BidRequest bidRequest, + Account account) { final JsonNode priceGranularityNode = targeting.getPricegranularity(); return priceGranularityNode == null || priceGranularityNode.isNull() @@ -1043,7 +1046,6 @@ private Map keywordsCreatorByBidType(ExtReque Account account) { final ExtMediaTypePriceGranularity mediaTypePriceGranularity = targeting.getMediatypepricegranularity(); - if (mediaTypePriceGranularity == null) { return Collections.emptyMap(); } @@ -1127,7 +1129,6 @@ private CacheAsset toCacheAsset(String cacheId) { private String integrationFrom(AuctionContext auctionContext) { final ExtRequest extRequest = auctionContext.getBidRequest().getExt(); final ExtRequestPrebid prebid = extRequest == null ? null : extRequest.getPrebid(); - return prebid != null ? prebid.getIntegration() : null; } diff --git a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java index 527f8fd55a1..4fffa12ae2a 100644 --- a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java @@ -47,6 +47,7 @@ import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.model.Privacy; import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; @@ -1140,6 +1141,31 @@ public void shouldNotChangeAnyOtherExtRequestPrebidCacheFields() { .containsOnly(tuple(cacheBids, cacheVastxml)); } + @Test + public void shouldNotChangeAnyOtherExtRequestPrebidTargetingFields() { + // given + givenBidRequest(BidRequest.builder() + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .includebrandcategory(ExtIncludeBrandCategory.of(1, "publisher", true)) + .truncateattrchars(10) + .build()) + .build())) + .build()); + + // when + final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest(); + + // then + assertThat(singletonList(request)) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getTargeting) + .extracting(ExtRequestTargeting::getIncludebrandcategory, ExtRequestTargeting::getTruncateattrchars) + .containsOnly(tuple(ExtIncludeBrandCategory.of(1, "publisher", true), 10)); + } + @Test public void shouldSetCacheWinningonlyFromRequestWhenCacheWinningonlyIsPresent() { // given diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json index e920fd3d553..0831b9c42ab 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json @@ -297,7 +297,12 @@ "precision": 2 }, "includewinners": true, - "includebidderkeys": true + "includebidderkeys": true, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "with_category": true + } }, "cache": { "vastxml": {} @@ -305,6 +310,10 @@ "channel": { "name": "web" } + }, + "appnexus": { + "include_brand_category": true, + "brand_category_uniqueness": true } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json index bbe5da789b4..aacd75194eb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json @@ -90,7 +90,12 @@ "precision": 2 }, "includewinners": true, - "includebidderkeys": true + "includebidderkeys": true, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "with_category": true + } }, "cache": { "vastxml": {} @@ -98,6 +103,10 @@ "channel": { "name": "web" } + }, + "appnexus": { + "include_brand_category": true, + "brand_category_uniqueness": true } } -} \ No newline at end of file +} From b31e5f2cfce5e8a591b4b31b56f5f11e3e903eff Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Wed, 20 Jan 2021 16:29:51 +0200 Subject: [PATCH 090/129] Fix passing ext.prebid.targeting.* fields for AMP (#1108) --- .../server/auction/AmpRequestFactory.java | 2 +- .../server/auction/AmpRequestFactoryTest.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/auction/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/AmpRequestFactory.java index 4b7a3130c41..ee60140194e 100644 --- a/src/main/java/org/prebid/server/auction/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AmpRequestFactory.java @@ -636,7 +636,7 @@ private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) final Boolean includeFormat = !isTargetingNull ? targeting.getIncludeformat() : null; - return ExtRequestTargeting.builder() + return (isTargetingNull ? ExtRequestTargeting.builder() : targeting.toBuilder()) .pricegranularity(outgoingPriceGranularityNode) .mediatypepricegranularity(mediaTypePriceGranularity) .includewinners(includeWinners) diff --git a/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java index 864a85ce91b..dc4e75455ca 100644 --- a/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java @@ -26,6 +26,7 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.metric.MetricName; +import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; @@ -398,6 +399,29 @@ public void shouldReturnBidRequestWithDefaultPriceGranularityIfStoredBidRequestE ExtGranularityRange.of(BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))))))); } + @Test + public void shouldReturnBidRequestWithNotChangedExtRequestPrebidTargetingFields() { + // given + givenBidRequest( + builder -> builder + .ext(givenRequestExt(ExtRequestTargeting.builder() + .includebrandcategory(ExtIncludeBrandCategory.of(1, "publisher", true)) + .truncateattrchars(10) + .build())), + Imp.builder().build()); + + // when + final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest(); + + // then + assertThat(singletonList(request)) + .extracting(BidRequest::getExt).isNotNull() + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getTargeting) + .extracting(ExtRequestTargeting::getIncludebrandcategory, ExtRequestTargeting::getTruncateattrchars) + .containsOnly(tuple(ExtIncludeBrandCategory.of(1, "publisher", true), 10)); + } + private Answer answerWithFirstArgument() { return invocationOnMock -> invocationOnMock.getArguments()[0]; } From 0567edbe7cf3947e6e0815100e7bc67ec059bb82 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Thu, 21 Jan 2021 12:02:47 +0200 Subject: [PATCH 091/129] Prebid Server prepare release 1.53.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index deb13eaf7a4..b0ecefbb78f 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.53.0-SNAPSHOT + 1.53.0 prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 1.53.0 From 49fab79265f0402ab3a5f5fecb2ed898c826cacf Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Thu, 21 Jan 2021 12:03:01 +0200 Subject: [PATCH 092/129] Prebid Server prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b0ecefbb78f..d702f3ccebf 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.53.0 + 1.54.0-SNAPSHOT prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - 1.53.0 + HEAD From 5dc8902fbd3cfa168aadcafacf27eed86be82a91 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Thu, 21 Jan 2021 15:15:39 +0200 Subject: [PATCH 093/129] Updating contact info for adprime (#1110) --- src/main/resources/bidder-config/adprime.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/bidder-config/adprime.yaml b/src/main/resources/bidder-config/adprime.yaml index 5c72a95a685..5ac2c4c0eca 100644 --- a/src/main/resources/bidder-config/adprime.yaml +++ b/src/main/resources/bidder-config/adprime.yaml @@ -8,7 +8,7 @@ adapters: deprecated-names: aliases: meta-info: - maintainer-email: rafal@adprime.com + maintainer-email: prebid@adprime.com app-media-types: - banner - video From d4b4dc39081e300e201c1747371deb7aa2a611d0 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Thu, 21 Jan 2021 15:16:32 +0200 Subject: [PATCH 094/129] Update ucfunnel endpoint (#1111) --- src/main/resources/bidder-config/ucfunnel.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/bidder-config/ucfunnel.yaml b/src/main/resources/bidder-config/ucfunnel.yaml index 3f011765857..acec14ae741 100644 --- a/src/main/resources/bidder-config/ucfunnel.yaml +++ b/src/main/resources/bidder-config/ucfunnel.yaml @@ -1,7 +1,7 @@ adapters: ucfunnel: enabled: false - endpoint: http://pbs.aralego.com/prebid + endpoint: https://pbs.aralego.com/prebid pbs-enforces-gdpr: true pbs-enforces-ccpa: true modifying-vast-xml-allowed: true From 936ff1ea111b365727a9d3a5a14c676fe46c72c4 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Thu, 21 Jan 2021 16:19:28 +0200 Subject: [PATCH 095/129] Drop duplicated targeting keys (#910) --- .../server/auction/TargetingKeywordsCreator.java | 3 ++- .../auction/TargetingKeywordsCreatorTest.java | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java index 1ddc9b9221a..663190052dc 100644 --- a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java +++ b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java @@ -299,7 +299,8 @@ private static String sizeFrom(Integer width, Integer height) { private Map truncateKeys(Map keyValues) { return truncateAttrChars > 0 ? keyValues.entrySet().stream() - .collect(Collectors.toMap(keyValue -> truncateKey(keyValue.getKey()), Map.Entry::getValue)) + .collect(Collectors + .toMap(keyValue -> truncateKey(keyValue.getKey()), Map.Entry::getValue, (key1, key2) -> key1)) : keyValues; } diff --git a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java index 773006fd3f8..020d30c5a9e 100644 --- a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java @@ -439,6 +439,21 @@ public void shouldTruncateTargetingWithoutBidderSuffixKeywordsIfTruncateAttrChar .containsKeys("hb_bidd", "hb_pb"); } + @Test + public void shouldTruncateTargetingAndDropDuplicatedWhenTruncateIsTooShort() { + // given + final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build(); + + // when + final Map keywords = TargetingKeywordsCreator.create(null, true, true, true, 6) + .makeFor(bid, "bidder", true, null, null, null); + + // then + // Without truncating: "hb_bidder", "hb_bidder_bidder", "hb_env", "hb_env_bidder", "hb_pb", "hb_pb_bidder" + assertThat(keywords).hasSize(4) + .containsKeys("hb_bid", "hb_env", "hb_pb", "hb_pb_"); + } + @Test public void shouldNotTruncateTargetingKeywordsIfTruncateAttrCharsIsNotDefined() { // given From 6ebb602c718bef9f1c66460678dca87975c47e5a Mon Sep 17 00:00:00 2001 From: bretg Date: Thu, 21 Jan 2021 11:36:17 -0500 Subject: [PATCH 096/129] adding status column to account table doc (#1112) --- docs/application-settings.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/application-settings.md b/docs/application-settings.md index a6132a997f4..0f2ed0076c3 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -187,7 +187,7 @@ Prebid Server returns expected data in the expected order. Here's an example con settings: database: type: mysql - account-query: SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, bid_validations FROM accounts_account where uuid = ? LIMIT 1 + account-query: SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, bid_validations, status FROM accounts_account where uuid = ? LIMIT 1 ``` The SQL query for account must: @@ -203,6 +203,7 @@ The SQL query for account must: * maximum targeting attribute size, integer * default integration value, string * analytics configuration, JSON string, see below + * status, string. Expected values: "active", "inactive", NULL. Only "inactive" has any effect and only when settings.enforce-valid-account is on. * specify a special single `%ACCOUNT_ID%` placeholder in the `WHERE` clause that will be replaced with account ID in runtime From dfe95721b7c4a6dde30e7c5a7b2f5fb9b304e75c Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Fri, 22 Jan 2021 12:47:11 +0200 Subject: [PATCH 097/129] Fix broken test (#1114) --- .../org/prebid/server/auction/TargetingKeywordsCreatorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java index 020d30c5a9e..f1618347f69 100644 --- a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java @@ -445,7 +445,7 @@ public void shouldTruncateTargetingAndDropDuplicatedWhenTruncateIsTooShort() { final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build(); // when - final Map keywords = TargetingKeywordsCreator.create(null, true, true, true, 6) + final Map keywords = TargetingKeywordsCreator.create(null, true, true, true, true, 6) .makeFor(bid, "bidder", true, null, null, null); // then From 3d8b15594c3bf20a4fa231fbd81b338efa1cbc19 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Fri, 22 Jan 2021 14:52:32 +0200 Subject: [PATCH 098/129] Validate that schains do not contain duplicate bidders (#816) --- .../server/auction/ExchangeService.java | 18 ++++------- .../server/validation/RequestValidator.java | 32 +++++++++++++++++++ .../server/auction/ExchangeServiceTest.java | 30 ----------------- .../validation/RequestValidatorTest.java | 22 +++++++++++++ 4 files changed, 60 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 11f21e978d7..bc10f83384f 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -544,17 +544,10 @@ private static Map bidderToPrebidSchains(E } final Map bidderToPrebidSchains = new HashMap<>(); - for (ExtRequestPrebidSchain schain : schains) { - final List bidders = schain.getBidders(); - if (CollectionUtils.isNotEmpty(bidders)) { - for (String bidder : bidders) { - if (bidderToPrebidSchains.containsKey(bidder)) { - bidderToPrebidSchains.remove(bidder); - logger.debug("Schain bidder {0} is rejected since it was defined more than once", bidder); - continue; - } - bidderToPrebidSchains.put(bidder, schain.getSchain()); - } + for (final ExtRequestPrebidSchain schain : schains) { + final List schainBidders = schain.getBidders(); + if (CollectionUtils.isNotEmpty(schainBidders)) { + schainBidders.forEach(bidder -> bidderToPrebidSchains.put(bidder, schain.getSchain())); } } return bidderToPrebidSchains; @@ -791,7 +784,8 @@ private List updateRequestMetric(List bidderReques private static BigDecimal bidAdjustmentForBidder(BidRequest bidRequest, String bidder) { final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); - final Map bidAdjustmentFactors = prebid != null ? prebid.getBidadjustmentfactors() : null; + final Map bidAdjustmentFactors = + prebid != null ? prebid.getBidadjustmentfactors() : null; return bidAdjustmentFactors != null ? bidAdjustmentFactors.get(bidder) : null; } diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java index 526c7114a41..f6bfb83cd11 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -45,6 +45,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -59,6 +60,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -129,6 +131,7 @@ public ValidationResult validate(BidRequest bidRequest) { validateBidAdjustmentFactors( ObjectUtils.defaultIfNull(extRequestPrebid.getBidadjustmentfactors(), Collections.emptyMap()), aliases); + validateSchains(extRequestPrebid.getSchains()); } if (CollectionUtils.isEmpty(bidRequest.getImp())) { @@ -202,6 +205,35 @@ private void validateBidAdjustmentFactors(Map adjustmentFact } } + private void validateSchains(List schains) throws ValidationException { + if (schains == null) { + return; + } + + final Set schainBidders = new HashSet<>(); + for (final ExtRequestPrebidSchain schain : schains) { + if (schain == null) { + continue; + } + + final List bidders = schain.getBidders(); + if (bidders == null) { + continue; + } + + for (final String bidder : bidders) { + if (schainBidders.contains(bidder)) { + throw new ValidationException( + "request.ext.prebid.schains contains multiple schains for bidder %s; " + + "it must contain no more than one per bidder.", + bidder); + } + + schainBidders.add(bidder); + } + } + } + private boolean isUnknownBidderOrAlias(String bidder, Map aliases) { return !bidderCatalog.isValidName(bidder) && !aliases.containsKey(bidder); } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index fae65f3d4d0..b9cfd04e0b8 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -547,36 +547,6 @@ public void shouldPassRequestWithInjectedSchainInSourceExt() { assertThat(capturedBidRequest3.getExt().getPrebid().getSchains()).isNull(); } - @Test - public void shouldRejectDuplicatedSchainBidders() { - // given - final String bidder1 = "bidder"; - final String bidder2 = "bidder"; // same name - - final ExtRequestPrebidSchain schainForBidder1 = ExtRequestPrebidSchain.of( - singletonList(bidder1), ExtRequestPrebidSchainSchain.of("ver1", null, null, null)); - final ExtRequestPrebidSchain schainForBidder2 = ExtRequestPrebidSchain.of( - singletonList(bidder2), ExtRequestPrebidSchainSchain.of("ver2", null, null, null)); - - final ExtRequest extRequest = ExtRequest.of( - ExtRequestPrebid.builder() - .schains(asList(schainForBidder1, schainForBidder2)) - .build()); - - final BidRequest bidRequest = givenBidRequest(asList( - givenImp(singletonMap(bidder1, 1), identity()), - givenImp(singletonMap(bidder2, 2), identity())), - builder -> builder.ext(extRequest)); - - // when - exchangeService.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class); - verify(httpBidderRequester).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean()); - assertThat(bidRequestCaptor.getValue().getSource()).isNull(); - } - @Test public void shouldReturnFailedFutureWithUnchangedMessageWhenPrivacyEnforcementServiceFails() { // given diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index ee4e722de52..6b9544862ba 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -45,6 +45,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -2441,6 +2442,27 @@ public void validateShouldEmptyValidationMessagesWhenBidderIsKnownAliasForCoreBi assertThat(result.getErrors()).isEmpty(); } + @Test + public void validateShouldReturnValidationMessageWhenMultipleSchainsForSameBidder() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of( + ExtRequestPrebid.builder() + .schains(asList( + ExtRequestPrebidSchain.of(asList("bidder1", "bidder2"), null), + ExtRequestPrebidSchain.of(asList("bidder2", "bidder3"), null))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()) + .containsOnly("request.ext.prebid.schains contains multiple schains for bidder bidder2; " + + "it must contain no more than one per bidder."); + } + @Test public void validateShouldReturnValidationMessageWhenRequestHaveDuplicatedImpIds() { // given From 532919510f79d1a5fcf3799b1e8484e2cc29d5d9 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Fri, 22 Jan 2021 15:40:58 +0200 Subject: [PATCH 099/129] Add support for host defined schain node (#1102) * Extract schain resolving into separate component * Inject global schain node if defined in configuration * Update documentation --- docs/config-app.md | 1 + .../server/auction/ExchangeService.java | 57 +++----- .../prebid/server/auction/SchainResolver.java | 101 +++++++++++++ .../spring/config/ServiceConfiguration.java | 11 ++ src/main/resources/application.yaml | 1 + .../server/auction/ExchangeServiceTest.java | 55 +++---- .../server/auction/SchainResolverTest.java | 136 ++++++++++++++++++ 7 files changed, 301 insertions(+), 61 deletions(-) create mode 100644 src/main/java/org/prebid/server/auction/SchainResolver.java create mode 100644 src/test/java/org/prebid/server/auction/SchainResolverTest.java diff --git a/docs/config-app.md b/docs/config-app.md index 229d8bba283..7d59a920ed9 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -81,6 +81,7 @@ Removes and downloads file again if depending service cant process probably corr - `auction.generate-source-tid` - whether to generate bidrequest.source.tid in the OpenRTB request. - `auction.validations.banner-creative-max-size` - enables creative max size validation for banners. Possible values: `skip`, `enforce`, `warn`. Default is `skip`. - `auction.validations.secure-markup` - enables secure markup validation. Possible values: `skip`, `enforce`, `warn`. Default is `skip`. +- `auction.host-schain-node` - defines global schain node that will be appended to `request.source.ext.schain.nodes` passed to bidders ## Amp (OpenRTB) - `amp.default-timeout-ms` - default operation timeout for OpenRTB Amp requests. diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index bc10f83384f..d2abfecdffd 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -82,7 +82,6 @@ public class ExchangeService { private static final String CONTEXT_EXT = "context"; private static final String DATA = "data"; private static final String ALL_BIDDERS_CONFIG = "*"; - private static final String GENERIC_SCHAIN_KEY = "*"; private static final BigDecimal THOUSAND = BigDecimal.valueOf(1000); @@ -91,6 +90,7 @@ public class ExchangeService { private final StoredResponseProcessor storedResponseProcessor; private final PrivacyEnforcementService privacyEnforcementService; private final FpdResolver fpdResolver; + private final SchainResolver schainResolver; private final HttpBidderRequester httpBidderRequester; private final ResponseBidValidator responseBidValidator; private final CurrencyConversionService currencyService; @@ -105,6 +105,7 @@ public ExchangeService(long expectedCacheTime, StoredResponseProcessor storedResponseProcessor, PrivacyEnforcementService privacyEnforcementService, FpdResolver fpdResolver, + SchainResolver schainResolver, HttpBidderRequester httpBidderRequester, ResponseBidValidator responseBidValidator, CurrencyConversionService currencyService, @@ -122,6 +123,7 @@ public ExchangeService(long expectedCacheTime, this.storedResponseProcessor = Objects.requireNonNull(storedResponseProcessor); this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); this.fpdResolver = Objects.requireNonNull(fpdResolver); + this.schainResolver = Objects.requireNonNull(schainResolver); this.httpBidderRequester = Objects.requireNonNull(httpBidderRequester); this.responseBidValidator = Objects.requireNonNull(responseBidValidator); this.currencyService = Objects.requireNonNull(currencyService); @@ -495,15 +497,18 @@ private List getBidderRequests(List bidderPr List imps, Map biddersToConfigs) { - final ExtRequest requestExt = bidRequest.getExt(); - final Map bidderToPrebidBidders = bidderToPrebidBidders(requestExt); - final Map bidderToPrebidSchains = bidderToPrebidSchains(requestExt); + final Map bidderToPrebidBidders = bidderToPrebidBidders(bidRequest); + final List bidderRequests = bidderPrivacyResults.stream() // for each bidder create a new request that is a copy of original request except buyerid, imp // extensions, ext.prebid.data.bidders and ext.prebid.bidders. // Also, check whether to pass user.ext.data, app.ext.data and site.ext.data or not. - .map(bidderPrivacyResult -> createBidderRequest(bidderPrivacyResult, bidRequest, imps, - biddersToConfigs, bidderToPrebidBidders, bidderToPrebidSchains)) + .map(bidderPrivacyResult -> createBidderRequest( + bidderPrivacyResult, + bidRequest, + imps, + biddersToConfigs, + bidderToPrebidBidders)) .filter(Objects::nonNull) .collect(Collectors.toList()); @@ -515,7 +520,8 @@ private List getBidderRequests(List bidderPr /** * Extracts a map of bidders to their arguments from {@link ObjectNode} prebid.bidders. */ - private static Map bidderToPrebidBidders(ExtRequest requestExt) { + private static Map bidderToPrebidBidders(BidRequest bidRequest) { + final ExtRequest requestExt = bidRequest.getExt(); final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); final ObjectNode bidders = prebid == null ? null : prebid.getBidders(); @@ -532,27 +538,6 @@ private static Map bidderToPrebidBidders(ExtRequest requestExt return bidderToPrebidParameters; } - /** - * Extracts a map of bidders to their arguments from {@link ObjectNode} prebid.schains. - */ - private static Map bidderToPrebidSchains(ExtRequest requestExt) { - final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); - final List schains = prebid == null ? null : prebid.getSchains(); - - if (schains == null || schains.isEmpty()) { - return Collections.emptyMap(); - } - - final Map bidderToPrebidSchains = new HashMap<>(); - for (final ExtRequestPrebidSchain schain : schains) { - final List schainBidders = schain.getBidders(); - if (CollectionUtils.isNotEmpty(schainBidders)) { - schainBidders.forEach(bidder -> bidderToPrebidSchains.put(bidder, schain.getSchain())); - } - } - return bidderToPrebidSchains; - } - /** * Returns {@link BidderRequest} for the given bidder. */ @@ -560,8 +545,7 @@ private BidderRequest createBidderRequest(BidderPrivacyResult bidderPrivacyResul BidRequest bidRequest, List imps, Map biddersToConfigs, - Map bidderToPrebidBidders, - Map bidderToPrebidSchains) { + Map bidderToPrebidBidders) { final String bidder = bidderPrivacyResult.getRequestBidder(); if (bidderPrivacyResult.isBlockedRequestByTcf()) { @@ -592,7 +576,7 @@ private BidderRequest createBidderRequest(BidderPrivacyResult bidderPrivacyResul .imp(prepareImps(bidder, imps, useFirstPartyData)) .app(prepareApp(bidRequestApp, fpdApp, useFirstPartyData)) .site(prepareSite(bidRequestSite, fpdSite, useFirstPartyData)) - .source(prepareSource(bidder, bidderToPrebidSchains, bidRequest.getSource())) + .source(prepareSource(bidder, bidRequest)) .ext(prepareExt(bidder, bidderToPrebidBidders, bidRequest.getExt())) .build()); } @@ -700,10 +684,10 @@ private ExtSite maskExtSite(ExtSite siteExt) { /** * Returns {@link Source} with corresponding request.ext.prebid.schains. */ - private Source prepareSource(String bidder, Map bidderToSchain, - Source receivedSource) { - final ExtRequestPrebidSchainSchain defaultSchain = bidderToSchain.get(GENERIC_SCHAIN_KEY); - final ExtRequestPrebidSchainSchain bidderSchain = bidderToSchain.getOrDefault(bidder, defaultSchain); + private Source prepareSource(String bidder, BidRequest bidRequest) { + final Source receivedSource = bidRequest.getSource(); + + final ExtRequestPrebidSchainSchain bidderSchain = schainResolver.resolveForBidder(bidder, bidRequest); if (bidderSchain == null) { return receivedSource; @@ -784,8 +768,7 @@ private List updateRequestMetric(List bidderReques private static BigDecimal bidAdjustmentForBidder(BidRequest bidRequest, String bidder) { final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); - final Map bidAdjustmentFactors = - prebid != null ? prebid.getBidadjustmentfactors() : null; + final Map bidAdjustmentFactors = prebid != null ? prebid.getBidadjustmentfactors() : null; return bidAdjustmentFactors != null ? bidAdjustmentFactors.get(bidder) : null; } diff --git a/src/main/java/org/prebid/server/auction/SchainResolver.java b/src/main/java/org/prebid/server/auction/SchainResolver.java new file mode 100644 index 00000000000..755537932ec --- /dev/null +++ b/src/main/java/org/prebid/server/auction/SchainResolver.java @@ -0,0 +1,101 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchain; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchainNode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class SchainResolver { + + private static final Logger logger = LoggerFactory.getLogger(SchainResolver.class); + + private final ExtRequestPrebidSchainSchainNode globalNode; + + private SchainResolver(ExtRequestPrebidSchainSchainNode globalNode) { + this.globalNode = globalNode; + } + + public static SchainResolver create(String globalNodeString, JacksonMapper mapper) { + return new SchainResolver(globalNodeOrNull(globalNodeString, Objects.requireNonNull(mapper))); + } + + public ExtRequestPrebidSchainSchain resolveForBidder(String bidder, BidRequest bidRequest) { + final ExtRequest requestExt = bidRequest.getExt(); + final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); + final List schains = prebid == null ? null : prebid.getSchains(); + + ExtRequestPrebidSchainSchain bidderSchain = null; + ExtRequestPrebidSchainSchain catchAllSchain = null; + for (final ExtRequestPrebidSchain schain : ListUtils.emptyIfNull(schains)) { + catchAllSchain = existingSchainOrNull("*", catchAllSchain, schain); + bidderSchain = existingSchainOrNull(bidder, bidderSchain, schain); + } + + return enrich(ObjectUtils.defaultIfNull(bidderSchain, catchAllSchain)); + } + + private static ExtRequestPrebidSchainSchainNode globalNodeOrNull(String globalNodeString, JacksonMapper mapper) { + if (StringUtils.isBlank(globalNodeString)) { + return null; + } + + try { + return mapper.decodeValue(globalNodeString, ExtRequestPrebidSchainSchainNode.class); + } catch (DecodeException e) { + throw new IllegalArgumentException("Exception occurred while parsing global schain node", e); + } + } + + private ExtRequestPrebidSchainSchain existingSchainOrNull(String bidder, + ExtRequestPrebidSchainSchain existingSchain, + ExtRequestPrebidSchain schainEntry) { + + if (schainEntry == null + || CollectionUtils.isEmpty(schainEntry.getBidders()) + || !schainEntry.getBidders().contains(bidder)) { + + return existingSchain; + } + + if (existingSchain != null) { + logger.debug("Schain bidder {0} is rejected since it was defined more than once", bidder); + return null; + } + + return schainEntry.getSchain(); + } + + private ExtRequestPrebidSchainSchain enrich(ExtRequestPrebidSchainSchain schain) { + if (globalNode == null) { + return schain; + } + + if (schain == null) { + return ExtRequestPrebidSchainSchain.of(null, null, Collections.singletonList(globalNode), null); + } + + final List nodes = new ArrayList<>(ListUtils.emptyIfNull(schain.getNodes())); + nodes.add(globalNode); + + return ExtRequestPrebidSchainSchain.of( + schain.getVer(), + schain.getComplete(), + nodes, + schain.getExt()); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 6e1082bd27c..e02d861c203 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -19,6 +19,7 @@ import org.prebid.server.auction.OrtbTypesResolver; import org.prebid.server.auction.PreBidRequestContextFactory; import org.prebid.server.auction.PrivacyEnforcementService; +import org.prebid.server.auction.SchainResolver; import org.prebid.server.auction.StoredRequestProcessor; import org.prebid.server.auction.StoredResponseProcessor; import org.prebid.server.auction.TimeoutResolver; @@ -134,6 +135,14 @@ OrtbTypesResolver ortbTypesResolver(JacksonMapper jacksonMapper, JsonMerger json return new OrtbTypesResolver(jacksonMapper, jsonMerger); } + @Bean + SchainResolver schainResolver( + @Value("${auction.host-schain-node}") String globalSchainNode, + JacksonMapper mapper) { + + return SchainResolver.create(globalSchainNode, mapper); + } + @Bean TimeoutResolver timeoutResolver( @Value("${default-timeout-ms}") long defaultTimeout, @@ -483,6 +492,7 @@ ExchangeService exchangeService( StoredResponseProcessor storedResponseProcessor, PrivacyEnforcementService privacyEnforcementService, FpdResolver fpdResolver, + SchainResolver schainResolver, HttpBidderRequester httpBidderRequester, ResponseBidValidator responseBidValidator, CurrencyConversionService currencyConversionService, @@ -498,6 +508,7 @@ ExchangeService exchangeService( storedResponseProcessor, privacyEnforcementService, fpdResolver, + schainResolver, httpBidderRequester, responseBidValidator, currencyConversionService, diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 233ec848976..3f6b4683ff5 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -90,6 +90,7 @@ auction: validations: banner-creative-max-size: skip secure-markup: skip + host-schain-node: video: stored-request-required: false stored-requests-timeout-ms: 90 diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index b9cfd04e0b8..2e6f753899f 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -71,7 +71,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchainNode; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtSource; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; @@ -142,6 +141,8 @@ public class ExchangeServiceTest extends VertxTest { @Mock private FpdResolver fpdResolver; @Mock + private SchainResolver schainResolver; + @Mock private HttpBidderRequester httpBidderRequester; @Mock private ResponseBidValidator responseBidValidator; @@ -188,6 +189,8 @@ public void setUp() { given(fpdResolver.resolveSite(any(), any())).willAnswer(invocation -> invocation.getArgument(0)); given(fpdResolver.resolveApp(any(), any())).willAnswer(invocation -> invocation.getArgument(0)); + given(schainResolver.resolveForBidder(anyString(), any())).willReturn(null); + given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); given(usersyncer.getCookieFamilyName()).willReturn("cookieFamily"); @@ -208,6 +211,7 @@ public void setUp() { storedResponseProcessor, privacyEnforcementService, fpdResolver, + schainResolver, httpBidderRequester, responseBidValidator, currencyService, @@ -227,6 +231,7 @@ public void creationShouldFailOnNegativeExpectedCacheTime() { storedResponseProcessor, privacyEnforcementService, fpdResolver, + schainResolver, httpBidderRequester, responseBidValidator, currencyService, @@ -490,33 +495,35 @@ public void shouldPassRequestWithInjectedSchainInSourceExt() { givenBidder(bidder2Name, bidder2, givenEmptySeatBid()); givenBidder(bidder3Name, bidder3, givenEmptySeatBid()); - final ObjectNode schainExtObjectNode = mapper.createObjectNode().put("any", "any"); - final ExtRequestPrebidSchainSchainNode specificNodes = ExtRequestPrebidSchainSchainNode.of("asi", "sid", 1, - "rid", "name", "domain", schainExtObjectNode); - final ExtRequestPrebidSchainSchain specificSchain = ExtRequestPrebidSchainSchain.of("ver", 1, - singletonList(specificNodes), schainExtObjectNode); + final ExtRequestPrebidSchainSchainNode specificNodes = ExtRequestPrebidSchainSchainNode.of( + "asi", "sid", 1, "rid", "name", "domain", null); + final ExtRequestPrebidSchainSchain specificSchain = ExtRequestPrebidSchainSchain.of( + "ver", 1, singletonList(specificNodes), null); final ExtRequestPrebidSchain schainForBidders = ExtRequestPrebidSchain.of( - asList(bidder1Name, bidder2Name), - specificSchain); - final ExtRequestPrebidSchainSchain allSchainObject = ExtRequestPrebidSchainSchain.of("ver", 1, - singletonList(specificNodes), schainExtObjectNode); - final ExtRequestPrebidSchainSchainNode generalNodes = ExtRequestPrebidSchainSchainNode.of("t", null, 0, "a", - null, "ads", null); - final ExtRequestPrebidSchainSchain generalSchain = ExtRequestPrebidSchainSchain.of("t", 123, - singletonList(generalNodes), null); + asList(bidder1Name, bidder2Name), specificSchain); + + final ExtRequestPrebidSchainSchainNode generalNodes = ExtRequestPrebidSchainSchainNode.of( + "t", null, 0, "a", null, "ads", null); + final ExtRequestPrebidSchainSchain generalSchain = ExtRequestPrebidSchainSchain.of( + "t", 123, singletonList(generalNodes), null); final ExtRequestPrebidSchain allSchain = ExtRequestPrebidSchain.of(singletonList("*"), generalSchain); + final ExtRequest extRequest = ExtRequest.of( ExtRequestPrebid.builder() .schains(asList(schainForBidders, allSchain)) .auctiontimestamp(1000L) .build()); - - final BidRequest bidRequest = givenBidRequest(asList( - givenImp(singletonMap(bidder1Name, 1), identity()), - givenImp(singletonMap(bidder2Name, 2), identity()), - givenImp(singletonMap(bidder3Name, 3), identity())), + final BidRequest bidRequest = givenBidRequest( + asList( + givenImp(singletonMap(bidder1Name, 1), identity()), + givenImp(singletonMap(bidder2Name, 2), identity()), + givenImp(singletonMap(bidder3Name, 3), identity())), builder -> builder.ext(extRequest)); + given(schainResolver.resolveForBidder(eq("bidder1"), same(bidRequest))).willReturn(specificSchain); + given(schainResolver.resolveForBidder(eq("bidder2"), same(bidRequest))).willReturn(specificSchain); + given(schainResolver.resolveForBidder(eq("bidder3"), same(bidRequest))).willReturn(generalSchain); + // when exchangeService.holdAuction(givenRequestContext(bidRequest)); @@ -524,8 +531,7 @@ public void shouldPassRequestWithInjectedSchainInSourceExt() { final ArgumentCaptor bidRequest1Captor = ArgumentCaptor.forClass(BidRequest.class); verify(httpBidderRequester).requestBids(same(bidder1), bidRequest1Captor.capture(), any(), anyBoolean()); final BidRequest capturedBidRequest1 = bidRequest1Captor.getValue(); - ExtSource extSource = capturedBidRequest1.getSource().getExt(); - ExtRequestPrebidSchainSchain requestSchain1 = extSource.getSchain(); + final ExtRequestPrebidSchainSchain requestSchain1 = capturedBidRequest1.getSource().getExt().getSchain(); assertThat(requestSchain1).isNotNull(); assertThat(requestSchain1).isEqualTo(specificSchain); assertThat(capturedBidRequest1.getExt().getPrebid().getSchains()).isNull(); @@ -533,7 +539,7 @@ public void shouldPassRequestWithInjectedSchainInSourceExt() { final ArgumentCaptor bidRequest2Captor = ArgumentCaptor.forClass(BidRequest.class); verify(httpBidderRequester).requestBids(same(bidder2), bidRequest2Captor.capture(), any(), anyBoolean()); final BidRequest capturedBidRequest2 = bidRequest2Captor.getValue(); - ExtRequestPrebidSchainSchain requestSchain2 = extSource.getSchain(); + final ExtRequestPrebidSchainSchain requestSchain2 = capturedBidRequest2.getSource().getExt().getSchain(); assertThat(requestSchain2).isNotNull(); assertThat(requestSchain2).isEqualTo(specificSchain); assertThat(capturedBidRequest2.getExt().getPrebid().getSchains()).isNull(); @@ -541,9 +547,9 @@ public void shouldPassRequestWithInjectedSchainInSourceExt() { final ArgumentCaptor bidRequest3Captor = ArgumentCaptor.forClass(BidRequest.class); verify(httpBidderRequester).requestBids(same(bidder3), bidRequest3Captor.capture(), any(), anyBoolean()); final BidRequest capturedBidRequest3 = bidRequest3Captor.getValue(); - ExtRequestPrebidSchainSchain requestSchain3 = extSource.getSchain(); + final ExtRequestPrebidSchainSchain requestSchain3 = capturedBidRequest3.getSource().getExt().getSchain(); assertThat(requestSchain3).isNotNull(); - assertThat(requestSchain3).isEqualTo(allSchainObject); + assertThat(requestSchain3).isEqualTo(generalSchain); assertThat(capturedBidRequest3.getExt().getPrebid().getSchains()).isNull(); } @@ -1804,6 +1810,7 @@ public void shouldPassReducedGlobalTimeoutToConnectorAndOriginalToBidResponseCre storedResponseProcessor, privacyEnforcementService, fpdResolver, + schainResolver, httpBidderRequester, responseBidValidator, currencyService, diff --git a/src/test/java/org/prebid/server/auction/SchainResolverTest.java b/src/test/java/org/prebid/server/auction/SchainResolverTest.java new file mode 100644 index 00000000000..a07f7329e12 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/SchainResolverTest.java @@ -0,0 +1,136 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.BidRequest; +import org.junit.Before; +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchain; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchainNode; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class SchainResolverTest extends VertxTest { + + private SchainResolver schainResolver; + + @Before + public void setUp() { + schainResolver = SchainResolver.create(null, jacksonMapper); + } + + @Test + public void shouldResolveSchainsWhenCatchAllPresent() { + // given + final ExtRequestPrebidSchainSchainNode specificNodes = ExtRequestPrebidSchainSchainNode.of( + "asi", "sid", 1, "rid", "name", "domain", null); + final ExtRequestPrebidSchainSchain specificSchain = ExtRequestPrebidSchainSchain.of( + "ver", 1, singletonList(specificNodes), null); + final ExtRequestPrebidSchain schainForBidders = ExtRequestPrebidSchain.of( + asList("bidder1", "bidder2"), specificSchain); + + final ExtRequestPrebidSchainSchainNode generalNodes = ExtRequestPrebidSchainSchainNode.of( + "t", null, 0, "a", null, "ads", null); + final ExtRequestPrebidSchainSchain generalSchain = ExtRequestPrebidSchainSchain.of( + "t", 123, singletonList(generalNodes), null); + final ExtRequestPrebidSchain allSchain = ExtRequestPrebidSchain.of(singletonList("*"), generalSchain); + + final BidRequest bidRequest = BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .schains(asList(schainForBidders, allSchain)) + .build())) + .build(); + + // when and then + assertThat(schainResolver.resolveForBidder("bidder1", bidRequest)).isSameAs(specificSchain); + assertThat(schainResolver.resolveForBidder("bidder2", bidRequest)).isSameAs(specificSchain); + assertThat(schainResolver.resolveForBidder("bidder3", bidRequest)).isSameAs(generalSchain); + } + + @Test + public void shouldReturnNullWhenAbsentForBidderAndNoCatchAll() { + // given + final ExtRequestPrebidSchainSchainNode specificNodes = ExtRequestPrebidSchainSchainNode.of( + "asi", "sid", 1, "rid", "name", "domain", null); + final ExtRequestPrebidSchainSchain specificSchain = ExtRequestPrebidSchainSchain.of( + "ver", 1, singletonList(specificNodes), null); + final ExtRequestPrebidSchain schainForBidders = ExtRequestPrebidSchain.of( + singletonList("bidder1"), specificSchain); + + final BidRequest bidRequest = BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .schains(singletonList(schainForBidders)) + .build())) + .build(); + + // when and then + assertThat(schainResolver.resolveForBidder("bidder2", bidRequest)).isNull(); + } + + @Test + public void shouldIgnoreDuplicatedBidderSchains() { + // given + final ExtRequestPrebidSchain schain1 = ExtRequestPrebidSchain.of( + singletonList("bidder"), ExtRequestPrebidSchainSchain.of("ver1", null, null, null)); + final ExtRequestPrebidSchain schain2 = ExtRequestPrebidSchain.of( + singletonList("bidder"), ExtRequestPrebidSchainSchain.of("ver2", null, null, null)); + + final BidRequest bidRequest = BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .schains(asList(schain1, schain2)) + .build())) + .build(); + + // when and then + assertThat(schainResolver.resolveForBidder("bidder", bidRequest)).isNull(); + } + + @Test + public void shouldInjectGlobalNodeIntoResolvedSchain() { + // given + schainResolver = SchainResolver.create( + "{\"asi\": \"pbshostcompany.com\", \"sid\":\"00001\"}", + jacksonMapper); + + final ExtRequestPrebidSchainSchainNode node = ExtRequestPrebidSchainSchainNode.of( + "asi", "sid", 1, "rid", "name", "domain", null); + final ExtRequestPrebidSchainSchain schain = ExtRequestPrebidSchainSchain.of( + "ver", 1, singletonList(node), null); + final ExtRequestPrebidSchain schainEntry = ExtRequestPrebidSchain.of( + singletonList("bidder"), schain); + + final BidRequest bidRequest = BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .schains(singletonList(schainEntry)) + .build())) + .build(); + + // when and then + final ExtRequestPrebidSchainSchainNode globalNode = ExtRequestPrebidSchainSchainNode.of( + "pbshostcompany.com", "00001", null, null, null, null, null); + final ExtRequestPrebidSchainSchain expectedSchain = ExtRequestPrebidSchainSchain.of( + "ver", 1, asList(node, globalNode), null); + assertThat(schainResolver.resolveForBidder("bidder", bidRequest)).isEqualTo(expectedSchain); + } + + @Test + public void shouldReturnSchainWithGlobalNodeOnly() { + // given + schainResolver = SchainResolver.create( + "{\"asi\": \"pbshostcompany.com\", \"sid\":\"00001\"}", + jacksonMapper); + + final BidRequest bidRequest = BidRequest.builder().build(); + + // when and then + final ExtRequestPrebidSchainSchainNode globalNode = ExtRequestPrebidSchainSchainNode.of( + "pbshostcompany.com", "00001", null, null, null, null, null); + final ExtRequestPrebidSchainSchain expectedSchain = ExtRequestPrebidSchainSchain.of( + null, null, singletonList(globalNode), null); + assertThat(schainResolver.resolveForBidder("bidder", bidRequest)).isEqualTo(expectedSchain); + } +} From f35f6c8cb82b0d29c3d9bf6ac8d01cfe966b78d6 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Fri, 22 Jan 2021 15:42:34 +0200 Subject: [PATCH 100/129] Update VerizonMedia bidder config (#1087) --- src/main/resources/bidder-config/verizonmedia.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/bidder-config/verizonmedia.yaml b/src/main/resources/bidder-config/verizonmedia.yaml index f8cdbef1ef0..56899216fbe 100644 --- a/src/main/resources/bidder-config/verizonmedia.yaml +++ b/src/main/resources/bidder-config/verizonmedia.yaml @@ -1,7 +1,7 @@ adapters: verizonmedia: enabled: false - endpoint: https:// + endpoint: https://s2shb.ssp.yahoo.com/admax/bid/partners/MAG pbs-enforces-gdpr: true pbs-enforces-ccpa: true modifying-vast-xml-allowed: true @@ -15,8 +15,8 @@ adapters: supported-vendors: vendor-id: 25 usersync: - url: + url: https://ups.analytics.yahoo.com/ups/58401/sync?redir=true&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}} redirect-url: cookie-family-name: verizonmedia type: redirect - support-cors: false \ No newline at end of file + support-cors: false From aea2dd825ab50a3c1c9adb20b9306c550c7ba839 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Fri, 22 Jan 2021 16:22:06 +0200 Subject: [PATCH 101/129] Add new Rubicon size (#980) --- src/main/java/org/prebid/server/bidder/rubicon/RubiconSize.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconSize.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconSize.java index d8b9d674bdd..63a52ee1500 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconSize.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconSize.java @@ -101,6 +101,7 @@ final class RubiconSize { SIZES.put(size(320, 500), 278); SIZES.put(size(320, 400), 282); SIZES.put(size(640, 380), 288); + SIZES.put(size(500, 1000), 548); } private final Integer w; From d39e9f7688c9106d11079090e436ff9158d30528 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Fri, 22 Jan 2021 16:36:02 +0200 Subject: [PATCH 102/129] Add app capabilities to VerizonMedia adapter (#1067) --- .../verizonmedia/VerizonmediaBidder.java | 13 ++++-- .../resources/bidder-config/verizonmedia.yaml | 1 + .../verizonmedia/VerizonmediaBidderTest.java | 43 ++++++++++++++++--- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidder.java b/src/main/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidder.java index f7604f55b56..54257780b4e 100644 --- a/src/main/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidder.java +++ b/src/main/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidder.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -110,12 +111,18 @@ private static BidRequest modifyRequest(BidRequest request, Imp imp, ExtImpVeriz impBuilder.banner(modifyBanner(banner)); } + final BidRequest.BidRequestBuilder requestBuilder = request.toBuilder(); + final Site site = request.getSite(); - final Site.SiteBuilder siteBuilder = site == null ? Site.builder() : site.toBuilder(); + final App app = request.getApp(); + if (site != null) { + requestBuilder.site(site.toBuilder().id(extImpVerizonmedia.getDcn()).build()); + } else if (app != null) { + requestBuilder.app(app.toBuilder().id(extImpVerizonmedia.getDcn()).build()); + } - return request.toBuilder() + return requestBuilder .imp(Collections.singletonList(impBuilder.build())) - .site(siteBuilder.id(extImpVerizonmedia.getDcn()).build()) .build(); } diff --git a/src/main/resources/bidder-config/verizonmedia.yaml b/src/main/resources/bidder-config/verizonmedia.yaml index 56899216fbe..784c33c963f 100644 --- a/src/main/resources/bidder-config/verizonmedia.yaml +++ b/src/main/resources/bidder-config/verizonmedia.yaml @@ -10,6 +10,7 @@ adapters: meta-info: maintainer-email: dsp-supply-prebid@verizonmedia.com app-media-types: + - banner site-media-types: - banner supported-vendors: diff --git a/src/test/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidderTest.java b/src/test/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidderTest.java index fbab520ee9e..0342a6f11ef 100644 --- a/src/test/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidderTest.java @@ -1,6 +1,7 @@ package org.prebid.server.bidder.verizonmedia; import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -127,7 +128,24 @@ public void makeHttpRequestsShouldCreateARequestForEachImpAndSkipImpsWithErrors( } @Test - public void makeHttpRequestsShouldAlwaysSetSiteIdAndImpTagIdFromImpExt() { + public void makeHttpRequestsShouldAlwaysSetImpTagIdFromImpExt() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), identity()); + + // when + final Result>> result = verizonmediaBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp).hasSize(1) + .extracting(Imp::getTagid) + .containsExactly("pos"); + } + + @Test + public void makeHttpRequestsShouldSetSiteIdIfSiteIsPresentInTheRequest() { // given final BidRequest bidRequest = givenBidRequest(identity(), identity()); @@ -140,12 +158,25 @@ public void makeHttpRequestsShouldAlwaysSetSiteIdAndImpTagIdFromImpExt() { .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .extracting(BidRequest::getSite) .extracting(Site::getId) - .containsOnly("dcn"); - assertThat(result.getValue()) + .containsExactly("dcn"); + } + + @Test + public void makeHttpRequestsShouldSetAppIdIfAppIsPresentInTheRequest() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), + bidRequestBuilder -> bidRequestBuilder.site(null).app(App.builder().build())); + + // when + final Result>> result = verizonmediaBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp).hasSize(1) - .extracting(Imp::getTagid) - .containsOnly("pos"); + .extracting(BidRequest::getApp) + .extracting(App::getId) + .containsExactly("dcn"); } @Test From 9b20db8292688241e9f29ce37414468168665c6c Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Fri, 22 Jan 2021 16:42:12 +0200 Subject: [PATCH 103/129] Set Kubient email to prebid@kubient.com (#1085) --- src/main/resources/bidder-config/kubient.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/bidder-config/kubient.yaml b/src/main/resources/bidder-config/kubient.yaml index 0e42f83de5b..b8856f0ff40 100644 --- a/src/main/resources/bidder-config/kubient.yaml +++ b/src/main/resources/bidder-config/kubient.yaml @@ -8,7 +8,7 @@ adapters: deprecated-names: aliases: meta-info: - maintainer-email: support@kubient.com + maintainer-email: prebid@kubient.com app-media-types: - banner - video From 756c859e1faaccde45561730b691525ba7370706 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Fri, 22 Jan 2021 18:04:43 +0200 Subject: [PATCH 104/129] Extract bids removal into separate component (#1080) --- .../server/auction/BidResponseCreator.java | 49 ++------ .../server/auction/BidResponseReducer.java | 85 +++++++++++++ .../spring/config/ServiceConfiguration.java | 8 ++ .../auction/BidResponseCreatorTest.java | 34 +++-- .../auction/BidResponseReducerTest.java | 116 ++++++++++++++++++ 5 files changed, 234 insertions(+), 58 deletions(-) create mode 100644 src/main/java/org/prebid/server/auction/BidResponseReducer.java create mode 100644 src/test/java/org/prebid/server/auction/BidResponseReducerTest.java diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index 7c56ccb0724..16a572afbc9 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -75,7 +75,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; @@ -102,6 +101,7 @@ public class BidResponseCreator { private final BidderCatalog bidderCatalog; private final EventsService eventsService; private final StoredRequestProcessor storedRequestProcessor; + private final BidResponseReducer bidResponseReducer; private final IdGenerator bidIdGenerator; private final int truncateAttrChars; private final Clock clock; @@ -115,6 +115,7 @@ public BidResponseCreator(CacheService cacheService, BidderCatalog bidderCatalog, EventsService eventsService, StoredRequestProcessor storedRequestProcessor, + BidResponseReducer bidResponseReducer, IdGenerator bidIdGenerator, int truncateAttrChars, Clock clock, @@ -124,6 +125,7 @@ public BidResponseCreator(CacheService cacheService, this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.eventsService = Objects.requireNonNull(eventsService); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.bidResponseReducer = Objects.requireNonNull(bidResponseReducer); this.bidIdGenerator = Objects.requireNonNull(bidIdGenerator); this.truncateAttrChars = validateTruncateAttrChars(truncateAttrChars); this.clock = Objects.requireNonNull(clock); @@ -195,7 +197,9 @@ private Future cacheBidsAndCreateResponse(List bidd final BidRequest bidRequest = auctionContext.getBidRequest(); - bidderResponses.forEach(BidResponseCreator::removeRedundantBids); + final List updatedBidderResponses = bidderResponses.stream() + .map(bidResponseReducer::removeRedundantBids) + .collect(Collectors.toList()); ExtRequestTargeting targeting = targeting(bidRequest); final Set winningBids = newOrEmptySet(targeting); @@ -208,12 +212,12 @@ private Future cacheBidsAndCreateResponse(List bidd // determine winning bids only if targeting is present if (targeting != null) { - populateWinningBids(bidderResponses, winningBids, winningBidsByBidder); + populateWinningBids(updatedBidderResponses, winningBids, winningBidsByBidder); } final Set bidsToCache = cacheInfo.isShouldCacheWinningBidsOnly() ? winningBids - : bidderResponses.stream().flatMap(BidResponseCreator::getBids).collect(Collectors.toSet()); + : updatedBidderResponses.stream().flatMap(BidResponseCreator::getBids).collect(Collectors.toSet()); final EventsContext eventsContext = EventsContext.builder() .enabledForAccount(eventsEnabledForAccount(auctionContext)) @@ -223,7 +227,7 @@ private Future cacheBidsAndCreateResponse(List bidd .build(); return toBidsWithCacheIds( - bidderResponses, + updatedBidderResponses, bidsToCache, generatedBidIds, auctionContext, @@ -231,7 +235,7 @@ private Future cacheBidsAndCreateResponse(List bidd eventsContext) .compose(cacheResult -> videoStoredDataResult(auctionContext) .map(videoStoredDataResult -> toBidResponse( - bidderResponses, + updatedBidderResponses, auctionContext, targeting, winningBids, @@ -286,39 +290,6 @@ private ExtBidResponse toExtBidResponse(List bidderResponses, ExtBidResponsePrebid.of(auctionTimestamp)); } - private static void removeRedundantBids(BidderResponse bidderResponse) { - final List responseBidderBids = bidderResponse.getSeatBid().getBids(); - if (responseBidderBids.size() < 2) { // no reason for removing if only one bid was responded - return; - } - - final Map> impIdToBidderBid = responseBidderBids.stream() - .collect(Collectors.groupingBy(bidderBid -> bidderBid.getBid().getImpid())); - - final Set mostValuableBids = impIdToBidderBid.values().stream() - .map(BidResponseCreator::mostValuableBid) - .collect(Collectors.toSet()); - - responseBidderBids.clear(); - responseBidderBids.addAll(mostValuableBids); - } - - private static BidderBid mostValuableBid(List bidderBids) { - if (bidderBids.size() == 1) { - return bidderBids.get(0); - } - - final List dealBidderBids = bidderBids.stream() - .filter(bidderBid -> StringUtils.isNotBlank(bidderBid.getBid().getDealid())) - .collect(Collectors.toList()); - - final List processedBidderBids = dealBidderBids.isEmpty() ? bidderBids : dealBidderBids; - - return processedBidderBids.stream() - .max(Comparator.comparing(bidderBid -> bidderBid.getBid().getPrice(), Comparator.naturalOrder())) - .orElse(bidderBids.get(0)); - } - /** * Returns new {@link HashSet} in case of existing keywordsCreator or empty collection if null. */ diff --git a/src/main/java/org/prebid/server/auction/BidResponseReducer.java b/src/main/java/org/prebid/server/auction/BidResponseReducer.java new file mode 100644 index 00000000000..5d1635abdd2 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/BidResponseReducer.java @@ -0,0 +1,85 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.response.Bid; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Class for removing bids from response for the same bidder-imp pair. + */ +public class BidResponseReducer { + + /** + * Removes {@link Bid}s with the same impId taking into account if {@link Bid} has deal. + *

+ * Returns given list of {@link BidderResponse}s if {@link Bid}s have different impIds. + */ + public BidderResponse removeRedundantBids(BidderResponse bidderResponse) { + final List bidderBids = ListUtils.emptyIfNull(bidderResponse.getSeatBid().getBids()); + final Map> impIdToBidderBids = bidderBids.stream() + .collect(Collectors.groupingBy(bidderBid -> bidderBid.getBid().getImpid())); + + final Set updatedBidderBids = impIdToBidderBids.values().stream() + .map(BidResponseReducer::removeRedundantBidsForImp) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + + if (bidderBids.size() == updatedBidderBids.size()) { + return bidderResponse; + } + + return updateBidderResponse(bidderResponse, updatedBidderBids); + } + + private static List removeRedundantBidsForImp(List bidderBids) { + return bidderBids.size() > 1 ? reduceBidsByImpId(bidderBids) : bidderBids; + } + + private static List reduceBidsByImpId(List bidderBids) { + return bidderBids.stream().anyMatch(bidderBid -> bidderBid.getBid().getDealid() != null) + ? removeRedundantDealsBids(bidderBids) + : removeRedundantForNonDealBids(bidderBids); + } + + private static List removeRedundantDealsBids(List bidderBids) { + final List dealBidderBids = bidderBids.stream() + .filter(bidderBid -> StringUtils.isNotBlank(bidderBid.getBid().getDealid())) + .collect(Collectors.toList()); + + return Collections.singletonList(getHighestPriceBid(bidderBids, dealBidderBids)); + } + + private static List removeRedundantForNonDealBids(List bidderBids) { + return Collections.singletonList(getHighestPriceBid(bidderBids, bidderBids)); + } + + private static BidderBid getHighestPriceBid(List bidderBids, List dealBidderBids) { + return dealBidderBids.stream() + .max(Comparator.comparing(bidderBid -> bidderBid.getBid().getPrice(), Comparator.naturalOrder())) + .orElse(bidderBids.get(0)); + } + + private static BidderResponse updateBidderResponse(BidderResponse bidderResponse, + Set updatedBidderBids) { + + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + final BidderSeatBid updatedSeatBid = BidderSeatBid.of( + new ArrayList<>(updatedBidderBids), + seatBid.getHttpCalls(), + seatBid.getErrors()); + + return BidderResponse.of(bidderResponse.getBidder(), updatedSeatBid, bidderResponse.getResponseTime()); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index e02d861c203..c68064be078 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -11,6 +11,7 @@ import org.prebid.server.auction.AuctionRequestFactory; import org.prebid.server.auction.BidResponseCreator; import org.prebid.server.auction.BidResponsePostProcessor; +import org.prebid.server.auction.BidResponseReducer; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.FpdResolver; import org.prebid.server.auction.ImplicitParametersExtractor; @@ -469,6 +470,7 @@ BidResponseCreator bidResponseCreator( BidderCatalog bidderCatalog, EventsService eventsService, StoredRequestProcessor storedRequestProcessor, + BidResponseReducer bidResponseReducer, IdGenerator bidIdGenerator, @Value("${settings.targeting.truncate-attr-chars}") int truncateAttrChars, Clock clock, @@ -479,6 +481,7 @@ BidResponseCreator bidResponseCreator( bidderCatalog, eventsService, storedRequestProcessor, + bidResponseReducer, bidIdGenerator, truncateAttrChars, clock, @@ -541,6 +544,11 @@ StoredRequestProcessor storedRequestProcessor( jsonMerger); } + @Bean + BidResponseReducer bidResponseReducer() { + return new BidResponseReducer(); + } + @Bean StoredResponseProcessor storedResponseProcessor(ApplicationSettings applicationSettings, BidderCatalog bidderCatalog, diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 4e76a72b36f..b3ccd2076a7 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -122,6 +122,8 @@ public class BidResponseCreatorTest extends VertxTest { @Mock private StoredRequestProcessor storedRequestProcessor; @Mock + private BidResponseReducer bidResponseReducer; + @Mock private IdGenerator idGenerator; private Clock clock; @@ -135,6 +137,8 @@ public void setUp() { given(cacheService.getEndpointHost()).willReturn("testHost"); given(cacheService.getEndpointPath()).willReturn("testPath"); given(cacheService.getCachedAssetURLTemplate()).willReturn("uuid="); + given(bidResponseReducer.removeRedundantBids(any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); given(storedRequestProcessor.videoStoredDataResult(any(), anyList(), anyList(), any())) .willReturn(Future.succeededFuture(VideoStoredDataResult.empty())); @@ -147,6 +151,7 @@ public void setUp() { bidderCatalog, eventsService, storedRequestProcessor, + bidResponseReducer, idGenerator, 0, clock, @@ -502,6 +507,7 @@ public void shouldOverrideBidIdWhenGenerateBidIdIsTurnedOn() { bidderCatalog, eventsService, storedRequestProcessor, + bidResponseReducer, idGenerator, 0, clock, @@ -566,6 +572,7 @@ public void shouldUseGeneratedBidIdForEventAndCacheWhenGeneratedIdIsTurnedOn() { bidderCatalog, eventsService, storedRequestProcessor, + bidResponseReducer, idGenerator, 0, clock, @@ -621,34 +628,22 @@ public void shouldSetExpectedResponseSeatBidAndBidFields() { } @Test - public void shouldFilterByDealsAndPriceBidsWhenImpIdsAreEqual() { + public void shouldFilterBidByBidReducerResponse() { // given final AuctionContext auctionContext = givenAuctionContext(givenBidRequest()); - final Bid simpleBidImp1 = Bid.builder().id("bidId1i1").price(BigDecimal.valueOf(5.67)).impid("i1").build(); - final Bid simpleBid1Imp2 = Bid.builder().id("bidId1i2").price(BigDecimal.valueOf(15.67)).impid("i2").build(); - final Bid simpleBid2Imp2 = Bid.builder().id("bidId2i2").price(BigDecimal.valueOf(17.67)).impid("i2").build(); final Bid dealBid1Imp1 = Bid.builder().id("bidId1i1d").dealid("d1").price(BigDecimal.valueOf(4.98)).impid("i1") .build(); final Bid dealBid2Imp1 = Bid.builder().id("bidId2i1d").dealid("d2").price(BigDecimal.valueOf(5.00)).impid("i1") .build(); final BidderSeatBid seatBidWithDeals = givenSeatBid( - BidderBid.of(simpleBidImp1, banner, null), - BidderBid.of(simpleBid1Imp2, banner, null), - BidderBid.of(simpleBid2Imp2, banner, null), // will stay (top price) - BidderBid.of(simpleBid2Imp2, banner, null), // duplicate should be removed - BidderBid.of(dealBid2Imp1, banner, null), // will stay (deal + topPrice) + BidderBid.of(dealBid2Imp1, banner, null), BidderBid.of(dealBid1Imp1, banner, null)); - final Bid simpleBid3Imp2 = Bid.builder().id("bidId3i2").price(BigDecimal.valueOf(7.25)).impid("i2").build(); - final Bid simpleBid4Imp2 = Bid.builder().id("bidId4i2").price(BigDecimal.valueOf(7.26)).impid("i2").build(); - final BidderSeatBid seatBidWithSimpleBids = givenSeatBid( - BidderBid.of(simpleBid3Imp2, banner, null), - BidderBid.of(simpleBid4Imp2, banner, null)); // will stay (top price) + final List bidderResponses = singletonList(BidderResponse.of("bidder1", seatBidWithDeals, 100)); - final List bidderResponses = asList( - BidderResponse.of("bidder1", seatBidWithDeals, 100), - BidderResponse.of("bidder2", seatBidWithSimpleBids, 111)); + given(bidResponseReducer.removeRedundantBids(any())) + .willReturn(BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(dealBid2Imp1, banner, null)), 100)); // when final BidResponse bidResponse = @@ -656,9 +651,9 @@ public void shouldFilterByDealsAndPriceBidsWhenImpIdsAreEqual() { // then assertThat(bidResponse.getSeatbid()) - .flatExtracting(SeatBid::getBid).hasSize(3) + .flatExtracting(SeatBid::getBid).hasSize(1) .extracting(Bid::getId) - .containsOnly("bidId2i2", "bidId2i1d", "bidId4i2"); + .containsOnly("bidId2i1d"); } @Test @@ -1000,6 +995,7 @@ public void shouldTruncateTargetingKeywordsByGlobalConfig() { bidderCatalog, eventsService, storedRequestProcessor, + bidResponseReducer, idGenerator, 20, clock, diff --git a/src/test/java/org/prebid/server/auction/BidResponseReducerTest.java b/src/test/java/org/prebid/server/auction/BidResponseReducerTest.java new file mode 100644 index 00000000000..d20f0dbb716 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/BidResponseReducerTest.java @@ -0,0 +1,116 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.response.Bid; +import org.junit.Test; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class BidResponseReducerTest { + + private final BidResponseReducer bidResponseReducer = new BidResponseReducer(); + + @Test + public void removeRedundantBidsShouldReduceNonDealBidsByPriceDroppingNonDealsBids() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder1", + givenSeatBid( + givenBidderBid("bidId1", "impId1", "dealId1", 5.0f), // deal + givenBidderBid("bidId2", "impId1", "dealId2", 6.0f), // deal + givenBidderBid("bidId3", "impId1", null, 7.0f) // non deal + ), + 0); + + // when + final BidderResponse resultBidderResponse = bidResponseReducer.removeRedundantBids(bidderResponse); + + // then + assertThat(resultBidderResponse.getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getId) + .containsOnly("bidId2"); + } + + @Test + public void removeRedundantBidsShouldReduceNonDealBidsByPrice() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder1", + givenSeatBid( + givenBidderBid("bidId1", "impId1", null, 5.0f), // non deal + givenBidderBid("bidId2", "impId1", null, 6.0f), // non deal + givenBidderBid("bidId3", "impId1", null, 7.0f) // non deal + ), + 0); + + // when + final BidderResponse resultBidderResponse = bidResponseReducer.removeRedundantBids(bidderResponse); + + // then + assertThat(resultBidderResponse.getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getId) + .containsOnly("bidId3"); + } + + @Test + public void removeRedundantBidsShouldNotReduceBids() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder1", + givenSeatBid(givenBidderBid("bidId1", "impId1", null, 5.0f)), + 0); + + // when + final BidderResponse resultBidderResponse = bidResponseReducer.removeRedundantBids(bidderResponse); + + // then + assertThat(resultBidderResponse.getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getId) + .containsOnly("bidId1"); + } + + @Test + public void removeRedundantBidsShouldReduceAllTypesOfBidsForMultipleImps() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder1", + givenSeatBid( + givenBidderBid("bidId3-1", "impId1", "dealId3", 5.0f), // deal + givenBidderBid("bidId4-1", "impId1", null, 5.0f), // non deal + givenBidderBid("bidId1-2", "impId2", "dealId4", 5.0f), // deal + givenBidderBid("bidId2-2", "impId2", "dealId5", 6.0f), // deal + givenBidderBid("bidId3-2", "impId2", null, 5.0f), // non deal + givenBidderBid("bidId1-3", "impId3", null, 5.0f), // non deal + givenBidderBid("bidId2-3", "impId3", null, 6.0f) // non deal + ), + 0); + + // when + final BidderResponse resultBidderResponse = bidResponseReducer.removeRedundantBids(bidderResponse); + + // then + assertThat(resultBidderResponse.getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getId) + .containsOnly("bidId3-1", "bidId2-2", "bidId2-3"); + } + + private static BidderBid givenBidderBid(String bidId, String impId, String dealId, float price) { + return BidderBid.of( + Bid.builder().id(bidId).impid(impId).dealid(dealId).price(BigDecimal.valueOf(price)).build(), + BidType.banner, "USD"); + } + + private static BidderSeatBid givenSeatBid(BidderBid... bidderBids) { + return BidderSeatBid.of(Arrays.asList(bidderBids), null, null); + } +} From 08d2f0a95acc0a7ebff98dac2aa0f8eb8fd6a74c Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Fri, 22 Jan 2021 18:11:13 +0200 Subject: [PATCH 105/129] Make sure jetty (used by WireMock under the hood) server is gracefully stopped between integration tests (#1106) --- src/test/java/org/prebid/server/it/IntegrationTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/prebid/server/it/IntegrationTest.java b/src/test/java/org/prebid/server/it/IntegrationTest.java index fbc2e0c110d..5098ec932a8 100644 --- a/src/test/java/org/prebid/server/it/IntegrationTest.java +++ b/src/test/java/org/prebid/server/it/IntegrationTest.java @@ -52,6 +52,7 @@ public abstract class IntegrationTest extends VertxTest { public static final WireMockClassRule WIRE_MOCK_RULE = new WireMockClassRule(options() .port(WIREMOCK_PORT) .gzipDisabled(true) + .jettyStopTimeout(5000L) .extensions(IntegrationTest.CacheResponseTransformer.class)); @Rule From 629ece348d3dc42a4711cdd63628a88a07f20216 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Fri, 22 Jan 2021 18:21:06 +0200 Subject: [PATCH 106/129] Set the right domain for AMP requests (#1033) --- .../server/auction/AmpRequestFactory.java | 5 +++++ .../server/auction/AmpRequestFactoryTest.java | 20 ++++++++++--------- .../it/amp/test-appnexus-bid-request.json | 2 +- .../it/amp/test-rubicon-bid-request.json | 2 +- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/AmpRequestFactory.java index ee60140194e..9d07d648978 100644 --- a/src/main/java/org/prebid/server/auction/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AmpRequestFactory.java @@ -340,6 +340,11 @@ private Site overrideSite(Site site, HttpServerRequest request) { final Site.SiteBuilder siteBuilder = hasSite ? site.toBuilder() : Site.builder(); if (StringUtils.isNotBlank(canonicalUrl)) { siteBuilder.page(canonicalUrl); + + final String domain = HttpUtil.getDomainFromUrl(canonicalUrl); + if (StringUtils.isNotBlank(domain)) { + siteBuilder.domain(domain); + } } if (StringUtils.isNotBlank(accountId)) { final Publisher publisher = hasSite ? site.getPublisher() : null; diff --git a/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java index dc4e75455ca..6b5889f6d28 100644 --- a/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java @@ -583,14 +583,14 @@ public void shouldSetBidRequestSiteExt() { } @Test - public void shouldReturnBidRequestWithOverriddenSitePageByCurlParamValue() { + public void shouldReturnBidRequestWithOverriddenSitePageAndDomainByCurlParamValue() { // given - given(httpRequest.getParam("curl")).willReturn("overridden-site-page"); + given(httpRequest.getParam("curl")).willReturn("http://overridden.site.page:8080/path"); givenBidRequest( builder -> builder .ext(ExtRequest.empty()) - .site(Site.builder().page("will-be-overridden").build()), + .site(Site.builder().page("http://will.be.overridden/path").build()), Imp.builder().build()); // when @@ -599,14 +599,15 @@ public void shouldReturnBidRequestWithOverriddenSitePageByCurlParamValue() { // then assertThat(singletonList(request)) .extracting(BidRequest::getSite) - .extracting(Site::getPage, Site::getExt) - .containsOnly(tuple("overridden-site-page", ExtSite.of(1, null))); + .extracting(Site::getPage, Site::getDomain, Site::getExt) + .containsOnly(tuple( + "http://overridden.site.page:8080/path", "overridden.site.page", ExtSite.of(1, null))); } @Test - public void shouldReturnBidRequestWithSitePageContainingCurlParamValueWhenSitePreviouslyNotExistInRequest() { + public void shouldReturnBidRequestWithSitePageAndDomainContainingCurlParamValueWhenSiteNotInRequest() { // given - given(httpRequest.getParam("curl")).willReturn("overridden-site-page"); + given(httpRequest.getParam("curl")).willReturn("http://overridden.site.page:8080/path"); givenBidRequest( builder -> builder @@ -620,8 +621,9 @@ public void shouldReturnBidRequestWithSitePageContainingCurlParamValueWhenSitePr // then assertThat(singletonList(request)) .extracting(BidRequest::getSite) - .extracting(Site::getPage, Site::getExt) - .containsOnly(tuple("overridden-site-page", ExtSite.of(1, null))); + .extracting(Site::getPage, Site::getDomain, Site::getExt) + .containsOnly(tuple( + "http://overridden.site.page:8080/path", "overridden.site.page", ExtSite.of(1, null))); } @Test diff --git a/src/test/resources/org/prebid/server/it/amp/test-appnexus-bid-request.json b/src/test/resources/org/prebid/server/it/amp/test-appnexus-bid-request.json index c60367a8448..b4c9ca2056b 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-appnexus-bid-request.json +++ b/src/test/resources/org/prebid/server/it/amp/test-appnexus-bid-request.json @@ -26,7 +26,7 @@ } ], "site": { - "domain": "example.com", + "domain": "google.com", "page": "https://google.com", "publisher": { "id": "accountId" diff --git a/src/test/resources/org/prebid/server/it/amp/test-rubicon-bid-request.json b/src/test/resources/org/prebid/server/it/amp/test-rubicon-bid-request.json index 980408f6332..c515f2c3f85 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-rubicon-bid-request.json +++ b/src/test/resources/org/prebid/server/it/amp/test-rubicon-bid-request.json @@ -38,7 +38,7 @@ } ], "site": { - "domain": "example.com", + "domain": "google.com", "page": "https://google.com", "publisher": { "ext": { From e1f8af776b79f4706c7d97ff1ef41372057e6b5a Mon Sep 17 00:00:00 2001 From: Braslavskyi Andrii Date: Mon, 25 Jan 2021 13:48:14 +0200 Subject: [PATCH 107/129] Resolve pubnative bid size from banner's size or formats (#1093) --- .../bidder/pubnative/PubnativeBidder.java | 72 ++++++++-- .../bidder/pubnative/PubnativeBidderTest.java | 131 ++++++++++++++++++ 2 files changed, 191 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java b/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java index 8ed8c94d3b3..830b7892428 100644 --- a/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java +++ b/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java @@ -7,6 +7,7 @@ import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; @@ -145,21 +146,68 @@ private static List bidsFromResponse(List seatbid, List .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, resolveBidType(bid.getImpid(), imps), currency)) + .map(bid -> createBidderBid(imps, currency, bid)) .collect(Collectors.toList()); } - private static BidType resolveBidType(String impid, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impid)) { - if (imp.getVideo() != null) { - return BidType.video; - } - if (imp.getXNative() != null) { - return BidType.xNative; - } - } + private static BidderBid createBidderBid(List imps, String currency, Bid bid) { + final Imp imp = findImpById(bid.getImpid(), imps); + return BidderBid.of(updateBidWithSize(bid, imp), resolveBidType(imp), currency); + } + + private static Bid updateBidWithSize(Bid bid, Imp imp) { + if (bid.getW() != null && bid.getH() != null) { + return bid; + } + + final Format format = imp != null && imp.getBanner() != null + ? resolveBidSizeFromBanner(imp.getBanner()) + : null; + return format != null + ? bid.toBuilder().w(format.getW()).h(format.getH()).build() + : bid; + } + + private static Imp findImpById(String impId, List imps) { + return imps.stream() + .filter(imp -> imp.getId().equals(impId)) + .findFirst() + .orElse(null); + } + + private static BidType resolveBidType(Imp imp) { + if (imp == null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } else { + return BidType.banner; + } + } + + private static Format resolveBidSizeFromBanner(Banner banner) { + Format result = null; + final Integer width = banner.getW(); + final Integer height = banner.getH(); + + final List formats = banner.getFormat(); + if (width != null && height != null) { + result = isOnlyOneSize(width, height, formats) + ? Format.builder().w(width).h(height).build() + : null; + } else if (formats.size() == 1) { + result = formats.get(0); } - return BidType.banner; + return result; + } + + private static boolean isOnlyOneSize(Integer width, Integer height, List formats) { + return CollectionUtils.isEmpty(formats) || (formats.size() == 1 && isSameFormat(width, height, formats.get(0))); + } + + private static boolean isSameFormat(Integer width, Integer height, Format format) { + return width.equals(format.getW()) && height.equals(format.getH()); } } diff --git a/src/test/java/org/prebid/server/bidder/pubnative/PubnativeBidderTest.java b/src/test/java/org/prebid/server/bidder/pubnative/PubnativeBidderTest.java index d24ed956c2d..4bbf7a63d75 100644 --- a/src/test/java/org/prebid/server/bidder/pubnative/PubnativeBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/pubnative/PubnativeBidderTest.java @@ -311,6 +311,137 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessi .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); } + @Test + public void makeBidsShouldResolveBidSizeFromBannerIfWAndHArePresent() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100).build()) + .id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = pubnativeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").w(100).h(100).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldResolveBidSizeForBannerWhenWAndHNotNullAndFormatHasSingleElementWithSameSize() + throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100) + .format(singletonList(Format.builder().w(100).h(100).build())).build()) + .id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = pubnativeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").w(100).h(100).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldNotResolveBidSizeForBannerWhenWAndHNotNullAndFormatHasSingleElementWithDifferentSize() + throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100) + .format(singletonList(Format.builder().w(150).h(150).build())).build()) + .id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = pubnativeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldNotResolveBidSizeForBannerWhenWAndHNotNullAndFormatHasMultipleElements() + throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100) + .format(singletonList(Format.builder().w(100).h(100).build())).build()) + .id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = pubnativeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").w(100).h(100).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldResolveBidSizeForBannerWhenWAndHAreNullAndFormatHasSingleElements() + throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder() + .format(singletonList(Format.builder().w(150).h(150).build())).build()) + .id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = pubnativeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").w(150).h(150).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldNotResolveBidSizeForBannerWhenWAndHAreNullAndFormatMultipleElements() + throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder() + .format(asList(Format.builder().w(100).h(100).build(), + Format.builder().w(150).h(150).build())).build()) + .id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = pubnativeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + private static BidRequest givenBidRequest( Function impModifier, Function requestModifier) { From f680e79168e61c2705d7547301028c01556f1efe Mon Sep 17 00:00:00 2001 From: Braslavskyi Andrii Date: Mon, 25 Jan 2021 13:49:26 +0200 Subject: [PATCH 108/129] Add generate bid id property for conversant bidder (#1094) --- .../bidder/conversant/ConversantBidder.java | 16 ++++++++-- .../bidder/ConversantConfiguration.java | 24 ++++++++++++--- .../resources/bidder-config/conversant.yaml | 1 + .../conversant/ConversantBidderTest.java | 29 +++++++++++++++++-- .../server/it/test-application.properties | 1 + 5 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java b/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java index cae7fcab8b4..3e3a1b65049 100644 --- a/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java +++ b/src/main/java/org/prebid/server/bidder/conversant/ConversantBidder.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -57,10 +58,12 @@ public class ConversantBidder implements Bidder { private static final String DISPLAY_MANAGER_VER = "2.0.0"; private final String endpointUrl; + private final boolean generateBidId; private final JacksonMapper mapper; - public ConversantBidder(String endpointUrl, JacksonMapper mapper) { + public ConversantBidder(String endpointUrl, boolean generateBidId, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.generateBidId = generateBidId; this.mapper = Objects.requireNonNull(mapper); } @@ -211,7 +214,7 @@ private List extractBids(HttpCall httpCall) { return bidsFromResponse(httpCall.getRequest().getPayload(), bidResponse); } - private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); final List bids = firstSeatBid.getBid(); @@ -220,10 +223,17 @@ private static List bidsFromResponse(BidRequest bidRequest, BidRespon } return bids.stream() .filter(Objects::nonNull) - .map(bid -> BidderBid.of(bid, getType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .map(bid -> BidderBid.of(updateBidWithId(bid), getType(bid.getImpid(), + bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); } + private Bid updateBidWithId(Bid bid) { + return generateBidId + ? bid.toBuilder().id(UUID.randomUUID().toString()).build() + : bid; + } + private static BidType getType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId)) { diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ConversantConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ConversantConfiguration.java index 91dc61f7823..4f2a10859e1 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ConversantConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ConversantConfiguration.java @@ -1,5 +1,8 @@ package org.prebid.server.spring.config.bidder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.conversant.ConversantAdapter; import org.prebid.server.bidder.conversant.ConversantBidder; @@ -17,8 +20,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; +import org.springframework.validation.annotation.Validated; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; @Configuration @PropertySource(value = "classpath:/bidder-config/conversant.yaml", factory = YamlPropertySourceFactory.class) @@ -35,12 +40,12 @@ public class ConversantConfiguration { @Autowired @Qualifier("conversantConfigurationProperties") - private BidderConfigurationProperties configProperties; + private ConversantConfigurationProperties configProperties; @Bean("conversantConfigurationProperties") @ConfigurationProperties("adapters.conversant") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); + ConversantConfigurationProperties configurationProperties() { + return new ConversantConfigurationProperties(); } @Bean @@ -51,9 +56,20 @@ BidderDeps conversantBidderDeps() { .withConfig(configProperties) .bidderInfo(BidderInfoCreator.create(configProperties)) .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl)) - .bidderCreator(() -> new ConversantBidder(configProperties.getEndpoint(), mapper)) + .bidderCreator(() -> new ConversantBidder(configProperties.getEndpoint(), + configProperties.getGenerateBidId(), mapper)) .adapterCreator(() -> new ConversantAdapter(usersync.getCookieFamilyName(), configProperties.getEndpoint(), mapper)) .assemble(); } + + @Validated + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class ConversantConfigurationProperties extends BidderConfigurationProperties { + + @NotNull + private Boolean generateBidId; + } } diff --git a/src/main/resources/bidder-config/conversant.yaml b/src/main/resources/bidder-config/conversant.yaml index dd6ed57d493..ac2caabe8ce 100644 --- a/src/main/resources/bidder-config/conversant.yaml +++ b/src/main/resources/bidder-config/conversant.yaml @@ -5,6 +5,7 @@ adapters: pbs-enforces-gdpr: true pbs-enforces-ccpa: true modifying-vast-xml-allowed: true + generate-bid-id: true deprecated-names: aliases: meta-info: diff --git a/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java b/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java index e87a25067bb..990a964ef2e 100644 --- a/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java @@ -39,17 +39,20 @@ public class ConversantBidderTest extends VertxTest { private static final String ENDPOINT_URL = "https://test.endpoint.com"; + private static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}" + + "-[0-9a-fA-F]{12}"; private ConversantBidder conversantBidder; @Before public void setUp() { - conversantBidder = new ConversantBidder(ENDPOINT_URL, jacksonMapper); + conversantBidder = new ConversantBidder(ENDPOINT_URL, false, jacksonMapper); } @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new ConversantBidder("invalid_url", jacksonMapper)); + assertThatIllegalArgumentException() + .isThrownBy(() -> new ConversantBidder("invalid_url", false, jacksonMapper)); } @Test @@ -595,6 +598,28 @@ public void makeBidsShouldReturnVideoBidIfRequestImpHasVideo() throws JsonProces .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); } + @Test + public void makeBidsShouldUpdateBidWithUUIDIfGenerateBidIdIsTrue() throws JsonProcessingException { + // given + conversantBidder = new ConversantBidder(ENDPOINT_URL, true, jacksonMapper); + final HttpCall httpCall = givenHttpCall( + givenBidRequest(builder -> builder.id("123") + .banner(Banner.builder().build())), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = conversantBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getId) + .element(0) + .matches(id -> id.matches(UUID_REGEX), "matches UUID format"); + } + private static BidRequest givenBidRequest( Function bidRequestCustomizer, Function impCustomizer, 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 cb2bd89e92b..d36d21f2664 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -107,6 +107,7 @@ adapters.consumable.usersync.url=//consumable-usersync adapters.conversant.enabled=true adapters.conversant.endpoint=http://localhost:8090/conversant-exchange adapters.conversant.pbs-enforces-gdpr=true +adapters.conversant.generate-bid-id=false adapters.conversant.usersync.url=//conversant-usersync adapters.datablocks.enabled=true adapters.datablocks.endpoint=http://{{Host}}/datablocks-exchange?sid={{SourceId}} From 2939c96b0657df00e97fbfca44427772ad3c4df1 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Thu, 28 Jan 2021 08:07:54 +0200 Subject: [PATCH 109/129] Add support for multi-imp requests in ttx bidder (#1069) * Add support for multi-imp requests in ttx bidder * Updated position of request ext creating * Fixes after review * Add addProperties instead of constructor creating * Remove changes related to core --- .../prebid/server/bidder/ttx/TtxBidder.java | 84 ++++++++----------- src/main/resources/bidder-config/ttx.yaml | 2 - .../server/bidder/ttx/TtxBidderTest.java | 43 ++-------- .../ttx/test-auction-ttx-request.json | 2 +- .../openrtb2/ttx/test-ttx-bid-request-1.json | 3 +- 5 files changed, 41 insertions(+), 93 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java b/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java index 3ecebbe7f76..23ad4fdaf01 100644 --- a/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java +++ b/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; @@ -56,25 +55,27 @@ public TtxBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - final Imp firstImp = request.getImp().get(0); final List errors = new ArrayList<>(); - Imp updatedFirstImp = null; - ExtImpTtx extImpTtx = null; - try { - extImpTtx = parseImpExt(firstImp); - updatedFirstImp = updatedImp(firstImp, extImpTtx.getProductId(), extImpTtx.getZoneId(), errors); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); + final List> requests = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + validateImp(imp); + final ExtImpTtx extImpTtx = parseImpExt(imp); + final Imp updatedImp = updateImp(imp, extImpTtx); + requests.add(createRequest(request, updatedImp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } } + return Result.of(requests, errors); + } - if (updatedFirstImp != null && updatedFirstImp.getBanner() == null && updatedFirstImp.getVideo() == null) { - return Result.withError(BidderError.badInput("At least one of [banner, video] " - + "formats must be defined in Imp. None found")); + private void validateImp(Imp imp) { + if (imp.getBanner() == null && imp.getVideo() == null) { + throw new PreBidException( + String.format("Imp ID %s must have at least one of [Banner, Video] defined", imp.getId())); } - - final HttpRequest httpRequest = createRequest(request, extImpTtx, updatedFirstImp); - - return Result.of(Collections.singletonList(httpRequest), errors); } private ExtImpTtx parseImpExt(Imp imp) { @@ -85,15 +86,15 @@ private ExtImpTtx parseImpExt(Imp imp) { } } - private Imp updatedImp(Imp imp, String productId, String zoneId, List errors) { - + private Imp updateImp(Imp imp, ExtImpTtx extImpTtx) { + final String productId = extImpTtx.getProductId(); return imp.toBuilder() - .video(updatedVideo(imp.getVideo(), productId, errors)) - .ext(createImpExt(productId, zoneId)) + .video(updatedVideo(imp.getVideo(), productId)) + .ext(createImpExt(productId, extImpTtx.getZoneId(), extImpTtx.getSiteId())) .build(); } - private static Video updatedVideo(Video video, String productId, List errors) { + private static Video updatedVideo(Video video, String productId) { if (video == null) { return null; } @@ -102,9 +103,8 @@ private static Video updatedVideo(Video video, String productId, List createRequest(BidRequest request, ExtImpTtx extImpTtx, Imp updatedFirstImp) { - final Site updatedSite = extImpTtx != null ? updateSite(request.getSite(), extImpTtx.getSiteId()) : null; - final BidRequest modifiedRequest = updateRequest(request, updatedSite, updatedFirstImp); + private HttpRequest createRequest(BidRequest request, Imp requestImp) { + final BidRequest modifiedRequest = request.toBuilder() + .imp(Collections.singletonList(requestImp)) + .build(); return HttpRequest.builder() .method(HttpMethod.POST) @@ -150,27 +152,6 @@ private HttpRequest createRequest(BidRequest request, ExtImpTtx extI .build(); } - private static Site updateSite(Site site, String siteId) { - return site == null ? null : site.toBuilder().id(siteId).build(); - } - - private static BidRequest updateRequest(BidRequest request, Site updatedSite, Imp updatedFirstImp) { - if (updatedSite == null && updatedFirstImp == null) { - return request; - } - final List requestImps = request.getImp(); - return request.toBuilder() - .site(updatedSite != null ? updatedSite : request.getSite()) - .imp(updatedFirstImp != null ? replaceFirstImp(requestImps, updatedFirstImp) : requestImps) - .build(); - } - - private static List replaceFirstImp(List imps, Imp firstImp) { - final List updatedImpList = new ArrayList<>(imps); - updatedImpList.set(0, firstImp); - return updatedImpList; - } - @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { @@ -198,7 +179,8 @@ private List bidsFromResponse(BidResponse bidResponse) { .collect(Collectors.toList()); } - private BidType getBidType(Bid bid) { + private BidType getBidType(Bid + bid) { try { final TtxBidExt ttxBidExt = mapper.mapper().convertValue(bid.getExt(), TtxBidExt.class); return ttxBidExt != null ? getBidTypeByTtx(ttxBidExt.getTtx()) : BidType.banner; diff --git a/src/main/resources/bidder-config/ttx.yaml b/src/main/resources/bidder-config/ttx.yaml index 454437d2bb8..c57ac2f027d 100644 --- a/src/main/resources/bidder-config/ttx.yaml +++ b/src/main/resources/bidder-config/ttx.yaml @@ -11,8 +11,6 @@ adapters: meta-info: maintainer-email: headerbidding@33across.com app-media-types: - - banner - - video site-media-types: - banner - video diff --git a/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java b/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java index 124ac7a0810..feceb706a09 100644 --- a/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java @@ -4,7 +4,6 @@ import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; @@ -70,15 +69,13 @@ public void makeHttpRequestsShouldAppendErrorIfImpExtCouldNotBeParsed() { assertThat(error.getMessage()).startsWith("Cannot deserialize instance of"); assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); }); - - assertThat(result.getValue()).hasSize(1); } @Test - public void makeHttpRequestsShouldSetSiteIdFromImpExt() { + public void makeHttpRequestsShouldNotUpdateSiteIfSiteNotPresent() { // given final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> - bidRequestBuilder.site(Site.builder().build()), identity()); + bidRequestBuilder.site(null), identity()); // when final Result>> result = ttxBidder.makeHttpRequests(bidRequest); @@ -88,8 +85,7 @@ public void makeHttpRequestsShouldSetSiteIdFromImpExt() { assertThat(result.getValue()).hasSize(1) .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .extracting(BidRequest::getSite) - .extracting(Site::getId) - .containsOnly("siteId"); + .containsNull(); } @Test @@ -108,37 +104,12 @@ public void makeHttpRequestsShouldNotCreateNewSiteIfSiteNotPresentInBidRequest() .containsNull(); } - @Test - public void makeHttpRequestsShouldGetDetailsOnlyFromFirstImpExt() { - // given - final BidRequest bidRequest = BidRequest.builder() - .site(Site.builder().build()) - .imp(asList( - givenImp(identity()), - givenImp(impBuilder -> impBuilder - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", "2", "3"))))))) - .build(); - - // when - final Result>> result = ttxBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .extracting(BidRequest::getSite) - .extracting(Site::getId) - .containsOnly("siteId"); - } - @Test public void makeHttpRequestsShouldChangeOnlyFirstImpExt() { // given final BidRequest bidRequest = BidRequest.builder() .imp(asList( - givenImp(identity()), - givenImp(impBuilder -> impBuilder - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3"))))))) + givenImp(identity()))) .build(); // when @@ -151,8 +122,7 @@ public void makeHttpRequestsShouldChangeOnlyFirstImpExt() { .flatExtracting(BidRequest::getImp) .extracting(Imp::getExt) .containsExactly( - mapper.valueToTree(TtxImpExt.of(TtxImpExtTtx.of("productId", "zoneId"))), - mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3")))); + mapper.valueToTree(TtxImpExt.of(TtxImpExtTtx.of("productId", "zoneId")))); } @Test @@ -255,8 +225,7 @@ public void makeBidsShouldReturnErrorIfNoBannerOrVideoPresent() { // then assertThat(result.getErrors()) - .containsExactly(BidderError.badInput("At least one of [banner, video] " - + "formats must be defined in Imp. None found")); + .containsExactly(BidderError.badInput("Imp ID 123 must have at least one of [Banner, Video] defined")); assertThat(result.getValue()).isEmpty(); } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json index 2a3a461727d..548d95c6df0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json @@ -72,4 +72,4 @@ "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request-1.json index ff0d24bf988..f82be234b00 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request-1.json @@ -20,7 +20,6 @@ } ], "site": { - "id": "site-id", "domain": "example.com", "page": "http://www.example.com", "publisher": { @@ -85,4 +84,4 @@ } } } -} \ No newline at end of file +} From eed8d5d7349c42d6bca795569671a958402f5e8a Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Thu, 28 Jan 2021 14:03:33 +0200 Subject: [PATCH 110/129] Fix setting site domain (#1120) --- .../server/auction/AuctionRequestFactory.java | 56 ++++++++----------- .../auction/AuctionRequestFactoryTest.java | 28 ++++++++-- .../ImplicitParametersExtractorTest.java | 8 +++ .../smaato/test-smaato-bid-request.json | 1 - .../test-video-appnexus-bid-request-1.json | 4 +- .../test-video-appnexus-bid-request-2.json | 4 +- .../video/test-video-appnexus-request.json | 2 +- .../test-auction-yieldlab-response.json | 1 - 8 files changed, 61 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java index 47cf8328a9b..de779a419fe 100644 --- a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -404,44 +403,37 @@ private void logWarnIfNoIp(String resolvedIp, String resolvedIpv6) { * and the request contains necessary info (domain, page). */ private Site populateSite(Site site, HttpServerRequest request) { - Site result = null; + final String page = site != null ? StringUtils.trimToNull(site.getPage()) : null; + final String updatedPage = page == null ? paramsExtractor.refererFrom(request) : null; + + final String domain = site != null ? StringUtils.trimToNull(site.getDomain()) : null; + final String updatedDomain = domain == null + ? getDomainOrNull(ObjectUtils.defaultIfNull(updatedPage, page)) + : null; - final String page = site != null ? site.getPage() : null; - final String domain = site != null ? site.getDomain() : null; final ExtSite siteExt = site != null ? site.getExt() : null; - final ObjectNode data = siteExt != null ? siteExt.getData() : null; - final boolean shouldSetExtAmp = siteExt == null || siteExt.getAmp() == null; - final ExtSite modifiedSiteExt = shouldSetExtAmp - ? ExtSite.of(0, data) + final ExtSite updatedSiteExt = siteExt == null || siteExt.getAmp() == null + ? ExtSite.of(0, getIfNotNull(siteExt, ExtSite::getData)) : null; - String referer = null; - String parsedDomain = null; - if (StringUtils.isBlank(page) || StringUtils.isBlank(domain)) { - referer = paramsExtractor.refererFrom(request); - if (StringUtils.isNotBlank(referer)) { - try { - parsedDomain = paramsExtractor.domainFrom(referer); - } catch (PreBidException e) { - logger.warn("Error occurred while populating bid request: {0}", e.getMessage()); - logger.debug("Error occurred while populating bid request", e); - } - } + if (updatedPage != null || updatedDomain != null || updatedSiteExt != null) { + return (site == null ? Site.builder() : site.toBuilder()) + // do not set page if domain was not parsed successfully + .page(domain == null && updatedDomain == null ? page : ObjectUtils.defaultIfNull(updatedPage, page)) + .domain(ObjectUtils.defaultIfNull(updatedDomain, domain)) + .ext(ObjectUtils.defaultIfNull(updatedSiteExt, siteExt)) + .build(); } - final boolean shouldModifyPageOrDomain = referer != null && parsedDomain != null; + return null; + } - if (shouldModifyPageOrDomain || shouldSetExtAmp) { - final Site.SiteBuilder builder = site == null ? Site.builder() : site.toBuilder(); - if (shouldModifyPageOrDomain) { - builder.domain(StringUtils.isNotBlank(domain) ? domain : parsedDomain); - builder.page(StringUtils.isNotBlank(page) ? page : referer); - } - if (shouldSetExtAmp) { - builder.ext(modifiedSiteExt); - } - result = builder.build(); + private String getDomainOrNull(String url) { + try { + return paramsExtractor.domainFrom(url); + } catch (PreBidException e) { + logger.warn("Error occurred while populating bid request: {0}", e.getMessage()); + return null; } - return result; } /** diff --git a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java index 4fffa12ae2a..edb01f4bf29 100644 --- a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java @@ -76,6 +76,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.apache.commons.lang3.StringUtils.EMPTY; @@ -652,9 +653,7 @@ public void shouldNotSetSitePageIfNoReferer() { @Test public void shouldNotSetSitePageIfDomainCouldNotBeDerived() { // given - givenBidRequest(BidRequest.builder() - .site(Site.builder().domain("home.com").build()) - .build()); + givenValidBidRequest(); given(paramsExtractor.refererFrom(any())).willReturn("http://not-valid-site"); given(paramsExtractor.domainFrom(anyString())).willThrow(new PreBidException("Couldn't derive domain")); @@ -664,7 +663,28 @@ public void shouldNotSetSitePageIfDomainCouldNotBeDerived() { // then assertThat(request.getSite()).isEqualTo( - Site.builder().domain("home.com").ext(ExtSite.of(0, null)).build()); + Site.builder().ext(ExtSite.of(0, null)).build()); + } + + @Test + public void shouldSetDomainFromPageInsteadOfReferer() { + // given + givenBidRequest(BidRequest.builder() + .site(Site.builder().page("http://page.site.com/page1.html").build()) + .build()); + + given(paramsExtractor.refererFrom(any())).willReturn("http://any-site/referer.html"); + given(paramsExtractor.domainFrom(anyString())).willReturn("site.com"); + + // when + final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest(); + + // then + verify(paramsExtractor).domainFrom(eq("http://page.site.com/page1.html")); + + assertThat(singleton(request.getSite())) + .extracting(Site::getPage, Site::getDomain) + .containsOnly(tuple("http://page.site.com/page1.html", "site.com")); } @Test diff --git a/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java b/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java index 39bf00d299a..12b57310bbc 100644 --- a/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java +++ b/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java @@ -85,6 +85,14 @@ public void refererFromShouldReturnRefererWithHttpSchemeIfRefererHeaderDoesNotCo assertThat(extractor.refererFrom(httpRequest)).isEqualTo("http://example.com"); } + @Test + public void domainFromShouldFailIfUrlIsMissing() { + assertThatCode(() -> extractor.domainFrom(null)) + .isInstanceOf(PreBidException.class) + .hasMessage("Invalid URL 'null': null") + .hasCauseInstanceOf(MalformedURLException.class); + } + @Test public void domainFromShouldFailIfUrlCouldNotBeParsed() { assertThatCode(() -> extractor.domainFrom("httpP://non_an_url")) diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json index 994b0c9a1aa..0f5f286397f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json @@ -17,7 +17,6 @@ } ], "site": { - "domain": "example.com", "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", "publisher": { "id": "11000" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json index 0831b9c42ab..b17816f4159 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json @@ -233,8 +233,8 @@ } ], "site": { - "domain": "example.com", - "page": "prebid.com", + "domain": "prebid.com", + "page": "https://prebid.com", "content": { "episode": 6, "title": "episodeName", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json index aacd75194eb..7fad685a3cd 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json @@ -26,8 +26,8 @@ } ], "site": { - "domain": "example.com", - "page": "prebid.com", + "domain": "prebid.com", + "page": "https://prebid.com", "content": { "episode": 6, "title": "episodeName", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json index 8de091e7bcb..f1d90701038 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json @@ -20,7 +20,7 @@ ] }, "site": { - "page": "prebid.com" + "page": "https://prebid.com" }, "user": { "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json index 28b402fc0f6..1f970ec91b1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json @@ -147,7 +147,6 @@ } }, "site": { - "domain": "example.com", "ext": { "amp": 0 }, From 439c01511720358fa07d2fa4ee71de9af5372a8e Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Thu, 28 Jan 2021 15:24:52 +0200 Subject: [PATCH 111/129] Fix logic around injecting global schain node (#1122) --- .../prebid/server/auction/SchainResolver.java | 50 +++++++++++++++---- .../server/auction/SchainResolverTest.java | 28 +++++++++++ 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/SchainResolver.java b/src/main/java/org/prebid/server/auction/SchainResolver.java index 755537932ec..2b06df94047 100644 --- a/src/main/java/org/prebid/server/auction/SchainResolver.java +++ b/src/main/java/org/prebid/server/auction/SchainResolver.java @@ -1,6 +1,7 @@ package org.prebid.server.auction; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Source; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; @@ -14,6 +15,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchainNode; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; import java.util.ArrayList; import java.util.Collections; @@ -46,7 +48,7 @@ public ExtRequestPrebidSchainSchain resolveForBidder(String bidder, BidRequest b bidderSchain = existingSchainOrNull(bidder, bidderSchain, schain); } - return enrich(ObjectUtils.defaultIfNull(bidderSchain, catchAllSchain)); + return enrich(ObjectUtils.defaultIfNull(bidderSchain, catchAllSchain), bidRequest); } private static ExtRequestPrebidSchainSchainNode globalNodeOrNull(String globalNodeString, JacksonMapper mapper) { @@ -80,22 +82,48 @@ private ExtRequestPrebidSchainSchain existingSchainOrNull(String bidder, return schainEntry.getSchain(); } - private ExtRequestPrebidSchainSchain enrich(ExtRequestPrebidSchainSchain schain) { + private ExtRequestPrebidSchainSchain enrich(ExtRequestPrebidSchainSchain bidderSpecificSchain, + BidRequest bidRequest) { + if (globalNode == null) { - return schain; + return bidderSpecificSchain; + } + + if (bidderSpecificSchain != null) { + return enrichSchainWithGlobalNode(bidderSpecificSchain); } - if (schain == null) { - return ExtRequestPrebidSchainSchain.of(null, null, Collections.singletonList(globalNode), null); + final ExtRequestPrebidSchainSchain requestSchain = requestSchain(bidRequest); + if (requestSchain != null) { + return enrichSchainWithGlobalNode(requestSchain); } - final List nodes = new ArrayList<>(ListUtils.emptyIfNull(schain.getNodes())); - nodes.add(globalNode); + return newSchainWithGlobalNode(); + } + + private ExtRequestPrebidSchainSchain requestSchain(BidRequest bidRequest) { + final Source source = bidRequest.getSource(); + final ExtSource extSource = source != null ? source.getExt() : null; + + return extSource != null ? extSource.getSchain() : null; + } + private ExtRequestPrebidSchainSchain enrichSchainWithGlobalNode(ExtRequestPrebidSchainSchain requestSchain) { return ExtRequestPrebidSchainSchain.of( - schain.getVer(), - schain.getComplete(), - nodes, - schain.getExt()); + requestSchain.getVer(), + requestSchain.getComplete(), + appendGlobalNode(requestSchain.getNodes()), + requestSchain.getExt()); + } + + private List appendGlobalNode(List nodes) { + final ArrayList updatedNodes = new ArrayList<>(ListUtils.emptyIfNull(nodes)); + updatedNodes.add(globalNode); + + return updatedNodes; + } + + private ExtRequestPrebidSchainSchain newSchainWithGlobalNode() { + return ExtRequestPrebidSchainSchain.of(null, null, Collections.singletonList(globalNode), null); } } diff --git a/src/test/java/org/prebid/server/auction/SchainResolverTest.java b/src/test/java/org/prebid/server/auction/SchainResolverTest.java index a07f7329e12..15109706c6e 100644 --- a/src/test/java/org/prebid/server/auction/SchainResolverTest.java +++ b/src/test/java/org/prebid/server/auction/SchainResolverTest.java @@ -1,6 +1,7 @@ package org.prebid.server.auction; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Source; import org.junit.Before; import org.junit.Test; import org.prebid.server.VertxTest; @@ -9,6 +10,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchainNode; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -133,4 +135,30 @@ public void shouldReturnSchainWithGlobalNodeOnly() { null, null, singletonList(globalNode), null); assertThat(schainResolver.resolveForBidder("bidder", bidRequest)).isEqualTo(expectedSchain); } + + @Test + public void shouldInjectGlobalNodeIntoExistingSchain() { + // given + schainResolver = SchainResolver.create( + "{\"asi\": \"pbshostcompany.com\", \"sid\":\"00001\"}", + jacksonMapper); + + final ExtRequestPrebidSchainSchainNode node = ExtRequestPrebidSchainSchainNode.of( + "asi", "sid", 1, "rid", "name", "domain", null); + final ExtRequestPrebidSchainSchain schain = ExtRequestPrebidSchainSchain.of( + "ver", 1, singletonList(node), null); + + final BidRequest bidRequest = BidRequest.builder() + .source(Source.builder() + .ext(ExtSource.of(schain)) + .build()) + .build(); + + // when and then + final ExtRequestPrebidSchainSchainNode globalNode = ExtRequestPrebidSchainSchainNode.of( + "pbshostcompany.com", "00001", null, null, null, null, null); + final ExtRequestPrebidSchainSchain expectedSchain = ExtRequestPrebidSchainSchain.of( + "ver", 1, asList(node, globalNode), null); + assertThat(schainResolver.resolveForBidder("bidder", bidRequest)).isEqualTo(expectedSchain); + } } From 8adab727d50876d925add68644f8d6ec1044ea52 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Fri, 29 Jan 2021 05:05:06 +0200 Subject: [PATCH 112/129] Prebid Server prepare release 1.54.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d702f3ccebf..29b64cfa8c4 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.54.0-SNAPSHOT + 1.54.0 prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 1.54.0 From eea1cdde99813e7375951afe880e07c3cdbe3637 Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Fri, 29 Jan 2021 05:05:19 +0200 Subject: [PATCH 113/129] Prebid Server prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 29b64cfa8c4..c69db5884b4 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.54.0 + 1.55.0-SNAPSHOT prebid-server Prebid Server (Server-side Header Bidding) @@ -15,7 +15,7 @@ scm:git:git@github.com:prebid/prebid-server-java.git - 1.54.0 + HEAD From 2865b07a1c27e5b5e3e49521d9d8a1f793a32544 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Fri, 29 Jan 2021 11:58:08 +0100 Subject: [PATCH 114/129] Apply stash --- .../server/bidder/adhese/AdheseBidder.java | 84 ++++++++++--------- .../bidder/adhese/AdheseBidderTest.java | 21 +++-- .../java/org/prebid/server/it/AdheseTest.java | 5 +- 3 files changed, 62 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java index be64ac8e752..17944af2b8f 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java +++ b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java @@ -15,6 +15,8 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; @@ -57,9 +59,9 @@ public class AdheseBidder implements Bidder { }; private static final String ORIGIN_BID = "JERLICIA"; - private static final String GDPR_QUERY_PARAMETER = "/xt"; - private static final String REFERER_QUERY_PARAMETER = "/xf"; - private static final String IFA_QUERY_PARAMETER = "/xz"; + private static final String GDPR_QUERY_PARAMETER = "xt"; + private static final String REFERER_QUERY_PARAMETER = "xf"; + private static final String IFA_QUERY_PARAMETER = "xz"; private final String endpointUrl; private final JacksonMapper mapper; @@ -82,12 +84,14 @@ public Result>> makeHttpRequests(BidRequest request) { return Result.withError(BidderError.badInput(e.getMessage())); } - final String uri = buildUrl(request, extImpAdhese); + final String uri = getUrl(extImpAdhese); + return Result.of(Collections.singletonList( HttpRequest.builder() .method(HttpMethod.POST) .uri(uri) + .body(buildBody(request, extImpAdhese)) .headers(replaceHeaders(request.getDevice())) .build()), Collections.emptyList()); @@ -110,36 +114,41 @@ private ExtImpAdhese parseImpExt(Imp imp) { } } - private String buildUrl(BidRequest request, ExtImpAdhese extImpAdhese) { - return String.format("%s%s%s%s%s%s", - getUrl(extImpAdhese), - getSlotParameter(extImpAdhese), - getTargetParameters(extImpAdhese), - getGdprParameter(request.getUser()), - getRefererParameter(request.getSite()), - getIfaParameter(request.getDevice())); + private String buildBody(BidRequest request, ExtImpAdhese extImpAdhese) { + JsonObject main = new JsonObject(); + + JsonArray slotsArray = new JsonArray(); + slotsArray.add(getSlotParameter(extImpAdhese)); + main.put("slots", slotsArray); + + JsonObject parameters = new JsonObject(); + insertTargetParameters(extImpAdhese, parameters); + insertGdprParameter(request.getUser(), parameters); + insertRefererParameter(request.getSite(), parameters); + insertIfaParameter(request.getDevice(), parameters); + main.put("parameters", parameters); + + return main.toString(); } private String getUrl(ExtImpAdhese extImpAdhese) { return endpointUrl.replace("{{AccountId}}", extImpAdhese.getAccount()); } - private static String getSlotParameter(ExtImpAdhese extImpAdhese) { - return String.format("/sl%s-%s", + private static JsonObject getSlotParameter(ExtImpAdhese extImpAdhese) { + JsonObject slots = new JsonObject(); + slots.put("slotname", String.format("%s-%s", HttpUtil.encodeUrl(extImpAdhese.getLocation()), - HttpUtil.encodeUrl(extImpAdhese.getFormat())); + HttpUtil.encodeUrl(extImpAdhese.getFormat()))); + return slots; } - private String getTargetParameters(ExtImpAdhese extImpAdhese) { + private void insertTargetParameters(ExtImpAdhese extImpAdhese, JsonObject parameters) { final JsonNode targets = extImpAdhese.getTargets(); - if (targets == null || targets.isNull()) { - return ""; + if (!(targets == null || targets.isNull())) { + final Map> targetParameters = parseTargetParametersAndSort(targets); + targetParameters.forEach((k, v) -> parameters.put(k, new JsonArray(v))); } - - final Map> targetParameters = parseTargetParametersAndSort(targets); - return targetParameters.entrySet().stream() - .map(entry -> createPartOrUrl(entry.getKey(), entry.getValue())) - .collect(Collectors.joining()); } private Map> parseTargetParametersAndSort(JsonNode targets) { @@ -148,31 +157,26 @@ private Map> parseTargetParametersAndSort(JsonNode targets) })); } - private static String createPartOrUrl(String key, List values) { - final String formattedValues = String.join(";", values); - return String.format("/%s%s", HttpUtil.encodeUrl(key), formattedValues); - } - - private static String getGdprParameter(User user) { + private static void insertGdprParameter(User user, JsonObject parameters) { final ExtUser extUser = user != null ? user.getExt() : null; final String consent = extUser != null ? extUser.getConsent() : null; - return StringUtils.isNotBlank(consent) - ? String.format("%s%s", GDPR_QUERY_PARAMETER, consent) - : ""; + if (StringUtils.isNotBlank(consent)) { + parameters.put(GDPR_QUERY_PARAMETER, new JsonArray(Collections.singletonList(consent))); + } } - private static String getRefererParameter(Site site) { + private static void insertRefererParameter(Site site, JsonObject parameters) { final String page = site != null ? site.getPage() : null; - return StringUtils.isNotBlank(page) - ? String.format("%s%s", REFERER_QUERY_PARAMETER, HttpUtil.encodeUrl(page)) - : ""; + if (StringUtils.isNotBlank(page)) { + parameters.put(REFERER_QUERY_PARAMETER, new JsonArray(Collections.singletonList(page))); + } } - private static String getIfaParameter(Device device) { + private static void insertIfaParameter(Device device, JsonObject parameters) { final String ifa = device != null ? device.getIfa() : null; - return StringUtils.isNotBlank(ifa) - ? String.format("%s%s", IFA_QUERY_PARAMETER, HttpUtil.encodeUrl(ifa)) - : ""; + if (StringUtils.isNotBlank(ifa)) { + parameters.put(IFA_QUERY_PARAMETER, new JsonArray(Collections.singletonList(ifa))); + } } @Override diff --git a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java index 67b32cada5c..103059848db 100644 --- a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java @@ -117,8 +117,10 @@ public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequest // then assertThat(result.getValue()) .extracting(HttpRequest::getUri) - .containsOnly("https://ads-demo.adhese.com/json/sl_adhese_prebid_demo_-leaderboard/ag55/cigent;brussels" - + "/tlall/xtdummy/xzdum-my"); + .containsOnly("https://ads-demo.adhese.com/json"); + assertThat(result.getValue()) + .extracting(HttpRequest::getBody) + .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}],\"parameters\":{\"ag\":[\"55\"],\"ci\":[\"gent\",\"brussels\"],\"tl\":[\"all\"],\"xt\":[\"dummy\"],\"xz\":[\"dum-my\"]}}"); } @Test @@ -139,7 +141,10 @@ public void makeHttpRequestsShouldModifyIncomingRequestWithIfaParameter() { // then assertThat(result.getValue()) .extracting(HttpRequest::getUri) - .containsOnly("https://ads-demo.adhese.com/json/sl_adhese_prebid_demo_-leaderboard/xzifaValue"); + .containsOnly("https://ads-demo.adhese.com/json"); + assertThat(result.getValue()) + .extracting(HttpRequest::getBody) + .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}],\"parameters\":{\"xz\":[\"ifaValue\"]}}"); } @Test @@ -160,7 +165,10 @@ public void makeHttpRequestsShouldModifyIncomingRequestWithRefererParameter() { // then assertThat(result.getValue()) .extracting(HttpRequest::getUri) - .containsOnly("https://ads-demo.adhese.com/json/sl_adhese_prebid_demo_-leaderboard/xfpageValue"); + .containsOnly("https://ads-demo.adhese.com/json"); + assertThat(result.getValue()) + .extracting(HttpRequest::getBody) + .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}],\"parameters\":{\"xf\":[\"pageValue\"]}}"); } @Test @@ -182,7 +190,10 @@ public void makeHttpRequestsShouldNotModifyIncomingRequestIfTargetsNotPresent() // then assertThat(result.getValue()) .extracting(HttpRequest::getUri) - .containsOnly("https://ads-demo.adhese.com/json/sl_adhese_prebid_demo_-leaderboard/xtdummy"); + .containsOnly("https://ads-demo.adhese.com/json"); + assertThat(result.getValue()) + .extracting(HttpRequest::getBody) + .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}],\"parameters\":{\"xt\":[\"dummy\"]}}"); } @Test diff --git a/src/test/java/org/prebid/server/it/AdheseTest.java b/src/test/java/org/prebid/server/it/AdheseTest.java index 5c108aaff67..e1b8b21ff14 100644 --- a/src/test/java/org/prebid/server/it/AdheseTest.java +++ b/src/test/java/org/prebid/server/it/AdheseTest.java @@ -20,11 +20,10 @@ public class AdheseTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdhese() throws IOException, JSONException { - WIRE_MOCK_RULE.stubFor(WireMock.get(WireMock.urlPathEqualTo("/adhese-exchange/sl_adhese_prebid_demo_-" - + "leaderboard/ag55/cigent;brussels/tlall/xtconsentValue/xfhttp%3A%2F%2Fwww.example.com/xzifaId")) + WIRE_MOCK_RULE.stubFor(WireMock.post(WireMock.urlPathEqualTo("/adhese-exchange")) .withHeader("Accept", WireMock.equalTo("application/json")) .withHeader("Content-Type", WireMock.equalTo("application/json;charset=UTF-8")) - .withRequestBody(WireMock.absent()) + .withRequestBody(WireMock.equalToJson("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}],\"parameters\":{\"ag\":[\"55\"],\"ci\":[\"gent\",\"brussels\"],\"tl\":[\"all\"],\"xt\":[\"consentValue\"],\"xf\":[\"http://www.example.com\"],\"xz\":[\"ifaId\"]}}")) .willReturn(WireMock.aResponse().withBody(jsonFrom("openrtb2/adhese/test-adhese-bid-response.json")))); // pre-bid cache From 05b7ea27cfc137000bb11c99ca63f7909090d5fc Mon Sep 17 00:00:00 2001 From: pragnesh Date: Fri, 29 Jan 2021 17:01:05 +0530 Subject: [PATCH 115/129] follow same convention for stored request example like stored impression (#1123) --- docs/developers/stored-requests.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developers/stored-requests.md b/docs/developers/stored-requests.md index e76cddb6811..8eac8fe9bc8 100644 --- a/docs/developers/stored-requests.md +++ b/docs/developers/stored-requests.md @@ -122,7 +122,7 @@ So far, our examples have only used Stored Imp data. However, Stored Requests are also allowed on the [BidRequest](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=15). These work exactly the same way, but support storing properties like timeouts and price granularity. -For example, assume the following `stored-requests/stored-request.json`: +For example, assume the following `stored-requests/{id}.json`: ```json { @@ -148,7 +148,7 @@ Then HTTP request like: "ext": { "prebid": { "storedrequest": { - "id": "stored-request.json" + "id": "{id}" } } } From 1927a30a7d47f11fcef8042274af10e1658f79fb Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Fri, 29 Jan 2021 12:52:20 +0100 Subject: [PATCH 116/129] Use obj serialization --- .../server/bidder/adhese/AdheseBidder.java | 74 ++++++----------- .../adhese/model/AdheseRequestBody.java | 83 +++++++++++++++++++ .../bidder/adhese/AdheseBidderTest.java | 7 +- 3 files changed, 111 insertions(+), 53 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java diff --git a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java index 17944af2b8f..1119565da8d 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java +++ b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java @@ -11,21 +11,13 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.adhese.model.AdheseBid; -import org.prebid.server.bidder.adhese.model.AdheseOriginData; -import org.prebid.server.bidder.adhese.model.AdheseResponseExt; -import org.prebid.server.bidder.adhese.model.Cpm; -import org.prebid.server.bidder.adhese.model.CpmValues; -import org.prebid.server.bidder.adhese.model.Prebid; +import org.prebid.server.bidder.adhese.model.*; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; @@ -47,7 +39,6 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; -import java.util.stream.Collectors; /** * Adhese {@link Bidder} implementation. @@ -91,7 +82,7 @@ public Result>> makeHttpRequests(BidRequest request) { HttpRequest.builder() .method(HttpMethod.POST) .uri(uri) - .body(buildBody(request, extImpAdhese)) + .body(mapper.encode(buildBody(request, extImpAdhese))) .headers(replaceHeaders(request.getDevice())) .build()), Collections.emptyList()); @@ -114,41 +105,30 @@ private ExtImpAdhese parseImpExt(Imp imp) { } } - private String buildBody(BidRequest request, ExtImpAdhese extImpAdhese) { - JsonObject main = new JsonObject(); + private AdheseRequestBody buildBody(BidRequest request, ExtImpAdhese extImpAdhese) { + AdheseRequestBody body = new AdheseRequestBody(); + body.slots.add(AdheseRequestBody.Slot.create(getSlotParameter(extImpAdhese))); + body.parameters.putAll(getTargetParameters(extImpAdhese)); + body.parameters.putAll(getGdprParameter(request.getUser())); + body.parameters.putAll(getRefererParameter(request.getSite())); + body.parameters.putAll(getIfaParameter(request.getDevice())); - JsonArray slotsArray = new JsonArray(); - slotsArray.add(getSlotParameter(extImpAdhese)); - main.put("slots", slotsArray); - - JsonObject parameters = new JsonObject(); - insertTargetParameters(extImpAdhese, parameters); - insertGdprParameter(request.getUser(), parameters); - insertRefererParameter(request.getSite(), parameters); - insertIfaParameter(request.getDevice(), parameters); - main.put("parameters", parameters); - - return main.toString(); + return body; } private String getUrl(ExtImpAdhese extImpAdhese) { return endpointUrl.replace("{{AccountId}}", extImpAdhese.getAccount()); } - private static JsonObject getSlotParameter(ExtImpAdhese extImpAdhese) { - JsonObject slots = new JsonObject(); - slots.put("slotname", String.format("%s-%s", + private static String getSlotParameter(ExtImpAdhese extImpAdhese) { + return String.format("%s-%s", HttpUtil.encodeUrl(extImpAdhese.getLocation()), - HttpUtil.encodeUrl(extImpAdhese.getFormat()))); - return slots; + HttpUtil.encodeUrl(extImpAdhese.getFormat())); } - private void insertTargetParameters(ExtImpAdhese extImpAdhese, JsonObject parameters) { + private Map> getTargetParameters(ExtImpAdhese extImpAdhese) { final JsonNode targets = extImpAdhese.getTargets(); - if (!(targets == null || targets.isNull())) { - final Map> targetParameters = parseTargetParametersAndSort(targets); - targetParameters.forEach((k, v) -> parameters.put(k, new JsonArray(v))); - } + return targets == null || targets.isNull() ? Collections.emptyMap() : parseTargetParametersAndSort(targets); } private Map> parseTargetParametersAndSort(JsonNode targets) { @@ -157,26 +137,26 @@ private Map> parseTargetParametersAndSort(JsonNode targets) })); } - private static void insertGdprParameter(User user, JsonObject parameters) { + private static Map> getGdprParameter(User user) { final ExtUser extUser = user != null ? user.getExt() : null; final String consent = extUser != null ? extUser.getConsent() : null; - if (StringUtils.isNotBlank(consent)) { - parameters.put(GDPR_QUERY_PARAMETER, new JsonArray(Collections.singletonList(consent))); - } + return StringUtils.isNotBlank(consent) ? + Collections.singletonMap(GDPR_QUERY_PARAMETER, Collections.singletonList(consent)) : + Collections.emptyMap(); } - private static void insertRefererParameter(Site site, JsonObject parameters) { + private static Map> getRefererParameter(Site site) { final String page = site != null ? site.getPage() : null; - if (StringUtils.isNotBlank(page)) { - parameters.put(REFERER_QUERY_PARAMETER, new JsonArray(Collections.singletonList(page))); - } + return StringUtils.isNotBlank(page) ? + Collections.singletonMap(REFERER_QUERY_PARAMETER, Collections.singletonList(page)) : + Collections.emptyMap(); } - private static void insertIfaParameter(Device device, JsonObject parameters) { + private static Map> getIfaParameter(Device device) { final String ifa = device != null ? device.getIfa() : null; - if (StringUtils.isNotBlank(ifa)) { - parameters.put(IFA_QUERY_PARAMETER, new JsonArray(Collections.singletonList(ifa))); - } + return StringUtils.isNotBlank(ifa) ? + Collections.singletonMap(IFA_QUERY_PARAMETER, Collections.singletonList(ifa)) : + Collections.emptyMap(); } @Override diff --git a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java new file mode 100644 index 00000000000..ceaf0c9298f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java @@ -0,0 +1,83 @@ +package org.prebid.server.bidder.adhese.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonUnwrapped; + +import java.util.*; + +import static com.fasterxml.jackson.annotation.JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY; + +public class AdheseRequestBody { + public static class Slot { + + public static Slot create(String slotname) { + Slot slot = new Slot(); + slot.slotname = slotname; + return slot; + } + + private String slotname; + + public String getSlotname() { + return slotname; + } + + @Override + public String toString() { + return "slot: " + slotname; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Slot)) return false; + Slot slot = (Slot) o; + return Objects.equals(slotname, slot.slotname); + } + + @Override + public int hashCode() { + return Objects.hash(slotname); + } + } + + public List slots = new ArrayList<>(); + + @JsonUnwrapped + @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY) + public Map> parameters = new TreeMap<>(); + + private Integer maxAds; + + public Integer getMaxAds() { + return maxAds; + } + + public void setMaxAds(Integer maxAds) { + this.maxAds = maxAds; + } + + @Override + public String toString() { + return "RequestBody{" + + "slots=" + slots + + ", parameters=" + parameters + + ", maxAds=" + maxAds + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AdheseRequestBody)) return false; + AdheseRequestBody that = (AdheseRequestBody) o; + return Objects.equals(slots, that.slots) && + Objects.equals(parameters, that.parameters) && + Objects.equals(maxAds, that.maxAds); + } + + @Override + public int hashCode() { + return Objects.hash(slots, parameters, maxAds); + } +} diff --git a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java index 103059848db..072dfdbee4a 100644 --- a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java @@ -15,12 +15,7 @@ import org.junit.Before; import org.junit.Test; import org.prebid.server.VertxTest; -import org.prebid.server.bidder.adhese.model.AdheseBid; -import org.prebid.server.bidder.adhese.model.AdheseOriginData; -import org.prebid.server.bidder.adhese.model.AdheseResponseExt; -import org.prebid.server.bidder.adhese.model.Cpm; -import org.prebid.server.bidder.adhese.model.CpmValues; -import org.prebid.server.bidder.adhese.model.Prebid; +import org.prebid.server.bidder.adhese.model.*; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; From 2d8eecbd74a26bf2cbe1052e0dc124f9ec5b4ba1 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Fri, 29 Jan 2021 13:31:33 +0100 Subject: [PATCH 117/129] Leave out unnecessary field --- .../bidder/adhese/model/AdheseRequestBody.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java index ceaf0c9298f..4631277f0d7 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java +++ b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java @@ -47,22 +47,11 @@ public int hashCode() { @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY) public Map> parameters = new TreeMap<>(); - private Integer maxAds; - - public Integer getMaxAds() { - return maxAds; - } - - public void setMaxAds(Integer maxAds) { - this.maxAds = maxAds; - } - @Override public String toString() { return "RequestBody{" + "slots=" + slots + ", parameters=" + parameters + - ", maxAds=" + maxAds + '}'; } @@ -72,12 +61,11 @@ public boolean equals(Object o) { if (!(o instanceof AdheseRequestBody)) return false; AdheseRequestBody that = (AdheseRequestBody) o; return Objects.equals(slots, that.slots) && - Objects.equals(parameters, that.parameters) && - Objects.equals(maxAds, that.maxAds); + Objects.equals(parameters, that.parameters); } @Override public int hashCode() { - return Objects.hash(slots, parameters, maxAds); + return Objects.hash(slots, parameters); } } From cd172bd0a14416aa1abda8242fe40979d72a78c6 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Fri, 29 Jan 2021 13:39:33 +0100 Subject: [PATCH 118/129] throw away unnecessary stuff --- .../adhese/model/AdheseRequestBody.java | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java index 4631277f0d7..ff47d6479c0 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java +++ b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java @@ -1,12 +1,7 @@ package org.prebid.server.bidder.adhese.model; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonUnwrapped; - import java.util.*; -import static com.fasterxml.jackson.annotation.JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY; - public class AdheseRequestBody { public static class Slot { @@ -43,8 +38,6 @@ public int hashCode() { public List slots = new ArrayList<>(); - @JsonUnwrapped - @JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY) public Map> parameters = new TreeMap<>(); @Override @@ -54,18 +47,4 @@ public String toString() { ", parameters=" + parameters + '}'; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof AdheseRequestBody)) return false; - AdheseRequestBody that = (AdheseRequestBody) o; - return Objects.equals(slots, that.slots) && - Objects.equals(parameters, that.parameters); - } - - @Override - public int hashCode() { - return Objects.hash(slots, parameters); - } } From 4eaa2142d80a7b7cb21dd7ac9562371789d7ac23 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Fri, 29 Jan 2021 14:37:00 +0100 Subject: [PATCH 119/129] Comply with code style --- .../server/bidder/adhese/AdheseBidder.java | 33 ++++++++++-------- .../adhese/model/AdheseRequestBody.java | 23 +++++++++---- .../bidder/adhese/AdheseBidderTest.java | 34 ++++++++++++++----- 3 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java index 1119565da8d..75e2ff43430 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java +++ b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java @@ -17,7 +17,13 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.adhese.model.*; +import org.prebid.server.bidder.adhese.model.AdheseBid; +import org.prebid.server.bidder.adhese.model.AdheseOriginData; +import org.prebid.server.bidder.adhese.model.AdheseRequestBody; +import org.prebid.server.bidder.adhese.model.AdheseResponseExt; +import org.prebid.server.bidder.adhese.model.Cpm; +import org.prebid.server.bidder.adhese.model.CpmValues; +import org.prebid.server.bidder.adhese.model.Prebid; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; @@ -77,7 +83,6 @@ public Result>> makeHttpRequests(BidRequest request) { final String uri = getUrl(extImpAdhese); - return Result.of(Collections.singletonList( HttpRequest.builder() .method(HttpMethod.POST) @@ -137,26 +142,26 @@ private Map> parseTargetParametersAndSort(JsonNode targets) })); } - private static Map> getGdprParameter(User user) { + private static Map> getGdprParameter(User user) { final ExtUser extUser = user != null ? user.getExt() : null; final String consent = extUser != null ? extUser.getConsent() : null; - return StringUtils.isNotBlank(consent) ? - Collections.singletonMap(GDPR_QUERY_PARAMETER, Collections.singletonList(consent)) : - Collections.emptyMap(); + return StringUtils.isNotBlank(consent) + ? Collections.singletonMap(GDPR_QUERY_PARAMETER, Collections.singletonList(consent)) + : Collections.emptyMap(); } - private static Map> getRefererParameter(Site site) { + private static Map> getRefererParameter(Site site) { final String page = site != null ? site.getPage() : null; - return StringUtils.isNotBlank(page) ? - Collections.singletonMap(REFERER_QUERY_PARAMETER, Collections.singletonList(page)) : - Collections.emptyMap(); + return StringUtils.isNotBlank(page) + ? Collections.singletonMap(REFERER_QUERY_PARAMETER, Collections.singletonList(page)) + : Collections.emptyMap(); } - private static Map> getIfaParameter(Device device) { + private static Map> getIfaParameter(Device device) { final String ifa = device != null ? device.getIfa() : null; - return StringUtils.isNotBlank(ifa) ? - Collections.singletonMap(IFA_QUERY_PARAMETER, Collections.singletonList(ifa)) : - Collections.emptyMap(); + return StringUtils.isNotBlank(ifa) + ? Collections.singletonMap(IFA_QUERY_PARAMETER, Collections.singletonList(ifa)) + : Collections.emptyMap(); } @Override diff --git a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java index ff47d6479c0..1f8d58bc7d0 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java +++ b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java @@ -1,8 +1,13 @@ package org.prebid.server.bidder.adhese.model; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; public class AdheseRequestBody { + public static class Slot { public static Slot create(String slotname) { @@ -24,8 +29,12 @@ public String toString() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Slot)) return false; + if (this == o) { + return true; + } + if (!(o instanceof Slot)) { + return false; + } Slot slot = (Slot) o; return Objects.equals(slotname, slot.slotname); } @@ -42,9 +51,9 @@ public int hashCode() { @Override public String toString() { - return "RequestBody{" + - "slots=" + slots + - ", parameters=" + parameters + - '}'; + return "RequestBody{" + + "slots=" + slots + + ", parameters=" + parameters + + '}'; } } diff --git a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java index 072dfdbee4a..eebdb5446e0 100644 --- a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java @@ -15,7 +15,12 @@ import org.junit.Before; import org.junit.Test; import org.prebid.server.VertxTest; -import org.prebid.server.bidder.adhese.model.*; +import org.prebid.server.bidder.adhese.model.AdheseBid; +import org.prebid.server.bidder.adhese.model.AdheseOriginData; +import org.prebid.server.bidder.adhese.model.AdheseResponseExt; +import org.prebid.server.bidder.adhese.model.Cpm; +import org.prebid.server.bidder.adhese.model.CpmValues; +import org.prebid.server.bidder.adhese.model.Prebid; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; @@ -100,8 +105,11 @@ public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequest .ext(ExtUser.builder().consent("dummy").build()) .build()) .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdhese.of("demo", - "_adhese_prebid_demo_", "leaderboard", mapper.convertValue(targets, JsonNode.class))))) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdhese.of( + "demo", + "_adhese_prebid_demo_", + "leaderboard", + mapper.convertValue(targets, JsonNode.class))))) .build())) .device(Device.builder().ifa("dum-my").build()) .build(); @@ -115,7 +123,9 @@ public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequest .containsOnly("https://ads-demo.adhese.com/json"); assertThat(result.getValue()) .extracting(HttpRequest::getBody) - .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}],\"parameters\":{\"ag\":[\"55\"],\"ci\":[\"gent\",\"brussels\"],\"tl\":[\"all\"],\"xt\":[\"dummy\"],\"xz\":[\"dum-my\"]}}"); + .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}]," + + "\"parameters\":{\"ag\":[\"55\"],\"ci\":[\"gent\",\"brussels\"]," + + "\"tl\":[\"all\"],\"xt\":[\"dummy\"],\"xz\":[\"dum-my\"]}}"); } @Test @@ -139,7 +149,8 @@ public void makeHttpRequestsShouldModifyIncomingRequestWithIfaParameter() { .containsOnly("https://ads-demo.adhese.com/json"); assertThat(result.getValue()) .extracting(HttpRequest::getBody) - .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}],\"parameters\":{\"xz\":[\"ifaValue\"]}}"); + .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}]," + + "\"parameters\":{\"xz\":[\"ifaValue\"]}}"); } @Test @@ -163,7 +174,8 @@ public void makeHttpRequestsShouldModifyIncomingRequestWithRefererParameter() { .containsOnly("https://ads-demo.adhese.com/json"); assertThat(result.getValue()) .extracting(HttpRequest::getBody) - .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}],\"parameters\":{\"xf\":[\"pageValue\"]}}"); + .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}]," + + "\"parameters\":{\"xf\":[\"pageValue\"]}}"); } @Test @@ -174,8 +186,11 @@ public void makeHttpRequestsShouldNotModifyIncomingRequestIfTargetsNotPresent() .ext(ExtUser.builder().consent("dummy").build()) .build()) .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdhese.of("demo", - "_adhese_prebid_demo_", "leaderboard", mapper.convertValue(null, JsonNode.class))))) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdhese.of( + "demo", + "_adhese_prebid_demo_", + "leaderboard", + mapper.convertValue(null, JsonNode.class))))) .build())) .build(); @@ -188,7 +203,8 @@ public void makeHttpRequestsShouldNotModifyIncomingRequestIfTargetsNotPresent() .containsOnly("https://ads-demo.adhese.com/json"); assertThat(result.getValue()) .extracting(HttpRequest::getBody) - .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}],\"parameters\":{\"xt\":[\"dummy\"]}}"); + .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}]," + + "\"parameters\":{\"xt\":[\"dummy\"]}}"); } @Test From f08035a2f439abe3736493394e084535eabe4aa7 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Fri, 29 Jan 2021 15:43:43 +0200 Subject: [PATCH 120/129] Support imp.ext.prebid.bidder. as a new place for bidder parameters (#976) * Moved bidder parameters from imp.ext to imp.ext.prebid.bidder before proceeding with an auction * Merge bidder parameters instead of replacing whole object * Adjust bid request validation to look into new location of bidder parameters * Update core exchange logic to look for bidder parameters in new location * Update stored response processing to look for bidders in new location * Update Rubicon bidder to look for cpmoverride in new location * Do not pass empty prebid sub-ext to bidders * Fix integration tests after moving bidder parameters to a new place * Tidy up * Update references to imp.ext. with a new location imp.ext.prebid.bidder. --- docs/bidders/appnexus.md | 12 +- docs/bidders/openx.md | 22 +- docs/bidders/pubmatic.md | 10 +- docs/bidders/pubnative.md | 16 +- docs/developers/add-new-bidder.md | 4 +- docs/developers/stored-requests.md | 16 +- docs/endpoints/info/bidders.md | 2 +- docs/endpoints/info/bidders/bidderName.md | 2 +- docs/endpoints/openrtb2/amp.md | 16 +- docs/endpoints/openrtb2/auction.md | 30 +- .../server/auction/AuctionRequestFactory.java | 116 +++- .../server/auction/ExchangeService.java | 50 +- .../auction/StoredResponseProcessor.java | 86 +-- .../bidder/adoppler/AdopplerBidder.java | 2 +- .../server/bidder/rubicon/RubiconBidder.java | 29 +- .../proto/RubiconImpExtPrebidBidder.java | 9 - .../RubiconImpExtPrebidRubiconDebug.java | 9 - .../sharethrough/SharethroughRequestUtil.java | 2 +- .../ext/request/rubicon/ExtImpRubicon.java | 2 + .../request/rubicon/ExtImpRubiconDebug.java | 9 + .../server/validation/RequestValidator.java | 55 +- .../auction/AuctionRequestFactoryTest.java | 106 +++- .../server/auction/ExchangeServiceTest.java | 97 ++- .../auction/StoredResponseProcessorTest.java | 577 ++++++++++++------ .../bidder/adoppler/AdopplerBidderTest.java | 2 +- .../bidder/rubicon/RubiconBidderTest.java | 13 +- .../validation/RequestValidatorTest.java | 96 ++- .../adman/test-auction-adman-request.json | 16 +- .../adocean/test-auction-adocean-request.json | 12 +- .../test-auction-adocean-response.json | 12 +- .../avocet/test-auction-avocet-request.json | 8 +- .../avocet/test-auction-avocet-response.json | 8 +- .../beintoo/test-auction-beintoo-request.json | 8 +- .../test-auction-beintoo-response.json | 8 +- .../test-auction-datablocks-request.json | 20 +- .../test-auction-datablocks-response.json | 20 +- .../test-auction-emxdigital-request.json | 10 +- .../test-auction-emxdigital-response.json | 8 +- .../kubient/test-auction-kubient-request.json | 8 +- .../test-auction-kubient-response.json | 8 +- ...test-auction-rubicon-appnexus-request.json | 134 ++-- ...est-auction-rubicon-appnexus-response.json | 146 +++-- .../test-auction-sharethrough-request.json | 20 +- .../test-auction-sharethrough-response.json | 20 +- .../test-auction-smartadserver-request.json | 14 +- .../test-auction-smartadserver-response.json | 14 +- ...est-auction-triplelift-native-request.json | 8 +- ...st-auction-triplelift-native-response.json | 8 +- .../test-auction-yieldlab-request.json | 22 +- .../test-auction-yieldlab-response.json | 20 +- .../test-auction-zeroclickfraud-request.json | 20 +- .../test-auction-zeroclickfraud-response.json | 20 +- 52 files changed, 1286 insertions(+), 696 deletions(-) delete mode 100644 src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidBidder.java delete mode 100644 src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidRubiconDebug.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubiconDebug.java diff --git a/docs/bidders/appnexus.md b/docs/bidders/appnexus.md index 434ba8048f0..7cd126fda71 100644 --- a/docs/bidders/appnexus.md +++ b/docs/bidders/appnexus.md @@ -37,9 +37,13 @@ that would match with the test creative. }] }, "ext": { - "appnexus": { - "placementId": 13144370 - } - } + "prebid": { + "bidder":{ + "appnexus": { + "placementId": 13144370 + } + } + } + } }] ``` \ No newline at end of file diff --git a/docs/bidders/openx.md b/docs/bidders/openx.md index f7fc0dab04f..f8f3b9fd492 100644 --- a/docs/bidders/openx.md +++ b/docs/bidders/openx.md @@ -34,9 +34,13 @@ If you have any questions regarding setting up, please reach out to your account ] }, "ext": { - "openx": { - "delDomain": "mobile-d.openx.net", - "unit": "541028953" + "prebid": { + "bidder":{ + "openx": { + "delDomain": "mobile-d.openx.net", + "unit": "541028953" + } + } } } } @@ -56,10 +60,14 @@ If you have any questions regarding setting up, please reach out to your account ] }, "ext": { - "openx": { - "unit": "540949380", - "delDomain": "sademo-d.openx.net" - }, + "prebid": { + "bidder":{ + "openx": { + "unit": "540949380", + "delDomain": "sademo-d.openx.net" + } + } + } } } ``` \ No newline at end of file diff --git a/docs/bidders/pubmatic.md b/docs/bidders/pubmatic.md index 610108b2e07..503c8e51026 100644 --- a/docs/bidders/pubmatic.md +++ b/docs/bidders/pubmatic.md @@ -23,9 +23,13 @@ and sizes that would match with the test creative. ] }, "ext":{ - "pubmatic":{ - "publisherId":“156276”, - "adSlot":"pubmatic_test" + "prebid": { + "bidder"{ + "pubmatic":{ + "publisherId":“156276”, + "adSlot":"pubmatic_test" + } + } } } } diff --git a/docs/bidders/pubnative.md b/docs/bidders/pubnative.md index ad421069fb2..17704e91305 100644 --- a/docs/bidders/pubnative.md +++ b/docs/bidders/pubnative.md @@ -10,9 +10,9 @@ Please see [documentation](https://developers.pubnative.net/docs/prebid-adding-p ## Configuration -- bidder should be always set to "pubnative" (`imp.ext.pubnative`) -- zone_id (int) should be always set to 1, unless special use case agreed with our account manager. (`imp.ext.pubnative.zone_id`) -- app_auth_token (string) is unique per publisher app. Please contact our account manager to obtain yours. (`imp.ext.pubnative.app_auth_token`) +- bidder should be always set to "pubnative" (`imp.ext.prebid.bidder.pubnative`) +- zone_id (int) should be always set to 1, unless special use case agreed with our account manager. (`imp.ext.prebid.bidder.pubnative.zone_id`) +- app_auth_token (string) is unique per publisher app. Please contact our account manager to obtain yours. (`imp.ext.prebid.bidder.pubnative.app_auth_token`) An example is illustrated in a section below. @@ -44,9 +44,13 @@ The following json can be used to do a request to prebid server for verifying it ] }, "ext": { - "pubnative": { - "zone_id": 1, - "app_auth_token": "b620e282f3c74787beedda34336a4821" + "prebid": { + "bidder": { + "pubnative": { + "zone_id": 1, + "app_auth_token": "b620e282f3c74787beedda34336a4821" + } + } } } } diff --git a/docs/developers/add-new-bidder.md b/docs/developers/add-new-bidder.md index 1bb8ac8b3dd..47d99dd930e 100644 --- a/docs/developers/add-new-bidder.md +++ b/docs/developers/add-new-bidder.md @@ -16,7 +16,7 @@ Throughout the rest of this document, substitute `{bidder}` with the name you've Bidders may define their own APIs for Publishers pass custom values. It is _strongly encouraged_ that these not duplicate values already present in the [OpenRTB 2.5 spec](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf). -Publishers will send values for these parameters in `request.imp[i].ext.{bidder}` of +Publishers will send values for these parameters in `request.imp[i].ext.prebid.bidder.{bidder}` of [the Auction endpoint](../endpoints/openrtb2/auction.md). Prebid Server will preprocess these so that your bidder will access them at `request.imp[i].ext.bidder`--regardless of what your `{bidder}` name is. @@ -134,7 +134,7 @@ We expect to see at least 90% code coverage on each bidder. Then `POST` an OpenRTB Request to `http://localhost:8080/openrtb2/auction`. -If at least one `request.imp[i].ext.{bidder}` is defined in your Request, then your bidder should be called. +If at least one `request.imp[i].ext.prebid.bidder.{bidder}` is defined in your Request, then your bidder should be called. To test user syncs, [save a UID](../endpoints/setuid.md) using the FamilyName of your Bidder. The next time you use `/openrtb2/auction`, the OpenRTB request sent to your Bidder should have diff --git a/docs/developers/stored-requests.md b/docs/developers/stored-requests.md index 8eac8fe9bc8..285e16b5771 100644 --- a/docs/developers/stored-requests.md +++ b/docs/developers/stored-requests.md @@ -36,8 +36,12 @@ Add the file `stored_imps/{id}.json` and populate it with some [Imp](https://www ] }, "ext": { - "appnexus": { - "placement_id": 10433394 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 10433394 + } + } } } } @@ -83,8 +87,12 @@ You can also store _part_ of the Imp on the server. For example: ] }, "ext": { - "appnexus": { - "placement_id": 10433394 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 10433394 + } + } } } } diff --git a/docs/endpoints/info/bidders.md b/docs/endpoints/info/bidders.md index a3868782993..103b9612389 100644 --- a/docs/endpoints/info/bidders.md +++ b/docs/endpoints/info/bidders.md @@ -3,7 +3,7 @@ ## `GET /info/bidders` This endpoint returns a list of active Bidders supported by Prebid Server. -These are the core values allowed to be used as `request.imp[i].ext.{bidder}` +These are the core values allowed to be used as `request.imp[i].ext.prebid.bidder.{bidder}` keys in [Auction](../openrtb2/auction.md) requests. For detailed info about a specific Bidder, use [`/info/bidders/{bidderName}`](bidders/bidderName.md) diff --git a/docs/endpoints/info/bidders/bidderName.md b/docs/endpoints/info/bidders/bidderName.md index bf4b65dc4e7..418377a45cc 100644 --- a/docs/endpoints/info/bidders/bidderName.md +++ b/docs/endpoints/info/bidders/bidderName.md @@ -40,7 +40,7 @@ The fields hold the following information: If `capabilities.app` or `capabilities.site` do not exist, then this Bidder does not support that platform. OpenRTB Requests which define a `request.app` or `request.site` property will fail if a -`request.imp[i].ext.{bidderName}` exists for a Bidder which doesn't support them. +`request.imp[i].ext.prebid.bidder.{bidderName}` exists for a Bidder which doesn't support them. ## `GET /info/bidders/all` diff --git a/docs/endpoints/openrtb2/amp.md b/docs/endpoints/openrtb2/amp.md index 178b68d48e7..7e7037c4790 100644 --- a/docs/endpoints/openrtb2/amp.md +++ b/docs/endpoints/openrtb2/amp.md @@ -54,12 +54,16 @@ An example Stored Request is given below: "id": "some-impression-id", "banner": {}, // The sizes are defined is set by your AMP tag query params "ext": { - "appnexus": { - // Insert parameters here - }, - "rubicon": { - // Insert parameters here - } + "prebid": { + "bidder": { + "appnexus": { + // Insert parameters here + }, + "rubicon": { + // Insert parameters here + } + } + } } } ] diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index a48a9a99bd0..f0792260993 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -39,8 +39,12 @@ The following is a "hello world" request which fetches the [Prebid sample ad](ht ] }, "ext": { - "appnexus": { - "placement_id": 10433394 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 10433394 + } + } } } } @@ -107,11 +111,11 @@ These fall under the `ext` property of JSON objects. If `ext` is defined on an object, Prebid Server uses the following conventions: -1. `ext` in "Request objects" uses `ext.prebid` and/or `ext.{anyBidderCode}`. +1. `ext` in "Request objects" uses `ext.prebid` and/or `ext.prebid.bidder.{anyBidderCode}`. 2. `ext` on "Response objects" uses `ext.prebid` and/or `ext.bidder`. The only exception here is the top-level `BidResponse`, because it's bidder-independent. -`ext.{anyBidderCode}` and `ext.bidder` extensions are defined by bidders. +`ext.prebid.bidder.{anyBidderCode}` and `ext.bidder` extensions are defined by bidders. `ext.prebid` extensions are defined by Prebid Server. Exceptions are made for extensions with "standard" recommendations: @@ -289,11 +293,15 @@ This can be used to request bids from the same Bidder with different params. For "mimes": ["video/mp4"] }, "ext": { - "appnexus: { - "placement_id": 123 - }, - "districtm": { - "placement_id": 456 + "prebid": { + "bidder": { + "appnexus: { + "placement_id": 123 + }, + "districtm": { + "placement_id": 456 + } + } } } } @@ -324,7 +332,7 @@ For example, if the Request defines an alias like this: } ``` -then any `imp.ext.appnexus` params will actually go to the **rubicon** adapter. +then any `imp.ext.prebid.bidder.appnexus` params will actually go to the **rubicon** adapter. It will become impossible to fetch bids from Appnexus within that Request. #### Bidder Response Times @@ -757,7 +765,7 @@ This section describes the ways in which Prebid Server **breaks** the OpenRTB sp Prebid Server returns a 400 on requests which define `wseat` or `bseat`. We may add support for these in the future, if there's compelling need. -Instead, an impression is only offered to a bidder if `bidrequest.imp[i].ext.{bidderName}` exists. +Instead, an impression is only offered to a bidder if `bidrequest.imp[i].ext.prebid.bidder.{bidderName}` exists. This supports publishers who want to sell different impressions to different bidders. diff --git a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java index de779a419fe..c191be47ba8 100644 --- a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -52,11 +53,13 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountStatus; import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.StreamUtil; import org.prebid.server.validation.RequestValidator; import org.prebid.server.validation.model.ValidationResult; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Currency; import java.util.HashMap; @@ -68,8 +71,6 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; /** * Used in OpenRTB request processing. @@ -83,6 +84,12 @@ public class AuctionRequestFactory { private static final String WEB_CHANNEL = "web"; private static final String APP_CHANNEL = "app"; + private static final String PREBID_EXT = "prebid"; + private static final String BIDDER_EXT = "bidder"; + + private static final Set IMP_EXT_NON_BIDDER_FIELDS = Collections.unmodifiableSet(new HashSet<>( + Arrays.asList(PREBID_EXT, "context"))); + private final long maxRequestSize; private final boolean enforceValidAccount; private final boolean shouldCacheOnlyWinningBids; @@ -454,18 +461,93 @@ private Source populateSource(Source source) { } /** - * Updates imps with security 1, when secured request was received and imp security was not defined. + * - Updates imps with security 1, when secured request was received and imp security was not defined. + * - Moves bidder parameters from imp.ext._bidder_ to imp.ext.prebid.bidder._bidder_ */ private List populateImps(List imps, HttpServerRequest request) { - List result = null; + if (CollectionUtils.isEmpty(imps)) { + return null; + } + + final Integer secureFromRequest = paramsExtractor.secureFrom(request); - if (Objects.equals(paramsExtractor.secureFrom(request), 1) - && imps.stream().map(Imp::getSecure).anyMatch(Objects::isNull)) { - result = imps.stream() - .map(imp -> imp.getSecure() == null ? imp.toBuilder().secure(1).build() : imp) - .collect(Collectors.toList()); + if (!shouldModifyImps(imps, secureFromRequest)) { + return imps; } - return result; + + return imps.stream() + .map(imp -> populateImp(imp, secureFromRequest)) + .collect(Collectors.toList()); + } + + private boolean shouldModifyImps(List imps, Integer secureFromRequest) { + return imps.stream() + .anyMatch(imp -> shouldSetImpSecure(imp, secureFromRequest) || shouldMoveBidderParams(imp)); + } + + private boolean shouldSetImpSecure(Imp imp, Integer secureFromRequest) { + return imp.getSecure() == null && Objects.equals(secureFromRequest, 1); + } + + private boolean shouldMoveBidderParams(Imp imp) { + return imp.getExt() != null + && StreamUtil.asStream(imp.getExt().fieldNames()).anyMatch(this::isImpExtBidderField); + } + + private boolean isImpExtBidderField(String field) { + return !IMP_EXT_NON_BIDDER_FIELDS.contains(field); + } + + private Imp populateImp(Imp imp, Integer secureFromRequest) { + final boolean shouldSetSecure = shouldSetImpSecure(imp, secureFromRequest); + final boolean shouldMoveBidderParams = shouldMoveBidderParams(imp); + + if (shouldSetSecure || shouldMoveBidderParams) { + final ObjectNode impExt = imp.getExt(); + + return imp.toBuilder() + .secure(shouldSetSecure ? Integer.valueOf(1) : imp.getSecure()) + .ext(shouldMoveBidderParams ? populateImpExt(impExt) : impExt) + .build(); + } + + return imp; + } + + private ObjectNode populateImpExt(ObjectNode impExt) { + final ObjectNode modifiedExt = impExt.deepCopy(); + + final ObjectNode modifiedExtPrebid = getOrCreateChildObjectNode(modifiedExt, PREBID_EXT); + modifiedExt.replace(PREBID_EXT, modifiedExtPrebid); + final ObjectNode modifiedExtPrebidBidder = getOrCreateChildObjectNode(modifiedExtPrebid, BIDDER_EXT); + modifiedExtPrebid.replace(BIDDER_EXT, modifiedExtPrebidBidder); + + final Set bidderFields = StreamUtil.asStream(modifiedExt.fieldNames()) + .filter(this::isImpExtBidderField) + .collect(Collectors.toSet()); + + for (final String currentBidderField : bidderFields) { + final ObjectNode modifiedExtPrebidBidderCurrentBidder = + getOrCreateChildObjectNode(modifiedExtPrebidBidder, currentBidderField); + modifiedExtPrebidBidder.replace(currentBidderField, modifiedExtPrebidBidderCurrentBidder); + + final JsonNode extCurrentBidder = modifiedExt.remove(currentBidderField); + if (isObjectNode(extCurrentBidder)) { + modifiedExtPrebidBidderCurrentBidder.setAll((ObjectNode) extCurrentBidder); + } + } + + return modifiedExt; + } + + private static ObjectNode getOrCreateChildObjectNode(ObjectNode parentNode, String fieldName) { + final JsonNode childNode = parentNode.get(fieldName); + + return isObjectNode(childNode) ? (ObjectNode) childNode : parentNode.objectNode(); + } + + private static boolean isObjectNode(JsonNode node) { + return node != null && node.isObject(); } /** @@ -638,7 +720,7 @@ private Map aliasesOrNull(ExtRequestPrebid prebid, List imp final Map resolvedAliases = imps.stream() .filter(Objects::nonNull) .filter(imp -> imp.getExt() != null) // request validator is not called yet - .flatMap(imp -> asStream(imp.getExt().fieldNames()) + .flatMap(imp -> StreamUtil.asStream(biddersFromImp(imp)) .filter(bidder -> !aliases.containsKey(bidder)) .filter(bidderCatalog::isAlias)) .distinct() @@ -654,6 +736,13 @@ private Map aliasesOrNull(ExtRequestPrebid prebid, List imp return result; } + private Iterator biddersFromImp(Imp imp) { + final JsonNode extPrebid = imp.getExt().get(PREBID_EXT); + final JsonNode extPrebidBidder = isObjectNode(extPrebid) ? extPrebid.get(BIDDER_EXT) : null; + + return isObjectNode(extPrebidBidder) ? extPrebidBidder.fieldNames() : Collections.emptyIterator(); + } + /** * Returns populated {@link ExtRequestPrebidCache} or null if no changes were applied. */ @@ -717,11 +806,6 @@ private static Long resolveTmax(Long requestTimeout, TimeoutResolver timeoutReso return !Objects.equals(requestTimeout, timeout) ? timeout : null; } - private static Stream asStream(Iterator iterator) { - final Iterable iterable = () -> iterator; - return StreamSupport.stream(iterable.spliterator(), false); - } - private static R getIfNotNull(T target, Function getter) { return target != null ? getter.apply(target) : null; } diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index d2abfecdffd..11254451e54 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -79,6 +79,7 @@ public class ExchangeService { private static final Logger logger = LoggerFactory.getLogger(ExchangeService.class); private static final String PREBID_EXT = "prebid"; + private static final String BIDDER_EXT = "bidder"; private static final String CONTEXT_EXT = "context"; private static final String DATA = "data"; private static final String ALL_BIDDERS_CONFIG = "*"; @@ -269,9 +270,9 @@ private static List populateStoredResponse(StoredResponseResult storedRespo * i.e. the bidders will not see any other extension fields. If Imp extension name is alias, which is also defined * in bidRequest.ext.prebid.aliases and valid, separate {@link BidRequest} will be created for this alias and sent * to appropriate bidder. - * For example suppose {@link BidRequest} has two {@link Imp}s. First one with imp.ext[].rubicon and - * imp.ext[].rubiconAlias and second with imp.ext[].appnexus and imp.ext[].rubicon. Three {@link BidRequest}s will - * be created: + * For example suppose {@link BidRequest} has two {@link Imp}s. First one with imp.ext.prebid.bidder.rubicon and + * imp.ext.prebid.bidder.rubiconAlias and second with imp.ext.prebid.bidder.appnexus and + * imp.ext.prebid.bidder.rubicon. Three {@link BidRequest}s will be created: * 1. {@link BidRequest} with one {@link Imp}, where bidder extension points to rubiconAlias extension and will be * sent to Rubicon bidder. * 2. {@link BidRequest} with two {@link Imp}s, where bidder extension points to appropriate rubicon extension from @@ -291,15 +292,14 @@ private static List populateStoredResponse(StoredResponseResult storedRespo private Future> extractBidderRequests(AuctionContext context, List requestedImps, BidderAliases aliases) { - // sanity check: discard imps without extension + final List imps = requestedImps.stream() - .filter(imp -> imp.getExt() != null) + .filter(imp -> bidderParamsFromImpExt(imp.getExt()) != null) .collect(Collectors.toList()); // identify valid bidders and aliases out of imps final List bidders = imps.stream() - .flatMap(imp -> StreamUtil.asStream(imp.getExt().fieldNames()) - .filter(bidder -> !Objects.equals(bidder, PREBID_EXT) && !Objects.equals(bidder, CONTEXT_EXT)) + .flatMap(imp -> StreamUtil.asStream(bidderParamsFromImpExt(imp.getExt()).fieldNames()) .filter(bidder -> isValidBidder(bidder, aliases))) .distinct() .collect(Collectors.toList()); @@ -307,6 +307,10 @@ private Future> extractBidderRequests(AuctionContext context return makeBidderRequests(bidders, context, aliases, imps); } + private static JsonNode bidderParamsFromImpExt(ObjectNode ext) { + return ext.get(PREBID_EXT).get(BIDDER_EXT); + } + /** * Checks if bidder name is valid in case when bidder can also be alias name. */ @@ -587,7 +591,7 @@ private BidderRequest createBidderRequest(BidderPrivacyResult bidderPrivacyResul */ private List prepareImps(String bidder, List imps, boolean useFirstPartyData) { return imps.stream() - .filter(imp -> imp.getExt().hasNonNull(bidder)) + .filter(imp -> bidderParamsFromImpExt(imp.getExt()).hasNonNull(bidder)) .map(imp -> imp.toBuilder() .ext(prepareImpExt(bidder, imp.getExt(), useFirstPartyData)) .build()) @@ -599,33 +603,35 @@ private List prepareImps(String bidder, List imps, boolean useFirstPar *

    *
  • "prebid" field populated with an imp.ext.prebid field value, may be null
  • *
  • "context" field populated with an imp.ext.context field value, may be null
  • - *
  • "bidder" field populated with an imp.ext.{bidder} field value, not null
  • + *
  • "bidder" field populated with an imp.ext.prebid.bidder.{bidder} field value, not null
  • *
*/ private ObjectNode prepareImpExt(String bidder, ObjectNode impExt, boolean useFirstPartyData) { - final JsonNode impExtPrebid = prepareImpExtPrebid(bidder, impExt.get(PREBID_EXT)); - final ObjectNode result = mapper.mapper().valueToTree(ExtPrebid.of(impExtPrebid, impExt.get(bidder))); + final JsonNode impExtPrebid = cleanBidderParamsFromImpExtPrebid(impExt.get(PREBID_EXT)); + final JsonNode impExtBidder = bidderParamsFromImpExt(impExt).get(bidder); - final JsonNode contextNode = impExt.get(CONTEXT_EXT); - final boolean isContextNodePresent = contextNode != null && !contextNode.isNull(); - if (isContextNodePresent) { - final JsonNode contextNodeCopy = contextNode.deepCopy(); + final ObjectNode result = mapper.mapper().valueToTree(ExtPrebid.of(impExtPrebid, impExtBidder)); + + if (impExt.hasNonNull(CONTEXT_EXT)) { + final JsonNode contextNodeCopy = impExt.get(CONTEXT_EXT).deepCopy(); if (!useFirstPartyData && contextNodeCopy.isObject()) { ((ObjectNode) contextNodeCopy).remove(DATA); } result.set(CONTEXT_EXT, contextNodeCopy); } + return result; } - private JsonNode prepareImpExtPrebid(String bidder, JsonNode extImpPrebidNode) { - if (extImpPrebidNode != null && extImpPrebidNode.hasNonNull(bidder)) { - final ExtImpPrebid extImpPrebid = extImpPrebid(extImpPrebidNode).toBuilder() - .bidder((ObjectNode) extImpPrebidNode.get(bidder)) // leave appropriate bidder related data - .build(); - return mapper.mapper().valueToTree(extImpPrebid); + private JsonNode cleanBidderParamsFromImpExtPrebid(JsonNode extImpPrebidNode) { + if (extImpPrebidNode.size() > 1) { + return mapper.mapper().valueToTree( + extImpPrebid(extImpPrebidNode).toBuilder() + .bidder(null) + .build()); } - return extImpPrebidNode; + + return null; } /** diff --git a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java index 63a827f100f..c3ec2c54a00 100644 --- a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java +++ b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; @@ -26,21 +27,19 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.StoredResponseDataResult; +import org.prebid.server.util.StreamUtil; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; /** * Resolves stored response data retrieving and BidderResponse merging processes. @@ -48,8 +47,10 @@ public class StoredResponseProcessor { private static final String PREBID_EXT = "prebid"; - private static final String CONTEXT_EXT = "context"; + private static final String BIDDER_EXT = "bidder"; + private static final String DEFAULT_BID_CURRENCY = "USD"; + private static final TypeReference> SEATBID_LIST_TYPEREFERENCE = new TypeReference>() { }; @@ -60,6 +61,7 @@ public class StoredResponseProcessor { public StoredResponseProcessor(ApplicationSettings applicationSettings, BidderCatalog bidderCatalog, JacksonMapper mapper) { + this.applicationSettings = Objects.requireNonNull(applicationSettings); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.mapper = Objects.requireNonNull(mapper); @@ -72,7 +74,7 @@ Future getStoredResponseResult( final Map storedResponseIdToImpId = new HashMap<>(); try { - fillStoredResponseIdsAndRequestingImps(imps, requiredRequestImps, storedResponseIdToImpId, aliases); + fillStoredResponseIdsAndRequestingImps(imps, aliases, requiredRequestImps, storedResponseIdToImpId); } catch (InvalidRequestException e) { return Future.failedFuture(e); } @@ -90,6 +92,7 @@ Future getStoredResponseResult( List mergeWithBidderResponses(List bidderResponses, List storedResponses, List imps) { + if (CollectionUtils.isEmpty(storedResponses)) { return bidderResponses; } @@ -109,28 +112,34 @@ List mergeWithBidderResponses(List bidderRespons .collect(Collectors.toList()); } - private void fillStoredResponseIdsAndRequestingImps(List imps, List requiredRequestImps, - Map storedResponseIdToImpId, - BidderAliases aliases) { + private void fillStoredResponseIdsAndRequestingImps(List imps, + BidderAliases aliases, + List requiredRequestImps, + Map storedResponseIdToImpId) { + for (final Imp imp : imps) { final String impId = imp.getId(); final ObjectNode extImpNode = imp.getExt(); final ExtImp extImp = getExtImp(extImpNode, impId); final ExtImpPrebid extImpPrebid = extImp != null ? extImp.getPrebid() : null; - if (extImpPrebid == null) { + + final ExtStoredAuctionResponse storedAuctionResponse = + extImpPrebid != null ? extImpPrebid.getStoredAuctionResponse() : null; + final List storedBidResponse = + extImpPrebid != null ? extImpPrebid.getStoredBidResponse() : null; + + if (storedAuctionResponse == null && storedBidResponse == null) { requiredRequestImps.add(imp); continue; } - final ExtStoredAuctionResponse storedAuctionResponse = extImpPrebid.getStoredAuctionResponse(); final String storedAuctionResponseId = storedAuctionResponse != null ? storedAuctionResponse.getId() : null; if (StringUtils.isNotEmpty(storedAuctionResponseId)) { storedResponseIdToImpId.put(storedAuctionResponseId, impId); continue; } - resolveStoredBidResponse(requiredRequestImps, storedResponseIdToImpId, aliases, imp, impId, extImpNode, - extImpPrebid); + resolveStoredBidResponse(storedBidResponse, imp, aliases, requiredRequestImps, storedResponseIdToImpId); } } @@ -138,22 +147,25 @@ private ExtImp getExtImp(ObjectNode extImpNode, String impId) { try { return mapper.mapper().treeToValue(extImpNode, ExtImp.class); } catch (JsonProcessingException e) { - throw new InvalidRequestException(String.format("Error decoding bidRequest.imp.ext for impId = %s : %s", - impId, e.getMessage())); + throw new InvalidRequestException(String.format( + "Error decoding bidRequest.imp.ext for impId = %s : %s", impId, e.getMessage())); } } - private void resolveStoredBidResponse(List requiredRequestImps, Map storedResponseIdToImpId, - BidderAliases aliases, Imp imp, String impId, - ObjectNode extImpNode, ExtImpPrebid extImpPrebid) { + private void resolveStoredBidResponse(List storedBidResponse, + Imp imp, + BidderAliases aliases, + List requiredRequestImps, + Map storedResponseIdToImpId) { + + final String impId = imp.getId(); - final List storedBidResponse = extImpPrebid.getStoredBidResponse(); final Map bidderToStoredId = storedBidResponse != null ? getBidderToStoredResponseId(storedBidResponse, impId) : Collections.emptyMap(); if (!bidderToStoredId.isEmpty()) { - final Imp resolvedBiddersImp = removeStoredResponseBidders(imp, extImpNode, bidderToStoredId.keySet()); - if (hasValidBidder(aliases, resolvedBiddersImp)) { + final Imp resolvedBiddersImp = removeStoredResponseBidders(imp, bidderToStoredId.keySet()); + if (hasValidBidder(resolvedBiddersImp, aliases)) { requiredRequestImps.add(resolvedBiddersImp); } storedResponseIdToImpId.putAll(bidderToStoredId.values().stream() @@ -165,6 +177,7 @@ private void resolveStoredBidResponse(List requiredRequestImps, Map getBidderToStoredResponseId(List extStoredBidResponses, String impId) { + final Map bidderToStoredResponseId = new HashMap<>(); for (final ExtStoredBidResponse extStoredBidResponse : extStoredBidResponses) { final String bidder = extStoredBidResponse.getBidder(); @@ -184,26 +197,29 @@ private Map getBidderToStoredResponseId(List bidders) { - boolean isUpdated = false; - for (final String bidder : bidders) { - if (extImp.hasNonNull(bidder)) { - extImp.remove(bidder); - isUpdated = true; - } + private Imp removeStoredResponseBidders(Imp imp, Set bidders) { + final ObjectNode ext = imp.getExt(); + final JsonNode bidderParams = bidderParamsFromImpExt(ext); + + if (bidderParams == null || StreamUtil.asStream(bidderParams.fieldNames()).noneMatch(bidders::contains)) { + return imp; } - return isUpdated ? imp.toBuilder().ext(extImp).build() : imp; + + final ObjectNode modifiedExt = ext.deepCopy(); + ((ObjectNode) bidderParamsFromImpExt(modifiedExt)).remove(bidders); + + return imp.toBuilder().ext(modifiedExt).build(); } - private boolean hasValidBidder(BidderAliases aliases, Imp resolvedBiddersImp) { - return asStream(resolvedBiddersImp.getExt().fieldNames()) - .anyMatch(bidder -> !Objects.equals(bidder, PREBID_EXT) && !Objects.equals(bidder, CONTEXT_EXT) - && isValidBidder(bidder, aliases)); + private static JsonNode bidderParamsFromImpExt(ObjectNode ext) { + return ext.get(PREBID_EXT).get(BIDDER_EXT); } - private Stream asStream(Iterator iterator) { - final Iterable iterable = () -> iterator; - return StreamSupport.stream(iterable.spliterator(), false); + private boolean hasValidBidder(Imp imp, BidderAliases aliases) { + final JsonNode bidderParams = bidderParamsFromImpExt(imp.getExt()); + + return bidderParams != null + && StreamUtil.asStream(bidderParams.fieldNames()).anyMatch(bidder -> isValidBidder(bidder, aliases)); } private boolean isValidBidder(String bidder, BidderAliases aliases) { diff --git a/src/main/java/org/prebid/server/bidder/adoppler/AdopplerBidder.java b/src/main/java/org/prebid/server/bidder/adoppler/AdopplerBidder.java index 1b475e56dd2..b8842478526 100644 --- a/src/main/java/org/prebid/server/bidder/adoppler/AdopplerBidder.java +++ b/src/main/java/org/prebid/server/bidder/adoppler/AdopplerBidder.java @@ -82,7 +82,7 @@ private ExtImpAdoppler parseAndValidateImpExt(Imp imp) { throw new PreBidException(e.getMessage()); } if (StringUtils.isBlank(extImpAdoppler.getAdunit())) { - throw new PreBidException("$.imp.ext.adoppler.adunit required"); + throw new PreBidException("adunit parameter is required for adoppler bidder"); } return extImpAdoppler; } 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 ac90f63aa8f..fb8040719d1 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -1,6 +1,5 @@ package org.prebid.server.bidder.rubicon; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -47,8 +46,6 @@ import org.prebid.server.bidder.rubicon.proto.RubiconExtPrebidBiddersBidder; import org.prebid.server.bidder.rubicon.proto.RubiconExtPrebidBiddersBidderDebug; import org.prebid.server.bidder.rubicon.proto.RubiconImpExt; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtPrebidBidder; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtPrebidRubiconDebug; import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRp; import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRpTrack; import org.prebid.server.bidder.rubicon.proto.RubiconPubExt; @@ -81,6 +78,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid; import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUidExt; import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtImpRubicon; +import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtImpRubiconDebug; import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtUserTpIdRubicon; import org.prebid.server.proto.openrtb.ext.request.rubicon.RubiconVideoParams; import org.prebid.server.proto.openrtb.ext.response.BidType; @@ -117,7 +115,6 @@ public class RubiconBidder implements Bidder { private static final String TK_XINT_QUERY_PARAMETER = "tk_xint"; private static final String PREBID_SERVER_USER_AGENT = "prebid-server/1.0"; - private static final String PREBID_EXT = "prebid"; private static final String ADSERVER_EID = "adserver.org"; private static final String LIVEINTENT_EID = "liveintent.com"; @@ -1130,29 +1127,9 @@ private Float cmpOverrideFromRequest(BidRequest bidRequest) { } private Float cpmOverrideFromImp(Imp imp) { - final JsonNode extImpPrebidNode = imp.getExt().get(PREBID_EXT); - final ExtImpPrebid prebid = extImpPrebidNode != null ? extImpPrebid(extImpPrebidNode) : null; - final RubiconImpExtPrebidBidder bidder = prebid != null - ? extImpPrebidBidder(prebid.getBidder()) - : null; - final RubiconImpExtPrebidRubiconDebug debug = bidder != null ? bidder.getDebug() : null; - return debug != null ? debug.getCpmoverride() : null; - } - - private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) { - try { - return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); - } catch (JsonProcessingException e) { - throw new PreBidException(String.format("Error decoding imp.ext.prebid: %s", e.getMessage()), e); - } - } + final ExtImpRubiconDebug debug = parseRubiconExt(imp).getBidder().getDebug(); - private RubiconImpExtPrebidBidder extImpPrebidBidder(ObjectNode extImpPrebidBidder) { - try { - return mapper.mapper().treeToValue(extImpPrebidBidder, RubiconImpExtPrebidBidder.class); - } catch (JsonProcessingException e) { - throw new PreBidException(String.format("Error decoding imp.ext.prebid.bidder: %s", e.getMessage()), e); - } + return debug != null ? debug.getCpmoverride() : null; } private static BidType bidType(BidRequest bidRequest) { diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidBidder.java deleted file mode 100644 index 9a8aa550c61..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidBidder.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.Value; - -@Value(staticConstructor = "of") -public class RubiconImpExtPrebidBidder { - - RubiconImpExtPrebidRubiconDebug debug; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidRubiconDebug.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidRubiconDebug.java deleted file mode 100644 index 7f3c601f1e0..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidRubiconDebug.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.Value; - -@Value(staticConstructor = "of") -public class RubiconImpExtPrebidRubiconDebug { - - Float cpmoverride; -} diff --git a/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtil.java b/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtil.java index 9d814a3670d..5b41dbb9e7a 100644 --- a/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtil.java +++ b/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtil.java @@ -25,7 +25,7 @@ class SharethroughRequestUtil { private static final int MIN_SAFARI_VERSION = 10; /** - * Retrieves size from imp.ext.sharethrough.iframeSize or from im.banner.format. + * Retrieves size from iframeSize or from imp.banner.format. */ Size getSize(Imp imp, ExtImpSharethrough extImpSharethrough) { final List iframeSize = extImpSharethrough.getIframeSize(); diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java index fc847965f79..c140e163661 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java @@ -34,4 +34,6 @@ public class ExtImpRubicon { String pchain; List keywords; + + ExtImpRubiconDebug debug; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubiconDebug.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubiconDebug.java new file mode 100644 index 00000000000..ce00852bb5f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubiconDebug.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.openrtb.ext.request.rubicon; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpRubiconDebug { + + Float cpmoverride; +} diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java index f6bfb83cd11..795577e2265 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -77,8 +77,10 @@ public class RequestValidator { private static final String PREBID_EXT = "prebid"; - private static final String CONTEXT_EXT = "context"; + private static final String BIDDER_EXT = "bidder"; + private static final Locale LOCALE = Locale.US; + private static final String DOCUMENTATION = "https://iabtechlab.com/wp-content/uploads/2016/07/" + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf"; @@ -518,12 +520,12 @@ private void fillAndValidateNative(Native xNative, int impIndex) throws Validati private Request parseNativeRequest(String rawStringNativeRequest, int impIndex) throws ValidationException { if (StringUtils.isBlank(rawStringNativeRequest)) { - throw new ValidationException("request.imp.[%d].ext.native contains empty request value", impIndex); + throw new ValidationException("request.imp[%d].native contains empty request value", impIndex); } try { return mapper.mapper().readValue(rawStringNativeRequest, Request.class); } catch (IOException e) { - throw new ValidationException("Error while parsing request.imp.[%d].ext.native.request", impIndex); + throw new ValidationException("Error while parsing request.imp[%d].native.request", impIndex); } } @@ -762,17 +764,38 @@ private String toEncodedRequest(Request nativeRequest, List updatedAssets } private void validateImpExt(ObjectNode ext, Map aliases, int impIndex) throws ValidationException { - if (ext == null || ext.size() < 1) { - throw new ValidationException("request.imp[%d].ext must contain at least one bidder", impIndex); + validateImpExtPrebid(childAsObjectNode(ext, PREBID_EXT), aliases, impIndex); + } + + private void validateImpExtPrebid(ObjectNode extPrebid, Map aliases, int impIndex) + throws ValidationException { + + if (extPrebid == null || extPrebid.size() < 1) { + throw new ValidationException( + "request.imp[%d].ext.prebid must be non-empty object", impIndex); } - final Iterator> bidderExtensions = ext.fields(); + validateImpExtPrebidBidder(extPrebid, aliases, impIndex); + } + + private void validateImpExtPrebidBidder(ObjectNode extPrebid, Map aliases, int impIndex) + throws ValidationException { + + final JsonNode extPrebidBidder = extPrebid.get(BIDDER_EXT); + + if (extPrebidBidder == null) { + return; + } + + if (!extPrebidBidder.isObject()) { + throw new ValidationException("request.imp[%d].ext.prebid.bidder must be object", impIndex); + } + + final Iterator> bidderExtensions = extPrebidBidder.fields(); while (bidderExtensions.hasNext()) { final Map.Entry bidderExtension = bidderExtensions.next(); final String bidder = bidderExtension.getKey(); - if (!Objects.equals(bidder, PREBID_EXT) && !Objects.equals(bidder, CONTEXT_EXT)) { - validateImpBidderExtName(impIndex, bidderExtension, aliases.getOrDefault(bidder, bidder)); - } + validateImpBidderExtName(impIndex, bidderExtension, aliases.getOrDefault(bidder, bidder)); } } @@ -781,12 +804,12 @@ private void validateImpBidderExtName(int impIndex, Map.Entry if (bidderCatalog.isValidName(bidderName)) { final Set messages = bidderParamValidator.validate(bidderName, bidderExtension.getValue()); if (!messages.isEmpty()) { - throw new ValidationException("request.imp[%d].ext.%s failed validation.\n%s", impIndex, + throw new ValidationException("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%s", impIndex, bidderName, String.join("\n", messages)); } } else if (!bidderCatalog.isDeprecatedName(bidderName) && !bidderCatalog.isAlias(bidderName)) { throw new ValidationException( - "request.imp[%d].ext contains unknown bidder: %s", impIndex, bidderName); + "request.imp[%d].ext.prebid.bidder contains unknown bidder: %s", impIndex, bidderName); } } @@ -893,6 +916,16 @@ private void validateMetrics(List metrics, int impIndex) throws Validati } } + private ObjectNode childAsObjectNode(ObjectNode parent, String fieldName) { + final JsonNode child = parent != null ? parent.get(fieldName) : null; + + return isObjectNode(child) ? (ObjectNode) child : null; + } + + private static boolean isObjectNode(JsonNode node) { + return node != null && node.isObject(); + } + private static boolean hasPositiveValue(Integer value) { return value != null && value > 0; } diff --git a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java index edb01f4bf29..719ecf2ca7c 100644 --- a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; @@ -556,20 +557,24 @@ public void shouldUpdateImpsWithSecurityOneIfRequestIsSecuredAndImpSecurityNotDe @Test public void shouldNotUpdateImpsWithSecurityOneIfRequestIsSecureAndImpSecurityIsZero() { // given - givenBidRequest(BidRequest.builder().imp(singletonList(Imp.builder().secure(0).build())).build()); + final List imps = singletonList(Imp.builder().secure(0).build()); + + givenBidRequest(BidRequest.builder().imp(imps).build()); + given(paramsExtractor.secureFrom(any())).willReturn(1); // when final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest(); // then - assertThat(request.getImp()).extracting(Imp::getSecure).containsOnly(0); + assertThat(request.getImp()).isSameAs(imps); } @Test public void shouldUpdateImpsOnlyWithNotDefinedSecurityWithSecurityOneIfRequestIsSecure() { // given - givenBidRequest(BidRequest.builder().imp(asList(Imp.builder().build(), Imp.builder().secure(0).build())) + givenBidRequest(BidRequest.builder() + .imp(asList(Imp.builder().build(), Imp.builder().secure(0).build())) .build()); given(paramsExtractor.secureFrom(any())).willReturn(1); @@ -583,14 +588,105 @@ public void shouldUpdateImpsOnlyWithNotDefinedSecurityWithSecurityOneIfRequestIs @Test public void shouldNotUpdateImpsWithSecurityOneIfRequestIsNotSecureAndImpSecurityIsNotDefined() { // given - givenBidRequest(BidRequest.builder().imp(singletonList(Imp.builder().build())).build()); + final List imps = singletonList(Imp.builder().build()); + + givenBidRequest(BidRequest.builder().imp(imps).build()); + given(paramsExtractor.secureFrom(any())).willReturn(0); // when final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest(); // then - assertThat(request.getImp()).extracting(Imp::getSecure).containsNull(); + assertThat(request.getImp()).isSameAs(imps); + } + + @Test + public void shouldMoveBidderParametersToImpExtPrebidBidderAndMergeWithExisting() { + // given + final List imps = singletonList( + Imp.builder() + .ext(mapper.createObjectNode() + .set("bidder1", mapper.createObjectNode().put("param1", "value1")) + .set("bidder2", mapper.createObjectNode().put("param2", "value2")) + .set("context", mapper.createObjectNode().put("data", "datavalue")) + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .set("bidder2", mapper.createObjectNode().put("param22", "value22"))) + .set("storedrequest", mapper.createObjectNode().put("id", "storedreq1")))) + .build()); + + givenBidRequest(BidRequest.builder().imp(imps).build()); + + // when + final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest(); + + // then + assertThat(request.getImp()).containsOnly( + Imp.builder() + .ext(mapper.createObjectNode() + .set("context", mapper.createObjectNode().put("data", "datavalue")) + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .set( + "bidder1", mapper.createObjectNode().put("param1", "value1")) + .set( + "bidder2", mapper.createObjectNode() + .put("param2", "value2") + .put("param22", "value22"))) + .set("storedrequest", mapper.createObjectNode().put("id", "storedreq1")))) + .build()); + } + + @Test + public void shouldMoveBidderParametersToImpExtPrebidBidderWhenImpExtPrebidAbsent() { + // given + final List imps = singletonList( + Imp.builder() + .ext(mapper.createObjectNode() + .set("bidder1", mapper.createObjectNode().put("param1", "value1")) + .set("bidder2", mapper.createObjectNode().put("param2", "value2"))) + .build()); + + givenBidRequest(BidRequest.builder().imp(imps).build()); + + // when + final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest(); + + // then + assertThat(request.getImp()).containsOnly( + Imp.builder() + .ext(mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .set( + "bidder1", mapper.createObjectNode().put("param1", "value1")) + .set( + "bidder2", mapper.createObjectNode().put("param2", "value2"))))) + .build()); + } + + @Test + public void shouldNotChangeImpExtWhenBidderParametersAreAtImpExtPrebidBidderOnly() { + // given + final List imps = singletonList( + Imp.builder() + .ext(mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .set( + "bidder1", mapper.createObjectNode().put("param1", "value1")) + .set( + "bidder2", mapper.createObjectNode().put("param2", "value2"))))) + .build()); + + givenBidRequest(BidRequest.builder().imp(imps).build()); + + // when + final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest(); + + // then + assertThat(request.getImp()).isSameAs(imps); } @Test diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 2e6f753899f..a0a9c836758 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -295,7 +295,7 @@ public void shouldExtractRequestWithBidderSpecificExtension() { givenBidder(givenEmptySeatBid()); final BidRequest bidRequest = givenBidRequest(singletonList( - givenImp(doubleMap("prebid", 0, "someBidder", 1), builder -> builder + givenImp(singletonMap("someBidder", 1), builder -> builder .id("impId") .banner(Banner.builder() .format(singletonList(Format.builder().w(400).h(300).build())) @@ -315,7 +315,7 @@ public void shouldExtractRequestWithBidderSpecificExtension() { .banner(Banner.builder() .format(singletonList(Format.builder().w(400).h(300).build())) .build()) - .ext(mapper.valueToTree(ExtPrebid.of(0, 1))) + .ext(mapper.valueToTree(ExtPrebid.of(null, 1))) .build())) .tmax(500L) .build()); @@ -331,7 +331,7 @@ public void shouldExtractRequestWithCurrencyRatesExtension() { "UAH", singletonMap("EUR", BigDecimal.valueOf(1.1565))); final BidRequest bidRequest = givenBidRequest(singletonList( - givenImp(doubleMap("prebid", 0, "someBidder", 1), builder -> builder + givenImp(singletonMap("someBidder", 1), builder -> builder .id("impId") .banner(Banner.builder() .format(singletonList(Format.builder().w(400).h(300).build())) @@ -357,7 +357,7 @@ public void shouldExtractRequestWithCurrencyRatesExtension() { .banner(Banner.builder() .format(singletonList(Format.builder().w(400).h(300).build())) .build()) - .ext(mapper.valueToTree(ExtPrebid.of(0, 1))) + .ext(mapper.valueToTree(ExtPrebid.of(null, 1))) .build())) .ext(ExtRequest.of( ExtRequestPrebid.builder().currency(ExtRequestCurrency.of(currencyRates, false)).build())) @@ -395,43 +395,6 @@ public void shouldExtractMultipleRequests() { .element(0).returns(2, imp -> imp.getExt().get("bidder").asInt()); } - @Test - public void shouldPassImpWithExtPrebidToDefinedBidder() { - // given - final String bidder1Name = "bidder1"; - final String bidder2Name = "bidder2"; - final Bidder bidder1 = mock(Bidder.class); - final Bidder bidder2 = mock(Bidder.class); - givenBidder(bidder1Name, bidder1, givenEmptySeatBid()); - givenBidder(bidder2Name, bidder2, givenEmptySeatBid()); - - final ObjectNode impExt = mapper.createObjectNode() - .put(bidder1Name, "ignored1") - .put(bidder2Name, "ignored2") - .putPOJO("prebid", doubleMap(bidder1Name, mapper.createObjectNode().put("somefield", "bidder1"), - bidder2Name, mapper.createObjectNode().put("somefield", "bidder2"))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(impExt, identity())), identity()); - - // when - exchangeService.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor bidRequest1Captor = ArgumentCaptor.forClass(BidRequest.class); - verify(httpBidderRequester).requestBids(same(bidder1), bidRequest1Captor.capture(), any(), anyBoolean()); - assertThat(bidRequest1Captor.getValue().getImp()).hasSize(1) - .extracting(imp -> imp.getExt().get("prebid")) - .containsOnly(mapper.createObjectNode().set("bidder", - mapper.createObjectNode().put("somefield", "bidder1"))); - - final ArgumentCaptor bidRequest2Captor = ArgumentCaptor.forClass(BidRequest.class); - verify(httpBidderRequester).requestBids(same(bidder2), bidRequest2Captor.capture(), any(), anyBoolean()); - assertThat(bidRequest2Captor.getValue().getImp()).hasSize(1) - .extracting(imp -> imp.getExt().get("prebid")) - .containsOnly(mapper.createObjectNode().set("bidder", - mapper.createObjectNode().put("somefield", "bidder2"))); - } - @Test public void shouldPassRequestWithExtPrebidToDefinedBidder() { // given @@ -668,10 +631,14 @@ public void shouldTolerateBidderResultWithoutBids() { } @Test - public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderWereUsedWithingSingleImp() { + public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderWereUsedWithinSingleImp() { // given given(httpBidderRequester.requestBids(any(), - eq(givenBidRequest(givenSingleImp(mapper.valueToTree(ExtPrebid.of(null, 1))), + eq(givenBidRequest( + singletonList(givenImp( + null, + builder -> builder.ext(mapper.valueToTree( + ExtPrebid.of(null, 1))))), builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .auctiontimestamp(1000L) .aliases(singletonMap("bidderAlias", "bidder")).build())))), any(), anyBoolean())) @@ -679,7 +646,11 @@ public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderW givenBid(Bid.builder().price(BigDecimal.ONE).build()))))); given(httpBidderRequester.requestBids(any(), - eq(givenBidRequest(givenSingleImp(mapper.valueToTree(ExtPrebid.of(null, 2))), + eq(givenBidRequest( + singletonList(givenImp( + null, + builder -> builder.ext(mapper.valueToTree( + ExtPrebid.of(null, 2))))), builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .auctiontimestamp(1000L) .aliases(singletonMap("bidderAlias", "bidder")).build())))), any(), anyBoolean())) @@ -1034,12 +1005,12 @@ public void shouldCreateRequestsFromImpsReturnedByStoredResponseProcessor() { givenBidder(givenEmptySeatBid()); final BidRequest bidRequest = givenBidRequest(asList( - givenImp(doubleMap("prebid", 0, "someBidder1", 1), builder -> builder + givenImp(singletonMap("someBidder1", 1), builder -> builder .id("impId1") .banner(Banner.builder() .format(singletonList(Format.builder().w(400).h(300).build())) .build())), - givenImp(doubleMap("prebid", 0, "someBidder2", 1), builder -> builder + givenImp(singletonMap("someBidder2", 1), builder -> builder .id("impId2") .banner(Banner.builder() .format(singletonList(Format.builder().w(400).h(300).build())) @@ -1048,7 +1019,7 @@ public void shouldCreateRequestsFromImpsReturnedByStoredResponseProcessor() { given(storedResponseProcessor.getStoredResponseResult(any(), any(), any())) .willReturn(Future.succeededFuture(StoredResponseResult - .of(singletonList(givenImp(doubleMap("prebid", 0, "someBidder1", 1), builder -> builder + .of(singletonList(givenImp(singletonMap("someBidder1", 1), builder -> builder .id("impId1") .banner(Banner.builder() .format(singletonList(Format.builder().w(400).h(300).build())) @@ -1067,7 +1038,7 @@ public void shouldCreateRequestsFromImpsReturnedByStoredResponseProcessor() { .banner(Banner.builder() .format(singletonList(Format.builder().w(400).h(300).build())) .build()) - .ext(mapper.valueToTree(ExtPrebid.of(0, 1))) + .ext(mapper.valueToTree(ExtPrebid.of(null, 1))) .build())) .tmax(500L) .build()); @@ -1226,13 +1197,20 @@ public void shouldNotChangeGdprFromRequestWhenDeviceLmtIsOne() { @Test public void shouldCleanImpExtContextDataWhenFirstPartyDataNotPermittedForBidder() { // given - final ObjectNode impExt = mapper.createObjectNode().put("someBidder", 1).set("context", - mapper.createObjectNode().put("data", "data").put("otherField", "value")); + final ObjectNode impExt = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .put("someBidder", 1))) + .set("context", mapper.createObjectNode() + .put("data", "data") + .put("otherField", "value")); final BidRequest bidRequest = givenBidRequest(singletonList(Imp.builder() .id("impId") .banner(Banner.builder() .format(singletonList(Format.builder().w(400).h(300).build())) - .build()).ext(impExt).build()), + .build()) + .ext(impExt) + .build()), builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .data(ExtRequestPrebidData.of(singletonList("otherBidder"))) .build()))); @@ -1251,13 +1229,21 @@ public void shouldCleanImpExtContextDataWhenFirstPartyDataNotPermittedForBidder( @Test public void shouldDeepCopyImpExtContextToEachImpressionAndNotRemoveDataForAllWhenDeprecatedOnlyOneBidder() { // given - final ObjectNode impExt = mapper.createObjectNode().put("someBidder", 1).put("deprecatedBidder", 2) - .set("context", mapper.createObjectNode().put("data", "data").put("otherField", "value")); + final ObjectNode impExt = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .put("someBidder", 1) + .put("deprecatedBidder", 2))) + .set("context", mapper.createObjectNode() + .put("data", "data") + .put("otherField", "value")); final BidRequest bidRequest = givenBidRequest(singletonList(Imp.builder() .id("impId") .banner(Banner.builder() .format(singletonList(Format.builder().w(400).h(300).build())) - .build()).ext(impExt).build()), + .build()) + .ext(impExt) + .build()), builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .data(ExtRequestPrebidData.of(singletonList("someBidder"))) .build()))); @@ -2345,7 +2331,8 @@ private static BidRequest givenBidRequest(List imp) { private static Imp givenImp(T ext, Function impBuilderCustomizer) { return impBuilderCustomizer.apply(Imp.builder() - .ext(ext != null ? mapper.valueToTree(ext) : null)) + .ext(mapper.valueToTree(singletonMap( + "prebid", ext != null ? singletonMap("bidder", ext) : emptyMap())))) .build(); } diff --git a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java index 6cfee994e73..c38d0888381 100644 --- a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java @@ -77,11 +77,11 @@ public void setUp() { @Test public void getStoredResponseResultShouldReturnSeatBidsForAuctionResponseId() throws JsonProcessingException { // given - final List imps = singletonList(Imp.builder().id("impId") - .ext(mapper.valueToTree( - ExtImp.of( - ExtImpPrebid.builder().storedAuctionResponse(ExtStoredAuctionResponse.of("1")).build(), - null))) + final List imps = singletonList(Imp.builder() + .id("impId") + .ext(mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder().storedAuctionResponse(ExtStoredAuctionResponse.of("1")).build(), + null))) .build()); given(applicationSettings.getStoredResponses(any(), any())) @@ -95,26 +95,32 @@ public void getStoredResponseResultShouldReturnSeatBidsForAuctionResponseId() th aliases, timeout); // then - assertThat(result.result()).isEqualTo(StoredResponseResult.of(emptyList(), - singletonList(SeatBid.builder().seat("rubicon") - .bid(singletonList(Bid.builder().id("id").impid("impId").build())).build()))); + assertThat(result.result()).isEqualTo(StoredResponseResult.of( + emptyList(), + singletonList(SeatBid.builder() + .seat("rubicon") + .bid(singletonList(Bid.builder().id("id").impid("impId").build())) + .build()))); } @Test public void getStoredResponseResultShouldNotChangeImpsAndReturnSeatBidsWhenThereAreNoStoredIds() { // given - final List imps = singletonList(Imp.builder() - .ext(mapper.createObjectNode().put("rubicon", 1)) - .build()); + final Imp imp = Imp.builder() + .ext(mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder().bidder(mapper.createObjectNode().put("rubicon", 1)).build(), + null))) + .build(); + given(bidderCatalog.isValidName(any())).willReturn(true); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(singletonList(imp), aliases, timeout); // then assertThat(result.result()).isEqualTo(StoredResponseResult.of( - singletonList(Imp.builder().ext(mapper.createObjectNode().put("rubicon", 1)).build()), + singletonList(imp), emptyList())); verifyZeroInteractions(applicationSettings); } @@ -122,22 +128,24 @@ public void getStoredResponseResultShouldNotChangeImpsAndReturnSeatBidsWhenThere @Test public void getStoredResponseResultShouldAddImpToRequiredRequestWhenItsStoredBidResponseIsEmpty() { // given - final List imps = singletonList(Imp.builder().id("impId1") - .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() - .storedBidResponse(emptyList()) - .build(), null))) + final List imps = singletonList(Imp.builder() + .id("impId1") + .ext(mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder().storedBidResponse(emptyList()).build(), + null))) .build()); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then assertThat(result.result()).isEqualTo(StoredResponseResult.of( - singletonList(Imp.builder().id("impId1") - .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() - .storedBidResponse(emptyList()) - .build(), null))) + singletonList(Imp.builder() + .id("impId1") + .ext(mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder().storedBidResponse(emptyList()).build(), + null))) .build()), emptyList())); verifyZeroInteractions(applicationSettings); @@ -156,8 +164,8 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenErrorHappenedDuri .willReturn(Future.failedFuture(new PreBidException("Failed."))); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then assertThat(result.failed()).isTrue(); @@ -169,9 +177,11 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenErrorHappenedDuri @Test public void getStoredResponseResultShouldReturnSeatBidsForBidStoredResponseId() throws JsonProcessingException { // given - final List imps = singletonList(Imp.builder().id("impId1") + final List imps = singletonList(Imp.builder() + .id("impId1") .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() - .storedBidResponse(asList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"), + .storedBidResponse(asList( + ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"), ExtStoredBidResponse.of("appnexus", "storedBidResponseId2"))) .build(), null))) @@ -179,39 +189,49 @@ public void getStoredResponseResultShouldReturnSeatBidsForBidStoredResponseId() final Map storedResponse = new HashMap<>(); storedResponse.put("storedBidResponseId1", mapper.writeValueAsString(singletonList( - SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id1").build())) - .build()))); + SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id1").build())).build()))); storedResponse.put("storedBidResponseId2", mapper.writeValueAsString(singletonList( - SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id2").build())) - .build()))); + SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id2").build())).build()))); given(applicationSettings.getStoredResponses(any(), any())).willReturn( Future.succeededFuture(StoredResponseDataResult.of(storedResponse, emptyList()))); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then - assertThat(result.result()).isEqualTo(StoredResponseResult.of(emptyList(), + assertThat(result.result()).isEqualTo(StoredResponseResult.of( + emptyList(), asList( - SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id2").impid("impId1") - .build())).build(), - SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id1").impid("impId1") - .build())).build()))); + SeatBid.builder() + .seat("appnexus") + .bid(singletonList(Bid.builder().id("id2").impid("impId1").build())) + .build(), + SeatBid.builder() + .seat("rubicon") + .bid(singletonList(Bid.builder().id("id1").impid("impId1").build())) + .build()))); } @Test public void getStoredResponseResultShouldReturnSeatBidsForBidAndAuctionStoredResponseId() throws JsonProcessingException { + // given final List imps = asList( - Imp.builder().id("impId1") - .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() - .storedAuctionResponse(ExtStoredAuctionResponse.of("storedAuctionRequest")) - .build(), null))).build(), - Imp.builder().id("impId2") - .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() + Imp.builder() + .id("impId1") + .ext(mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder() + .storedAuctionResponse(ExtStoredAuctionResponse.of("storedAuctionRequest")) + .build(), + null))) + .build(), + Imp.builder() + .id("impId2") + .ext(mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder() .storedBidResponse(singletonList( ExtStoredBidResponse.of("rubicon", "storedBidRequest"))) .build(), @@ -220,114 +240,143 @@ public void getStoredResponseResultShouldReturnSeatBidsForBidAndAuctionStoredRes final Map storedResponse = new HashMap<>(); storedResponse.put("storedAuctionRequest", mapper.writeValueAsString(singletonList( - SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").build())) - .build()))); + SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").build())).build()))); storedResponse.put("storedBidRequest", mapper.writeValueAsString(singletonList( - SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id2").build())) - .build()))); + SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id2").build())).build()))); given(applicationSettings.getStoredResponses(any(), any())).willReturn( Future.succeededFuture(StoredResponseDataResult.of(storedResponse, emptyList()))); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then - assertThat(result.result()).isEqualTo(StoredResponseResult.of(emptyList(), + assertThat(result.result()).isEqualTo(StoredResponseResult.of( + emptyList(), asList( - SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").impid("impId1") - .build())).build(), - SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id2").impid("impId2") - .build())).build()))); + SeatBid.builder() + .seat("appnexus") + .bid(singletonList(Bid.builder().id("id1").impid("impId1").build())) + .build(), + SeatBid.builder() + .seat("rubicon") + .bid(singletonList(Bid.builder().id("id2").impid("impId2").build())) + .build()))); } @Test public void getStoredResponseResultShouldRemoveMockedBiddersFromImps() throws JsonProcessingException { - final ObjectNode impExt = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"))) - .build(), null)); - impExt.put("rubicon", 1); - impExt.put("appnexus", 2); + final ObjectNode impExt = mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"))) + .bidder(mapper.createObjectNode() + .put("rubicon", 1) + .put("appnexus", 2)) + .build(), + null)); given(bidderCatalog.isValidName(any())).willReturn(true); - final List imps = singletonList(Imp.builder().id("impId1").ext(impExt).build()); + final List imps = singletonList(Imp.builder() + .id("impId1") + .ext(impExt).build()); final Map storedResponse = new HashMap<>(); storedResponse.put("storedBidResponseId1", mapper.writeValueAsString(singletonList( - SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id1").build())) + SeatBid.builder() + .seat("rubicon") + .bid(singletonList(Bid.builder().id("id1").build())) .build()))); given(applicationSettings.getStoredResponses(any(), any())).willReturn( Future.succeededFuture(StoredResponseDataResult.of(storedResponse, emptyList()))); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then - final ObjectNode impExtResult = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"))) - .build(), null)); - impExtResult.put("appnexus", 2); - - assertThat(result.result()).isEqualTo(StoredResponseResult.of(singletonList(Imp.builder().id("impId1") - .ext(impExtResult).build()), - singletonList(SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().impid("impId1") - .id("id1").build())).build()))); + final ObjectNode impExtResult = mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"))) + .bidder(mapper.createObjectNode() + .put("appnexus", 2)) + .build(), + null)); + + assertThat(result.result()).isEqualTo(StoredResponseResult.of( + singletonList(Imp.builder().id("impId1").ext(impExtResult).build()), + singletonList(SeatBid.builder() + .seat("rubicon") + .bid(singletonList(Bid.builder().impid("impId1").id("id1").build())) + .build()))); } @Test public void getStoredResponseResultShouldMergeStoredSeatBidsForTheSameBidder() throws JsonProcessingException { // given final List imps = asList( - Imp.builder().id("impId1") - .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() + Imp.builder() + .id("impId1") + .ext(mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder() .storedAuctionResponse(ExtStoredAuctionResponse.of("storedAuctionRequest")) .build(), - null))).build(), - Imp.builder().id("impId2") - .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() - .storedBidResponse( - singletonList( + null))) + .build(), + Imp.builder() + .id("impId2") + .ext(mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder() + .storedBidResponse(singletonList( ExtStoredBidResponse.of("rubicon", "storedBidRequest"))) - .build(), null))) + .build(), + null))) .build()); final Map storedResponse = new HashMap<>(); storedResponse.put("storedAuctionRequest", mapper.writeValueAsString(asList( - SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").build())) - .build(), SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id3").build())) - .build()))); + SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").build())).build(), + SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id3").build())).build()))); storedResponse.put("storedBidRequest", mapper.writeValueAsString(singletonList( - SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id2").build())) - .build()))); + SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id2").build())).build()))); given(applicationSettings.getStoredResponses(any(), any())).willReturn( Future.succeededFuture(StoredResponseDataResult.of(storedResponse, emptyList()))); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then - assertThat(result.result()).isEqualTo(StoredResponseResult.of(emptyList(), + assertThat(result.result()).isEqualTo(StoredResponseResult.of( + emptyList(), asList( - SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").impid("impId1") - .build())).build(), - SeatBid.builder().seat("rubicon").bid(asList(Bid.builder().id("id3").impid("impId1").build(), - Bid.builder().id("id2").impid("impId2").build())).build()))); + SeatBid.builder() + .seat("appnexus") + .bid(singletonList(Bid.builder().id("id1").impid("impId1").build())) + .build(), + SeatBid.builder() + .seat("rubicon") + .bid(asList( + Bid.builder().id("id3").impid("impId1").build(), + Bid.builder().id("id2").impid("impId2").build())) + .build()))); } @Test public void getStoredResponseResultShouldSupportAliasesWhenDecidingIfImpRequiredRequestToExchange() throws JsonProcessingException { - final ObjectNode impExt = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"))) - .build(), null)); - impExt.put("rubicon", 1); - impExt.put("appnexusAlias", 2); + + final ObjectNode impExt = mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"))) + .bidder(mapper.createObjectNode() + .put("rubicon", 1) + .put("appnexusAlias", 2)) + .build(), + null)); given(bidderCatalog.isValidName(any())).willReturn(false); @@ -335,7 +384,9 @@ public void getStoredResponseResultShouldSupportAliasesWhenDecidingIfImpRequired final Map storedResponse = new HashMap<>(); storedResponse.put("storedBidResponseId1", mapper.writeValueAsString(singletonList( - SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id1").build())) + SeatBid.builder() + .seat("rubicon") + .bid(singletonList(Bid.builder().id("id1").build())) .build()))); given(applicationSettings.getStoredResponses(any(), any())).willReturn( @@ -343,33 +394,39 @@ public void getStoredResponseResultShouldSupportAliasesWhenDecidingIfImpRequired given(aliases.isAliasDefined(eq("appnexusAlias"))).willReturn(true); given(aliases.resolveBidder(eq("appnexusAlias"))).willReturn("appnexus"); - given(aliases.resolveAliasVendorId(eq("appnexusAlias"))).willReturn(1); // when final Future result = storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then - final ObjectNode impExtResult = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"))) - .build(), null)); - impExtResult.put("appnexusAlias", 2); - - assertThat(result.result()).isEqualTo(StoredResponseResult.of(singletonList(Imp.builder().ext(impExtResult) - .id("impId1").build()), - singletonList(SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder() - .id("id1").impid("impId1").build())).build()))); + final ObjectNode impExtResult = mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"))) + .bidder(mapper.createObjectNode() + .put("appnexusAlias", 2)) + .build(), + null)); + + assertThat(result.result()).isEqualTo(StoredResponseResult.of( + singletonList(Imp.builder().ext(impExtResult).id("impId1").build()), + singletonList(SeatBid.builder() + .seat("rubicon") + .bid(singletonList(Bid.builder().id("id1").impid("impId1").build())) + .build()))); } @Test public void getStoredResponseResultShouldReturnFailedFutureWhenImpExtIsNotValid() { // given - final List imps = singletonList(Imp.builder().id("impId").ext(mapper.createObjectNode() - .put("prebid", 5)).build()); + final List imps = singletonList(Imp.builder() + .id("impId") + .ext(mapper.createObjectNode().put("prebid", 5)) + .build()); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then assertThat(result.failed()).isTrue(); @@ -380,14 +437,16 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenImpExtIsNotValid( @Test public void getStoredResponseResultShouldReturnFailedFutureWhenBidderIsMissedInStoredBidResponse() { // given - final ObjectNode impExt = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of(null, "storedBidResponseId1"))) - .build(), null)); + final ObjectNode impExt = mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of(null, "storedBidResponseId1"))) + .build(), + null)); final List imps = singletonList(Imp.builder().id("impId").ext(impExt).build()); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then assertThat(result.failed()).isTrue(); @@ -398,13 +457,16 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenBidderIsMissedInS @Test public void getStoredResponseResultShouldReturnFailedFutureWhenIdIsMissedInStoredBidResponse() { // given - final ObjectNode impExt = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", null))).build(), null)); + final ObjectNode impExt = mapper.valueToTree(ExtImp.of( + ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", null))) + .build(), + null)); final List imps = singletonList(Imp.builder().ext(impExt).id("impId").build()); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then assertThat(result.failed()).isTrue(); @@ -415,6 +477,7 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenIdIsMissedInStore @Test public void getStoredResponseResultShouldReturnFailedFutureWhenSeatIsEmptyInStoredSeatBid() throws JsonProcessingException { + // given final List imps = singletonList(Imp.builder() .ext(mapper.valueToTree(ExtImp.of( @@ -423,14 +486,17 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenSeatIsEmptyInStor .build()); given(applicationSettings.getStoredResponses(any(), any())) - .willReturn(Future.succeededFuture(StoredResponseDataResult.of(singletonMap("responseId", - mapper.writeValueAsString(singletonList(SeatBid.builder().bid(singletonList( - Bid.builder().id("id").build())).build()))), + .willReturn(Future.succeededFuture(StoredResponseDataResult.of( + singletonMap( + "responseId", + mapper.writeValueAsString(singletonList(SeatBid.builder() + .bid(singletonList(Bid.builder().id("id").build())) + .build()))), emptyList()))); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then assertThat(result.failed()).isTrue(); @@ -441,6 +507,7 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenSeatIsEmptyInStor @Test public void getStoredResponseResultShouldReturnFailedFutureWhenBidsAreEmptyInStoredSeatBid() throws JsonProcessingException { + // given final List imps = singletonList(Imp.builder() .ext(mapper.valueToTree(ExtImp.of( @@ -449,15 +516,17 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenBidsAreEmptyInSto .build()); given(applicationSettings.getStoredResponses(any(), any())) - .willReturn(Future.succeededFuture(StoredResponseDataResult.of(singletonMap("responseId", - mapper.writeValueAsString(singletonList(SeatBid.builder() - .seat("seat") - .build()))), + .willReturn(Future.succeededFuture(StoredResponseDataResult.of( + singletonMap( + "responseId", + mapper.writeValueAsString(singletonList(SeatBid.builder() + .seat("seat") + .build()))), emptyList()))); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then assertThat(result.failed()).isTrue(); @@ -466,20 +535,20 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenBidsAreEmptyInSto } @Test - public void getStoredResponseResultShouldReturnFailedFutureSeatBidsCantBeParsed() { + public void getStoredResponseResultShouldReturnFailedFutureSeatBidsCannotBeParsed() { // given final List imps = singletonList(Imp.builder().id("impId") .ext(mapper.valueToTree(ExtImp.of( ExtImpPrebid.builder().storedAuctionResponse(ExtStoredAuctionResponse.of("1")).build(), - null))).build()); + null))) + .build()); - given(applicationSettings.getStoredResponses(any(), any())) - .willReturn(Future.succeededFuture(StoredResponseDataResult.of( - singletonMap("1", "{invalid"), emptyList()))); + given(applicationSettings.getStoredResponses(any(), any())).willReturn(Future.succeededFuture( + StoredResponseDataResult.of(singletonMap("1", "{invalid"), emptyList()))); // when - final Future result = storedResponseProcessor.getStoredResponseResult(imps, - aliases, timeout); + final Future result = + storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout); // then assertThat(result.failed()).isTrue(); @@ -490,121 +559,210 @@ public void getStoredResponseResultShouldReturnFailedFutureSeatBidsCantBeParsed( @Test public void mergeWithBidderResponsesShouldReturnMergedStoredSeatWithResponse() { // given - final List bidderResponses = singletonList(BidderResponse.of("rubicon", BidderSeatBid.of( - singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(), - emptyList()), 100)); + final List bidderResponses = singletonList(BidderResponse.of( + "rubicon", + BidderSeatBid.of( + singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), + emptyList(), + emptyList()), + 100)); final List seatBid = singletonList(SeatBid.builder() - .seat("rubicon").bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())).build()); + .seat("rubicon") + .bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())) + .build()); final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build()); // when - final List result = storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, - imps); + final List result = + storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, imps); // then - assertThat(result).contains(BidderResponse.of("rubicon", BidderSeatBid.of( - asList(BidderBid.of(Bid.builder().id("bid2").impid("storedImp").build(), BidType.banner, "USD"), - BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(), - emptyList()), 100)); + assertThat(result).contains(BidderResponse.of( + "rubicon", + BidderSeatBid.of( + asList( + BidderBid.of( + Bid.builder() + .id("bid2") + .impid("storedImp") + .build(), + BidType.banner, + "USD"), + BidderBid.of( + Bid.builder() + .id("bid1") + .build(), + BidType.banner, + "USD")), + emptyList(), + emptyList()), + 100)); } @Test public void mergeWithBidderResponsesShouldMergeBidderResponsesWithoutCorrespondingStoredSeatBid() { // given - final List bidderResponses = singletonList(BidderResponse.of("rubicon", BidderSeatBid.of( - singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(), - emptyList()), 100)); + final List bidderResponses = singletonList(BidderResponse.of( + "rubicon", + BidderSeatBid.of( + singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), + emptyList(), + emptyList()), + 100)); final List seatBid = singletonList(SeatBid.builder() - .seat("appnexus").bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())).build()); + .seat("appnexus") + .bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())) + .build()); final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build()); // when - final List result = storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, - imps); + final List result = + storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, imps); // then assertThat(result).contains( - BidderResponse.of("rubicon", BidderSeatBid.of( - singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), - emptyList(), - emptyList()), 100), - BidderResponse.of("appnexus", BidderSeatBid.of( - singletonList(BidderBid.of(Bid.builder().id("bid2").impid("storedImp").build(), - BidType.banner, "USD")), emptyList(), emptyList()), 0)); + BidderResponse.of( + "rubicon", + BidderSeatBid.of( + singletonList(BidderBid.of( + Bid.builder().id("bid1").build(), + BidType.banner, + "USD")), + emptyList(), + emptyList()), + 100), + BidderResponse.of( + "appnexus", + BidderSeatBid.of( + singletonList(BidderBid.of( + Bid.builder().id("bid2").impid("storedImp").build(), + BidType.banner, + "USD")), + emptyList(), + emptyList()), + 0)); } @Test public void mergeWithBidderResponsesShouldMergeStoredSeatBidsWithoutBidderResponses() { // given final List seatBid = singletonList(SeatBid.builder() - .seat("rubicon").bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())).build()); + .seat("rubicon") + .bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())) + .build()); final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build()); // when - final List result = storedResponseProcessor.mergeWithBidderResponses(emptyList(), seatBid, - imps); + final List result = + storedResponseProcessor.mergeWithBidderResponses(emptyList(), seatBid, imps); // then - assertThat(result).contains(BidderResponse.of("rubicon", BidderSeatBid.of( - singletonList(BidderBid.of(Bid.builder().id("bid2").impid("storedImp").build(), BidType.banner, "USD")), - emptyList(), emptyList()), 0)); + assertThat(result).contains(BidderResponse.of( + "rubicon", + BidderSeatBid.of( + singletonList(BidderBid.of( + Bid.builder().id("bid2").impid("storedImp").build(), + BidType.banner, + "USD")), + emptyList(), + emptyList()), + 0)); } @Test public void mergeWithBidderResponsesShouldResolveCurrencyFromBidderResponse() { // given - final List bidderResponses = singletonList(BidderResponse.of("rubicon", BidderSeatBid.of( - singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "EUR")), emptyList(), - emptyList()), 100)); + final List bidderResponses = singletonList(BidderResponse.of( + "rubicon", + BidderSeatBid.of( + singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "EUR")), + emptyList(), + emptyList()), + 100)); final List seatBid = singletonList(SeatBid.builder() - .seat("rubicon").bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())).build()); + .seat("rubicon") + .bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())) + .build()); final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build()); // when - final List result = storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, - imps); + final List result = + storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, imps); // then - assertThat(result).contains(BidderResponse.of("rubicon", BidderSeatBid.of( - asList(BidderBid.of(Bid.builder().id("bid2").impid("storedImp").build(), BidType.banner, "EUR"), - BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "EUR")), emptyList(), - emptyList()), 100)); + assertThat(result).contains(BidderResponse.of( + "rubicon", + BidderSeatBid.of( + asList( + BidderBid.of( + Bid.builder().id("bid2").impid("storedImp").build(), + BidType.banner, + "EUR"), + BidderBid.of( + Bid.builder().id("bid1").build(), + BidType.banner, + "EUR")), + emptyList(), + emptyList()), + 100)); } @Test public void mergeWithBidderResponsesShouldResolveBidTypeFromStoredBidExt() { // given - final List bidderResponses = singletonList(BidderResponse.of("rubicon", BidderSeatBid.of( - singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(), - emptyList()), 100)); + final List bidderResponses = singletonList(BidderResponse.of( + "rubicon", + BidderSeatBid.of( + singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), + emptyList(), + emptyList()), + 100)); final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder().type(BidType.video).build(); final List seatBid = singletonList(SeatBid.builder() - .seat("rubicon").bid(singletonList(Bid.builder().ext(mapper.createObjectNode() - .set("prebid", mapper.valueToTree(extBidPrebid))).id("bid2").impid("storedImp").build())) + .seat("rubicon") + .bid(singletonList(Bid.builder() + .id("bid2") + .impid("storedImp") + .ext(mapper.createObjectNode().set("prebid", mapper.valueToTree(extBidPrebid))) + .build())) .build()); final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build()); // when - final List result = storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, - imps); + final List result = + storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, imps); // then - assertThat(result).contains(BidderResponse.of("rubicon", BidderSeatBid.of( - asList(BidderBid.of( - Bid.builder().id("bid2").impid("storedImp").ext(mapper.createObjectNode() - .set("prebid", mapper.valueToTree(extBidPrebid))).build(), BidType.video, "USD"), - BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(), - emptyList()), 100)); + assertThat(result).contains(BidderResponse.of( + "rubicon", + BidderSeatBid.of( + asList(BidderBid.of( + Bid.builder() + .id("bid2") + .impid("storedImp") + .ext(mapper.createObjectNode().set("prebid", mapper.valueToTree(extBidPrebid))) + .build(), + BidType.video, + "USD"), + BidderBid.of( + Bid.builder() + .id("bid1") + .build(), + BidType.banner, + "USD")), + emptyList(), + emptyList()), + 100)); } @Test @@ -613,8 +771,13 @@ public void mergeWithBidderResponsesShouldThrowPrebidExceptionWhenExtBidPrebidIn final ObjectNode extBidPrebid = mapper.createObjectNode().put("type", "invalid"); final List seatBid = singletonList(SeatBid.builder() - .seat("rubicon").bid(singletonList(Bid.builder().ext(mapper.createObjectNode() - .set("prebid", extBidPrebid)).id("bid2").impid("storedImp").build())).build()); + .seat("rubicon") + .bid(singletonList(Bid.builder() + .id("bid2") + .impid("storedImp") + .ext(mapper.createObjectNode().set("prebid", extBidPrebid)) + .build())) + .build()); final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build()); @@ -626,19 +789,27 @@ public void mergeWithBidderResponsesShouldThrowPrebidExceptionWhenExtBidPrebidIn @Test public void mergeWithBidderResponsesShouldReturnSameResponseWhenThereAreNoStoredResponses() { // given - final List bidderResponses = singletonList(BidderResponse.of("rubicon", BidderSeatBid.of( - singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(), - emptyList()), 100)); + final List bidderResponses = singletonList(BidderResponse.of( + "rubicon", + BidderSeatBid.of( + singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), + emptyList(), + emptyList()), + 100)); final List imps = singletonList(Imp.builder().banner(Banner.builder().build()).build()); // when - final List result = storedResponseProcessor.mergeWithBidderResponses(bidderResponses, - emptyList(), imps); + final List result = + storedResponseProcessor.mergeWithBidderResponses(bidderResponses, emptyList(), imps); // then - assertThat(result).containsOnly(BidderResponse.of("rubicon", BidderSeatBid.of( - singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(), - emptyList()), 100)); + assertThat(result).containsOnly(BidderResponse.of( + "rubicon", + BidderSeatBid.of( + singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), + emptyList(), + emptyList()), + 100)); } } diff --git a/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java b/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java index 2f5aad3ea3a..9fbc71e9990 100644 --- a/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java @@ -63,7 +63,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // then assertThat(result.getErrors()) - .containsExactly(BidderError.badInput("$.imp.ext.adoppler.adunit required")); + .containsExactly(BidderError.badInput("adunit parameter is required for adoppler bidder")); } @Test 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 5d8b50ae993..4896ac6f8a5 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -40,8 +40,6 @@ import org.prebid.server.bidder.rubicon.proto.RubiconBannerExt; import org.prebid.server.bidder.rubicon.proto.RubiconBannerExtRp; import org.prebid.server.bidder.rubicon.proto.RubiconImpExt; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtPrebidBidder; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtPrebidRubiconDebug; import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRp; import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRpTrack; import org.prebid.server.bidder.rubicon.proto.RubiconPubExt; @@ -58,7 +56,6 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.ExtPrebidBidders; import org.prebid.server.proto.openrtb.ext.request.ExtApp; -import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpContext; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; @@ -72,6 +69,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUidExt; import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtImpRubicon; import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtImpRubicon.ExtImpRubiconBuilder; +import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtImpRubiconDebug; import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtUserTpIdRubicon; import org.prebid.server.proto.openrtb.ext.request.rubicon.RubiconVideoParams; import org.prebid.server.util.HttpUtil; @@ -2356,10 +2354,11 @@ public void makeBidsShouldReturnBidWithOverriddenCpmFromImp() throws JsonProcess mapper.createObjectNode().put("cpmoverride", 5.55))))) // will be ignored .build()); - final ExtImp extImp = ExtImp.of(ExtImpPrebid.builder() - .bidder(mapper.valueToTree( - RubiconImpExtPrebidBidder.of(RubiconImpExtPrebidRubiconDebug.of(4.44f)))) - .build(), null); + final ExtPrebid extImp = ExtPrebid.of( + null, + ExtImpRubicon.builder() + .debug(ExtImpRubiconDebug.of(4.44f)) + .build()); // when final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest( diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index 6b9544862ba..9a7478ffaeb 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -278,7 +278,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasNullFormatAndNoSiz .banner(Banner.builder() .format(null) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -302,7 +303,8 @@ public void validateShouldReturnEmptyValidationMessagesWhenBannerHasNullFormatAn .h(250) .format(null) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -322,7 +324,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoSi .banner(Banner.builder() .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -345,7 +348,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoHe .w(300) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -368,7 +372,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoWi .h(600) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -392,7 +397,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZero .h(0) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -416,7 +422,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasZeroHeight() { .h(0) .format(singletonList(Format.builder().build())) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -439,7 +446,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZero .w(0) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -463,7 +471,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasZeroWidth() { .w(0) .format(singletonList(Format.builder().build())) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -486,7 +495,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNega .w(-300) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -510,7 +520,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasNegativeWidth() { .w(-300) .format(singletonList(Format.builder().build())) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -533,7 +544,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNega .w(600) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -557,7 +569,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasNegativeHeight() { .w(600) .format(singletonList(Format.builder().build())) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -1150,7 +1163,7 @@ public void validateShouldReturnEmptyValidationMessagesWhenBidRequestIsOk() { } @Test - public void validateShouldReturnValidationMessageWhenNoImpExtBiddersPresent() { + public void validateShouldReturnValidationMessageWhenNoImpExtPrebidPresent() { // given final BidRequest bidRequest = validBidRequestBuilder() .imp(singletonList(validImpBuilder().ext(null).build())) @@ -1161,29 +1174,30 @@ public void validateShouldReturnValidationMessageWhenNoImpExtBiddersPresent() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext must contain at least one bidder"); + .containsOnly("request.imp[0].ext.prebid must be non-empty object"); } @Test - public void validateShouldReturnValidationMessagesWhenImpExtBidderIsUnknown() { + public void validateShouldReturnValidationMessageWhenImpExtPrebidIsNotObject() { // given - final BidRequest bidRequest = validBidRequestBuilder().build(); - given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(false); + final BidRequest bidRequest = validBidRequestBuilder() + .imp(singletonList(validImpBuilder().ext(mapper.valueToTree(singletonMap("prebid", "test"))).build())) + .build(); // when final ValidationResult result = requestValidator.validate(bidRequest); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext contains unknown bidder: rubicon"); + .containsOnly("request.imp[0].ext.prebid must be non-empty object"); } @Test - public void validateShouldReturnEmptyValidationMessagesWhenOnlyPrebidImpExtExist() { + public void validateShouldReturnEmptyValidationMessagesWhenOnlyImpExtPrebidExist() { // given final BidRequest bidRequest = validBidRequestBuilder() .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", "test"))).build())) + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("attr", "value")))).build())) .build(); // when @@ -1193,6 +1207,37 @@ public void validateShouldReturnEmptyValidationMessagesWhenOnlyPrebidImpExtExist assertThat(result.getErrors()).isEmpty(); } + @Test + public void validateShouldReturnValidationMessageWhenImpExtPrebidBidderIsNotObject() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .imp(singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", "test")))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.imp[0].ext.prebid.bidder must be object"); + } + + @Test + public void validateShouldReturnValidationMessagesWhenImpExtPrebidBidderIsUnknown() { + // given + final BidRequest bidRequest = validBidRequestBuilder().build(); + given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(false); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.imp[0].ext.prebid.bidder contains unknown bidder: rubicon"); + } + @Test public void validateShouldReturnValidationMessageWhenBidderExtIsInvalid() { // given @@ -1205,7 +1250,8 @@ public void validateShouldReturnValidationMessageWhenBidderExtIsInvalid() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.rubicon failed validation.\nerrorMessage1\nerrorMessage2"); + .containsOnly( + "request.imp[0].ext.prebid.bidder.rubicon failed validation.\nerrorMessage1\nerrorMessage2"); } @Test @@ -1748,7 +1794,7 @@ public void validateShouldThrowExceptionWhenNativeRequestEmpty() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp.[0].ext.native contains empty request value"); + .containsOnly("request.imp[0].native contains empty request value"); } @Test @@ -1761,7 +1807,7 @@ public void validateShouldThrowExceptionWhenNativeRequestMalformed() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Error while parsing request.imp.[0].ext.native.request"); + .containsOnly("Error while parsing request.imp[0].native.request"); } @Test @@ -2518,7 +2564,7 @@ private static Imp.ImpBuilder validImpBuilder() { .format(singletonList(Format.builder().wmin(1).wratio(5).hratio(1).build())) .build()) .pmp(Pmp.builder().deals(singletonList(Deal.builder().id("1").build())).build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))); + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))); } private static BidRequest overwriteBannerFormatInFirstImp( diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json index 4478ab51de1..ed309b7c5f3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json @@ -12,8 +12,12 @@ ] }, "ext": { - "adman": { - "TagID": "first-tagid" + "prebid": { + "bidder": { + "adman": { + "TagID": "first-tagid" + } + } } } }, @@ -27,8 +31,12 @@ "h": 480 }, "ext": { - "adman": { - "TagID": "second-tagid" + "prebid": { + "bidder": { + "adman": { + "TagID": "second-tagid" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json index fd5e23cb665..819bab7a863 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json @@ -12,10 +12,14 @@ ] }, "ext": { - "adocean": { - "emiter": "myao.adocean.pl", - "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", - "slaveId": "adoceanmyaozpniqismex" + "prebid": { + "bidder": { + "adocean": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json index bb50cf1fdd9..c8de82b117d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json @@ -76,10 +76,14 @@ ] }, "ext": { - "adocean": { - "emiter": "myao.adocean.pl", - "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", - "slaveId": "adoceanmyaozpniqismex" + "prebid": { + "bidder": { + "adocean": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json index 7f817cd6eb4..5eabb73f301 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json @@ -14,8 +14,12 @@ "h": 400 }, "ext": { - "avocet": { - "placement": "5ea9601ac865f911007f1b6a" + "prebid": { + "bidder": { + "avocet": { + "placement": "5ea9601ac865f911007f1b6a" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json index c758d0c5d95..d7f70aa47e7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json @@ -90,8 +90,12 @@ "h": 400 }, "ext": { - "avocet": { - "placement": "5ea9601ac865f911007f1b6a" + "prebid": { + "bidder": { + "avocet": { + "placement": "5ea9601ac865f911007f1b6a" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json index 4cf9ae64ba2..f22fe111f8d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json @@ -12,8 +12,12 @@ ] }, "ext": { - "beintoo": { - "tagid": "25251" + "prebid": { + "bidder": { + "beintoo": { + "tagid": "25251" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json index 0e159e53c0e..6487685eeaf 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json @@ -78,8 +78,12 @@ ] }, "ext": { - "beintoo": { - "tagid": "25251" + "prebid": { + "bidder": { + "beintoo": { + "tagid": "25251" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-request.json b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-request.json index a3edebf4a84..6282fac2e20 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-request.json @@ -12,9 +12,13 @@ ] }, "ext": { - "datablocks": { - "host": "localhost:8090", - "sourceId": 1 + "prebid": { + "bidder": { + "datablocks": { + "host": "localhost:8090", + "sourceId": 1 + } + } } } }, @@ -29,9 +33,13 @@ ] }, "ext": { - "datablocks": { - "host": "localhost:8090", - "sourceId": 2 + "prebid": { + "bidder": { + "datablocks": { + "host": "localhost:8090", + "sourceId": 2 + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json index 66ddb8803ef..6334dca90c2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json @@ -129,9 +129,13 @@ ] }, "ext": { - "datablocks": { - "host": "localhost:8090", - "sourceId": 1 + "prebid": { + "bidder": { + "datablocks": { + "host": "localhost:8090", + "sourceId": 1 + } + } } } }, @@ -146,9 +150,13 @@ "pos": 1 }, "ext": { - "datablocks": { - "host": "localhost:8090", - "sourceId": 2 + "prebid": { + "bidder": { + "datablocks": { + "host": "localhost:8090", + "sourceId": 2 + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json index 9f0db45cbbc..3b58e7b6288 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-request.json @@ -12,8 +12,12 @@ ] }, "ext": { - "emx_digital": { - "tagid": "25251" + "prebid": { + "bidder": { + "emx_digital": { + "tagid": "25251" + } + } } } } @@ -24,7 +28,7 @@ "language": "en", "ifa": "ifaId", "ua": "Android Chrome/60", - "ip" : "127.0.0.1" + "ip": "127.0.0.1" }, "site": { "page": "http://www.example.com", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json index 1eb31ec7cf8..74d80b920ab 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json @@ -78,8 +78,12 @@ ] }, "ext": { - "emx_digital": { - "tagid": "25251" + "prebid": { + "bidder": { + "emx_digital": { + "tagid": "25251" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json index a2d90139b66..1cc440f4aaa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-request.json @@ -14,8 +14,12 @@ "h": 400 }, "ext": { - "kubient": { - "zoneid": "9042" + "prebid": { + "bidder": { + "kubient": { + "zoneid": "9042" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json index 22af329541c..f606cc5a73d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kubient/test-auction-kubient-response.json @@ -84,8 +84,12 @@ "h": 400 }, "ext": { - "kubient": { - "zoneid":"9042" + "prebid": { + "bidder": { + "kubient": { + "zoneid": "9042" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json index 2484264b8a9..c6c718fbb83 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json @@ -22,35 +22,37 @@ ] }, "ext": { - "rubicon": { - "accountId": 2001, - "siteId": 3001, - "zoneId": 4001, - "inventory": { - "rating": [ - "5-star" - ], - "prodtype": [ - "tech" - ] - }, - "visitor": { - "ucat": [ - "new" - ], - "search": [ - "iphone" - ] - }, - "video": { - "size_id": 15, - "playerWidth": 780, - "playerHeight": "438", - "skip": 5, - "skipdelay": 1 - } - }, "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001, + "inventory": { + "rating": [ + "5-star" + ], + "prodtype": [ + "tech" + ] + }, + "visitor": { + "ucat": [ + "new" + ], + "search": [ + "iphone" + ] + }, + "video": { + "size_id": 15, + "playerWidth": 780, + "playerHeight": "438", + "skip": 5, + "skipdelay": 1 + } + } + }, "is_rewarded_inventory": 1 } } @@ -79,37 +81,41 @@ ] }, "ext": { - "appnexus": { - "member": "103", - "inv_code": "abc", - "reserve": 1.0, - "position": "below", - "traffic_source_code": "trafficSource", - "keywords": [ - { - "key": "foo", - "value": [ - "bar", - "baz" + "prebid": { + "bidder": { + "appnexus": { + "member": "103", + "inv_code": "abc", + "reserve": 1.0, + "position": "below", + "traffic_source_code": "trafficSource", + "keywords": [ + { + "key": "foo", + "value": [ + "bar", + "baz" + ] + } ] - } - ] - }, - "appnexusAlias": { - "member": "104", - "inv_code": "abc", - "reserve": 1.0, - "position": "above", - "traffic_source_code": "trafficSourceAlias", - "keywords": [ - { - "key": "foo", - "value": [ - "barAlias", - "bazAlias" + }, + "appnexusAlias": { + "member": "104", + "inv_code": "abc", + "reserve": 1.0, + "position": "above", + "traffic_source_code": "trafficSourceAlias", + "keywords": [ + { + "key": "foo", + "value": [ + "barAlias", + "bazAlias" + ] + } ] } - ] + } } } }, @@ -120,8 +126,12 @@ "ver": "1.1" }, "ext": { - "appnexus": { - "placement_id": 9880618 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 9880618 + } + } } } }, @@ -133,8 +143,12 @@ ] }, "ext": { - "appnexus": { - "placement_id": 10433394 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 10433394 + } + } } } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json index 1683448972d..99dc0964de7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json @@ -430,35 +430,37 @@ ] }, "ext": { - "rubicon": { - "accountId": 2001, - "siteId": 3001, - "zoneId": 4001, - "inventory": { - "rating": [ - "5-star" - ], - "prodtype": [ - "tech" - ] - }, - "visitor": { - "ucat": [ - "new" - ], - "search": [ - "iphone" - ] - }, - "video": { - "size_id": 15, - "playerWidth": 780, - "playerHeight": "438", - "skip": 5, - "skipdelay": 1 - } - }, "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001, + "inventory": { + "rating": [ + "5-star" + ], + "prodtype": [ + "tech" + ] + }, + "visitor": { + "ucat": [ + "new" + ], + "search": [ + "iphone" + ] + }, + "video": { + "size_id": 15, + "playerWidth": 780, + "playerHeight": "438", + "skip": 5, + "skipdelay": 1 + } + } + }, "is_rewarded_inventory": 1 } } @@ -476,12 +478,14 @@ "h": 600 }, "ext": { - "rubicon": { - "accountId": 5001, - "siteId": 6001, - "zoneId": 7001 - }, "prebid": { + "bidder": { + "rubicon": { + "accountId": 5001, + "siteId": 6001, + "zoneId": 7001 + } + }, "storedrequest": { "id": "test-rubicon-stored-request-2" } @@ -503,37 +507,41 @@ ] }, "ext": { - "appnexus": { - "member": "103", - "inv_code": "abc", - "reserve": 1.0, - "position": "below", - "traffic_source_code": "trafficSource", - "keywords": [ - { - "key": "foo", - "value": [ - "bar", - "baz" + "prebid": { + "bidder": { + "appnexus": { + "member": "103", + "inv_code": "abc", + "reserve": 1.0, + "position": "below", + "traffic_source_code": "trafficSource", + "keywords": [ + { + "key": "foo", + "value": [ + "bar", + "baz" + ] + } ] - } - ] - }, - "appnexusAlias": { - "member": "104", - "inv_code": "abc", - "reserve": 1.0, - "position": "above", - "traffic_source_code": "trafficSourceAlias", - "keywords": [ - { - "key": "foo", - "value": [ - "barAlias", - "bazAlias" + }, + "appnexusAlias": { + "member": "104", + "inv_code": "abc", + "reserve": 1.0, + "position": "above", + "traffic_source_code": "trafficSourceAlias", + "keywords": [ + { + "key": "foo", + "value": [ + "barAlias", + "bazAlias" + ] + } ] } - ] + } } } }, @@ -544,8 +552,12 @@ "ver": "1.1" }, "ext": { - "appnexus": { - "placement_id": 9880618 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 9880618 + } + } } } }, @@ -557,8 +569,12 @@ ] }, "ext": { - "appnexus": { - "placement_id": 10433394 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 10433394 + } + } } } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json index 073016adb28..276ab3616a8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json @@ -12,14 +12,18 @@ ] }, "ext": { - "sharethrough": { - "pkey": "abc123", - "iframe": true, - "iframeSize": [ - 50, - 50 - ], - "bidfloor": 3.5 + "prebid": { + "bidder": { + "sharethrough": { + "pkey": "abc123", + "iframe": true, + "iframeSize": [ + 50, + 50 + ], + "bidfloor": 3.5 + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json index e86ac798d08..b20ecd3bf0a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json @@ -79,14 +79,18 @@ ] }, "ext": { - "sharethrough": { - "pkey": "abc123", - "iframe": true, - "iframeSize": [ - 50, - 50 - ], - "bidfloor": 3.5 + "prebid": { + "bidder": { + "sharethrough": { + "pkey": "abc123", + "iframe": true, + "iframeSize": [ + 50, + 50 + ], + "bidfloor": 3.5 + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json index 34abff9aca8..5076a6a85dd 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json @@ -14,11 +14,15 @@ "h": 400 }, "ext": { - "smartadserver": { - "siteId": 1, - "pageId": 2, - "formatId": 3, - "networkId": 73 + "prebid": { + "bidder": { + "smartadserver": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json index f53c1f4dcc4..3eadc63eab9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json @@ -84,11 +84,15 @@ "h": 400 }, "ext": { - "smartadserver": { - "siteId": 1, - "pageId": 2, - "formatId": 3, - "networkId": 73 + "prebid": { + "bidder": { + "smartadserver": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json index c3c161d62aa..bc3effc291b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json @@ -7,8 +7,12 @@ "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"required\":1,\"title\":{\"len\":500}}]}" }, "ext": { - "triplelift_native": { - "inventoryCode": "foo" + "prebid": { + "bidder": { + "triplelift_native": { + "inventoryCode": "foo" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json index 570aefa9feb..1e9e509aa44 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json @@ -78,8 +78,12 @@ "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":500}}]}" }, "ext": { - "triplelift_native": { - "inventoryCode": "foo" + "prebid": { + "bidder": { + "triplelift_native": { + "inventoryCode": "foo" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json index c53a1e56cb3..13e6ab9ba21 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json @@ -12,15 +12,19 @@ ] }, "ext": { - "yieldlab": { - "adslotId": "12345", - "supplyId": "123456789", - "adSize": "400x300", - "targeting": { - "key1": "value1", - "key2": "value2" - }, - "extId": "abc" + "prebid": { + "bidder": { + "yieldlab": { + "adslotId": "12345", + "supplyId": "123456789", + "adSize": "400x300", + "targeting": { + "key1": "value1", + "key2": "value2" + }, + "extId": "abc" + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json index 1f970ec91b1..92eda441221 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json @@ -127,14 +127,18 @@ ] }, "ext": { - "yieldlab": { - "adSize": "400x300", - "adslotId": "12345", - "extId": "abc", - "supplyId": "123456789", - "targeting": { - "key1": "value1", - "key2": "value2" + "prebid": { + "bidder": { + "yieldlab": { + "adSize": "400x300", + "adslotId": "12345", + "extId": "abc", + "supplyId": "123456789", + "targeting": { + "key1": "value1", + "key2": "value2" + } + } } } }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json index be89aba4f07..cf0d5e2ef9c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json @@ -12,9 +12,13 @@ ] }, "ext": { - "zeroclickfraud": { - "host": "localhost:8090", - "sourceId": 1 + "prebid": { + "bidder": { + "zeroclickfraud": { + "host": "localhost:8090", + "sourceId": 1 + } + } } } }, @@ -29,9 +33,13 @@ ] }, "ext": { - "zeroclickfraud": { - "host": "localhost:8090", - "sourceId": 2 + "prebid": { + "bidder": { + "zeroclickfraud": { + "host": "localhost:8090", + "sourceId": 2 + } + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json index 6b9f939a0b9..e4bd8fe0950 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json @@ -129,9 +129,13 @@ ] }, "ext": { - "zeroclickfraud": { - "host": "localhost:8090", - "sourceId": 1 + "prebid": { + "bidder": { + "zeroclickfraud": { + "host": "localhost:8090", + "sourceId": 1 + } + } } } }, @@ -146,9 +150,13 @@ "pos": 1 }, "ext": { - "zeroclickfraud": { - "host": "localhost:8090", - "sourceId": 2 + "prebid": { + "bidder": { + "zeroclickfraud": { + "host": "localhost:8090", + "sourceId": 2 + } + } } } } From 97453c1715ee48f0c438615e4ae5c531c294fe38 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Thu, 11 Feb 2021 14:28:29 +0100 Subject: [PATCH 121/129] Process feedback --- .../server/bidder/adhese/AdheseBidder.java | 21 ++++++++++--------- .../adhese/model/AdheseRequestBody.java | 20 +++++++++++------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java index caa4d3e65bc..03ad3ea7069 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java +++ b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java @@ -52,7 +52,7 @@ public class AdheseBidder implements Bidder { private static final TypeReference> ADHESE_EXT_TYPE_REFERENCE = - new TypeReference>() { + new TypeReference<>() { }; private static final String ORIGIN_BID = "JERLICIA"; @@ -111,14 +111,15 @@ private ExtImpAdhese parseImpExt(Imp imp) { } private AdheseRequestBody buildBody(BidRequest request, ExtImpAdhese extImpAdhese) { - AdheseRequestBody body = new AdheseRequestBody(); - body.slots.add(AdheseRequestBody.Slot.create(getSlotParameter(extImpAdhese))); - body.parameters.putAll(getTargetParameters(extImpAdhese)); - body.parameters.putAll(getGdprParameter(request.getUser())); - body.parameters.putAll(getRefererParameter(request.getSite())); - body.parameters.putAll(getIfaParameter(request.getDevice())); - - return body; + Map> parameterMap = new TreeMap<>(); + parameterMap.putAll(getTargetParameters(extImpAdhese)); + parameterMap.putAll(getGdprParameter(request.getUser())); + parameterMap.putAll(getRefererParameter(request.getSite())); + parameterMap.putAll(getIfaParameter(request.getDevice())); + + return new AdheseRequestBody( + Collections.singletonList(AdheseRequestBody.Slot.create(getSlotParameter(extImpAdhese))), + parameterMap); } private String getUrl(ExtImpAdhese extImpAdhese) { @@ -133,7 +134,7 @@ private static String getSlotParameter(ExtImpAdhese extImpAdhese) { private Map> getTargetParameters(ExtImpAdhese extImpAdhese) { final JsonNode targets = extImpAdhese.getTargets(); - return targets == null || targets.isNull() ? Collections.emptyMap() : parseTargetParametersAndSort(targets); + return targets == null || targets.isEmpty() ? Collections.emptyMap() : parseTargetParametersAndSort(targets); } private Map> parseTargetParametersAndSort(JsonNode targets) { diff --git a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java index 1f8d58bc7d0..79c58e3da6a 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java +++ b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java @@ -1,11 +1,13 @@ package org.prebid.server.bidder.adhese.model; -import java.util.ArrayList; +import lombok.Getter; +import lombok.Value; + import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.TreeMap; +@Value public class AdheseRequestBody { public static class Slot { @@ -16,12 +18,9 @@ public static Slot create(String slotname) { return slot; } + @Getter private String slotname; - public String getSlotname() { - return slotname; - } - @Override public String toString() { return "slot: " + slotname; @@ -45,9 +44,14 @@ public int hashCode() { } } - public List slots = new ArrayList<>(); + List slots; - public Map> parameters = new TreeMap<>(); + Map> parameters; + + public AdheseRequestBody(List slots, Map> parameters) { + this.slots = slots; + this.parameters = parameters; + } @Override public String toString() { From ff1c90aea38f6f97d2f01691de4c4841326e0a23 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Thu, 11 Feb 2021 15:21:34 +0100 Subject: [PATCH 122/129] Looks like this can't go --- src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java index 03ad3ea7069..28f44080f23 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java +++ b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java @@ -52,7 +52,7 @@ public class AdheseBidder implements Bidder { private static final TypeReference> ADHESE_EXT_TYPE_REFERENCE = - new TypeReference<>() { + new TypeReference>() { }; private static final String ORIGIN_BID = "JERLICIA"; From fbea48cab3a952e520e893f49cd1c39f9639d1f4 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Thu, 18 Feb 2021 12:15:22 +0100 Subject: [PATCH 123/129] Process feedback --- .../server/bidder/adhese/AdheseBidder.java | 12 +++-- .../adhese/model/AdheseRequestBody.java | 52 +++---------------- 2 files changed, 15 insertions(+), 49 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java index 28f44080f23..df9545801f4 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java +++ b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java @@ -111,15 +111,19 @@ private ExtImpAdhese parseImpExt(Imp imp) { } private AdheseRequestBody buildBody(BidRequest request, ExtImpAdhese extImpAdhese) { - Map> parameterMap = new TreeMap<>(); + final Map> parameterMap = new TreeMap<>(); parameterMap.putAll(getTargetParameters(extImpAdhese)); parameterMap.putAll(getGdprParameter(request.getUser())); parameterMap.putAll(getRefererParameter(request.getSite())); parameterMap.putAll(getIfaParameter(request.getDevice())); - return new AdheseRequestBody( - Collections.singletonList(AdheseRequestBody.Slot.create(getSlotParameter(extImpAdhese))), - parameterMap); + return AdheseRequestBody.builder() + .slots(Collections.singletonList( + AdheseRequestBody.Slot.builder() + .slotname(getSlotParameter(extImpAdhese)) + .build())) + .parameters(parameterMap) + .build(); } private String getUrl(ExtImpAdhese extImpAdhese) { diff --git a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java index 79c58e3da6a..9c9b709ce82 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java +++ b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java @@ -1,63 +1,25 @@ package org.prebid.server.bidder.adhese.model; +import lombok.Builder; import lombok.Getter; import lombok.Value; import java.util.List; import java.util.Map; -import java.util.Objects; +@Builder @Value public class AdheseRequestBody { - public static class Slot { - - public static Slot create(String slotname) { - Slot slot = new Slot(); - slot.slotname = slotname; - return slot; - } - - @Getter - private String slotname; - - @Override - public String toString() { - return "slot: " + slotname; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof Slot)) { - return false; - } - Slot slot = (Slot) o; - return Objects.equals(slotname, slot.slotname); - } - - @Override - public int hashCode() { - return Objects.hash(slotname); - } - } - List slots; Map> parameters; - public AdheseRequestBody(List slots, Map> parameters) { - this.slots = slots; - this.parameters = parameters; - } + @Builder + @Value + public static class Slot { - @Override - public String toString() { - return "RequestBody{" - + "slots=" + slots - + ", parameters=" + parameters - + '}'; + @Getter + private String slotname; } } From afc068f43bcad7a16650f9679a4fa637e7a3557b Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Mon, 22 Feb 2021 09:06:10 +0100 Subject: [PATCH 124/129] Process feedback --- .../prebid/server/bidder/adhese/model/AdheseRequestBody.java | 5 +---- .../org/prebid/server/bidder/adhese/AdheseBidderTest.java | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java index 9c9b709ce82..608f79d905b 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java +++ b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java @@ -1,7 +1,6 @@ package org.prebid.server.bidder.adhese.model; import lombok.Builder; -import lombok.Getter; import lombok.Value; import java.util.List; @@ -12,14 +11,12 @@ public class AdheseRequestBody { List slots; - Map> parameters; @Builder @Value public static class Slot { - @Getter - private String slotname; + String slotname; } } diff --git a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java index eebdb5446e0..697f5d21c4e 100644 --- a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java @@ -123,7 +123,7 @@ public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequest .containsOnly("https://ads-demo.adhese.com/json"); assertThat(result.getValue()) .extracting(HttpRequest::getBody) - .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}]," + .containsExactly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}]," + "\"parameters\":{\"ag\":[\"55\"],\"ci\":[\"gent\",\"brussels\"]," + "\"tl\":[\"all\"],\"xt\":[\"dummy\"],\"xz\":[\"dum-my\"]}}"); } From 70e0b75f58af2b63f506f14b5a3e790e9352c0ae Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Wed, 28 Apr 2021 10:09:10 +0200 Subject: [PATCH 125/129] Replace literal JSON strings with AdheseRequestBody --- .../bidder/adhese/AdheseBidderTest.java | 64 +++++++++++++++---- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java index 697f5d21c4e..dba5ba70b5f 100644 --- a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java @@ -17,6 +17,7 @@ import org.prebid.server.VertxTest; import org.prebid.server.bidder.adhese.model.AdheseBid; import org.prebid.server.bidder.adhese.model.AdheseOriginData; +import org.prebid.server.bidder.adhese.model.AdheseRequestBody; import org.prebid.server.bidder.adhese.model.AdheseResponseExt; import org.prebid.server.bidder.adhese.model.Cpm; import org.prebid.server.bidder.adhese.model.CpmValues; @@ -33,9 +34,11 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import java.math.BigDecimal; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TreeMap; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; @@ -94,7 +97,7 @@ public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { } @Test - public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequestUri() { + public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequestUri() throws JsonProcessingException { // given Map> targets = new HashMap<>(); targets.put("ci", asList("gent", "brussels")); @@ -123,13 +126,24 @@ public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequest .containsOnly("https://ads-demo.adhese.com/json"); assertThat(result.getValue()) .extracting(HttpRequest::getBody) - .containsExactly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}]," - + "\"parameters\":{\"ag\":[\"55\"],\"ci\":[\"gent\",\"brussels\"]," - + "\"tl\":[\"all\"],\"xt\":[\"dummy\"],\"xz\":[\"dum-my\"]}}"); + .containsOnly(jacksonMapper.mapper().writeValueAsString( + AdheseRequestBody + .builder() + .slots(Collections.singletonList( + AdheseRequestBody.Slot.builder() + .slotname("_adhese_prebid_demo_-leaderboard") + .build())) + .parameters(new TreeMap<>(Map.of( + "ag", List.of("55"), + "ci", List.of("gent", "brussels"), + "tl", List.of("all"), + "xt", List.of("dummy"), + "xz", List.of("dum-my")))) + .build())); } @Test - public void makeHttpRequestsShouldModifyIncomingRequestWithIfaParameter() { + public void makeHttpRequestsShouldModifyIncomingRequestWithIfaParameter() throws JsonProcessingException { // given final BidRequest bidRequest = BidRequest.builder() .device(Device.builder().ifa("ifaValue").build()) @@ -149,12 +163,20 @@ public void makeHttpRequestsShouldModifyIncomingRequestWithIfaParameter() { .containsOnly("https://ads-demo.adhese.com/json"); assertThat(result.getValue()) .extracting(HttpRequest::getBody) - .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}]," - + "\"parameters\":{\"xz\":[\"ifaValue\"]}}"); + .containsOnly(jacksonMapper.mapper().writeValueAsString( + AdheseRequestBody + .builder() + .slots(Collections.singletonList( + AdheseRequestBody.Slot.builder() + .slotname("_adhese_prebid_demo_-leaderboard") + .build())) + .parameters(new TreeMap<>(Map.of( + "xz", List.of("ifaValue")))) + .build())); } @Test - public void makeHttpRequestsShouldModifyIncomingRequestWithRefererParameter() { + public void makeHttpRequestsShouldModifyIncomingRequestWithRefererParameter() throws JsonProcessingException { // given final BidRequest bidRequest = BidRequest.builder() .site(Site.builder().page("pageValue").build()) @@ -174,12 +196,20 @@ public void makeHttpRequestsShouldModifyIncomingRequestWithRefererParameter() { .containsOnly("https://ads-demo.adhese.com/json"); assertThat(result.getValue()) .extracting(HttpRequest::getBody) - .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}]," - + "\"parameters\":{\"xf\":[\"pageValue\"]}}"); + .containsOnly(jacksonMapper.mapper().writeValueAsString( + AdheseRequestBody + .builder() + .slots(Collections.singletonList( + AdheseRequestBody.Slot.builder() + .slotname("_adhese_prebid_demo_-leaderboard") + .build())) + .parameters(new TreeMap<>(Map.of( + "xf", List.of("pageValue")))) + .build())); } @Test - public void makeHttpRequestsShouldNotModifyIncomingRequestIfTargetsNotPresent() { + public void makeHttpRequestsShouldNotModifyIncomingRequestIfTargetsNotPresent() throws JsonProcessingException { // given final BidRequest bidRequest = BidRequest.builder() .user(User.builder() @@ -203,8 +233,16 @@ public void makeHttpRequestsShouldNotModifyIncomingRequestIfTargetsNotPresent() .containsOnly("https://ads-demo.adhese.com/json"); assertThat(result.getValue()) .extracting(HttpRequest::getBody) - .containsOnly("{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}]," - + "\"parameters\":{\"xt\":[\"dummy\"]}}"); + .containsOnly(jacksonMapper.mapper().writeValueAsString( + AdheseRequestBody + .builder() + .slots(Collections.singletonList( + AdheseRequestBody.Slot.builder() + .slotname("_adhese_prebid_demo_-leaderboard") + .build())) + .parameters(new TreeMap<>(Map.of( + "xt", List.of("dummy")))) + .build())); } @Test From c401146ce01b5d304c8029b6557d46a691b04c76 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Wed, 28 Apr 2021 10:13:36 +0200 Subject: [PATCH 126/129] fix over-sized line --- .../java/org/prebid/server/bidder/adhese/AdheseBidderTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java index dba5ba70b5f..6d27c4e3a65 100644 --- a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java @@ -97,7 +97,8 @@ public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { } @Test - public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequestUri() throws JsonProcessingException { + public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequestUri() + throws JsonProcessingException { // given Map> targets = new HashMap<>(); targets.put("ci", asList("gent", "brussels")); From 3fe60403f7fd514d3cbd567573edf80d57223add Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Wed, 28 Apr 2021 10:38:34 +0200 Subject: [PATCH 127/129] Replace List.of with Arrays.asList --- .../server/bidder/adhese/AdheseBidderTest.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java index 6d27c4e3a65..e078f101e6d 100644 --- a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java @@ -34,6 +34,7 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import java.math.BigDecimal; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -135,11 +136,11 @@ public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequest .slotname("_adhese_prebid_demo_-leaderboard") .build())) .parameters(new TreeMap<>(Map.of( - "ag", List.of("55"), - "ci", List.of("gent", "brussels"), - "tl", List.of("all"), - "xt", List.of("dummy"), - "xz", List.of("dum-my")))) + "ag", Arrays.asList("55"), + "ci", Arrays.asList("gent", "brussels"), + "tl", Arrays.asList("all"), + "xt", Arrays.asList("dummy"), + "xz", Arrays.asList("dum-my")))) .build())); } @@ -172,7 +173,7 @@ public void makeHttpRequestsShouldModifyIncomingRequestWithIfaParameter() throws .slotname("_adhese_prebid_demo_-leaderboard") .build())) .parameters(new TreeMap<>(Map.of( - "xz", List.of("ifaValue")))) + "xz", Arrays.asList("ifaValue")))) .build())); } @@ -205,7 +206,7 @@ public void makeHttpRequestsShouldModifyIncomingRequestWithRefererParameter() th .slotname("_adhese_prebid_demo_-leaderboard") .build())) .parameters(new TreeMap<>(Map.of( - "xf", List.of("pageValue")))) + "xf", Arrays.asList("pageValue")))) .build())); } @@ -242,7 +243,7 @@ public void makeHttpRequestsShouldNotModifyIncomingRequestIfTargetsNotPresent() .slotname("_adhese_prebid_demo_-leaderboard") .build())) .parameters(new TreeMap<>(Map.of( - "xt", List.of("dummy")))) + "xt", Arrays.asList("dummy")))) .build())); } From 03c63d7d380b34a954d60ddc954a95a58aba52e6 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Wed, 28 Apr 2021 10:44:28 +0200 Subject: [PATCH 128/129] Replace the other Java 9 shorthands --- .../server/bidder/adhese/AdheseBidderTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java index e078f101e6d..6e75ba79e30 100644 --- a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java @@ -135,12 +135,12 @@ public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequest AdheseRequestBody.Slot.builder() .slotname("_adhese_prebid_demo_-leaderboard") .build())) - .parameters(new TreeMap<>(Map.of( - "ag", Arrays.asList("55"), - "ci", Arrays.asList("gent", "brussels"), - "tl", Arrays.asList("all"), - "xt", Arrays.asList("dummy"), - "xz", Arrays.asList("dum-my")))) + .parameters(new TreeMap<>() {{ + put("ag", Arrays.asList("55")); + put("ci", Arrays.asList("gent", "brussels")); + put("tl", Arrays.asList("all")); + put("xt", Arrays.asList("dummy")); + put("xz", Arrays.asList("dum-my")); }}) .build())); } @@ -172,7 +172,7 @@ public void makeHttpRequestsShouldModifyIncomingRequestWithIfaParameter() throws AdheseRequestBody.Slot.builder() .slotname("_adhese_prebid_demo_-leaderboard") .build())) - .parameters(new TreeMap<>(Map.of( + .parameters(new TreeMap<>(Collections.singletonMap( "xz", Arrays.asList("ifaValue")))) .build())); } @@ -205,7 +205,7 @@ public void makeHttpRequestsShouldModifyIncomingRequestWithRefererParameter() th AdheseRequestBody.Slot.builder() .slotname("_adhese_prebid_demo_-leaderboard") .build())) - .parameters(new TreeMap<>(Map.of( + .parameters(new TreeMap<>(Collections.singletonMap( "xf", Arrays.asList("pageValue")))) .build())); } @@ -242,7 +242,7 @@ public void makeHttpRequestsShouldNotModifyIncomingRequestIfTargetsNotPresent() AdheseRequestBody.Slot.builder() .slotname("_adhese_prebid_demo_-leaderboard") .build())) - .parameters(new TreeMap<>(Map.of( + .parameters(new TreeMap<>(Collections.singletonMap( "xt", Arrays.asList("dummy")))) .build())); } From 50e2eca8bdf4c7745513d955d2c3dacf4cc5f227 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Wed, 28 Apr 2021 10:51:39 +0200 Subject: [PATCH 129/129] Explicit typing --- .../java/org/prebid/server/bidder/adhese/AdheseBidderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java index 6e75ba79e30..7cb67c1739b 100644 --- a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java @@ -135,7 +135,7 @@ public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequest AdheseRequestBody.Slot.builder() .slotname("_adhese_prebid_demo_-leaderboard") .build())) - .parameters(new TreeMap<>() {{ + .parameters(new TreeMap>() {{ put("ag", Arrays.asList("55")); put("ci", Arrays.asList("gent", "brussels")); put("tl", Arrays.asList("all"));