diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index 9d0146b23da..685d5e27180 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -290,8 +290,7 @@ private static boolean isEmptyBidderResponses(List bidderRes .allMatch(CollectionUtils::isEmpty); } - private static List toBidderResponseInfos(List bidderResponses, - List imps) { + private List toBidderResponseInfos(List bidderResponses, List imps) { final List result = new ArrayList<>(); for (BidderResponse bidderResponse : bidderResponses) { final String bidder = bidderResponse.getBidder(); @@ -317,7 +316,7 @@ private static List toBidderResponseInfos(List imps, String bidder) { + private BidInfo toBidInfo(Bid bid, BidType type, List imps, String bidder) { return BidInfo.builder() .bid(bid) .bidType(type) @@ -358,8 +357,8 @@ private Future cacheBidsAndCreateResponse(List final Set winningBidInfos = targeting == null ? null : bidInfos.stream() - .filter(bidInfo -> bidInfo.getTargetingInfo().isWinningBid()) - .collect(Collectors.toSet()); + .filter(bidInfo -> bidInfo.getTargetingInfo().isWinningBid()) + .collect(Collectors.toSet()); final Set bidsToCache = cacheInfo.isShouldCacheWinningBidsOnly() ? winningBidInfos : bidInfos; diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 9e35d41aabd..6a82ab44a10 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -14,9 +14,11 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.StoredRequestProcessor; import org.prebid.server.auction.TimeoutResolver; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.IpAddress; import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.exception.BlacklistedAccountException; import org.prebid.server.exception.InvalidRequestException; @@ -55,10 +57,11 @@ public class Ortb2RequestFactory { private final List blacklistedAccounts; private final UidsCookieService uidsCookieService; private final RequestValidator requestValidator; + private final TimeoutResolver timeoutResolver; private final TimeoutFactory timeoutFactory; private final StoredRequestProcessor storedRequestProcessor; private final ApplicationSettings applicationSettings; - private final TimeoutResolver timeoutResolver; + private final IpAddressHelper ipAddressHelper; public Ortb2RequestFactory(boolean enforceValidAccount, List blacklistedAccounts, @@ -67,7 +70,8 @@ public Ortb2RequestFactory(boolean enforceValidAccount, TimeoutResolver timeoutResolver, TimeoutFactory timeoutFactory, StoredRequestProcessor storedRequestProcessor, - ApplicationSettings applicationSettings) { + ApplicationSettings applicationSettings, + IpAddressHelper ipAddressHelper) { this.enforceValidAccount = enforceValidAccount; this.blacklistedAccounts = Objects.requireNonNull(blacklistedAccounts); @@ -75,8 +79,9 @@ public Ortb2RequestFactory(boolean enforceValidAccount, this.requestValidator = Objects.requireNonNull(requestValidator); this.timeoutResolver = Objects.requireNonNull(timeoutResolver); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); - this.applicationSettings = Objects.requireNonNull(applicationSettings); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.applicationSettings = Objects.requireNonNull(applicationSettings); + this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); } public Future fetchAccountAndCreateAuctionContext(RoutingContext routingContext, @@ -280,21 +285,30 @@ private ExtRequest enrichExtRequest(ExtRequest ext, Account account) { private Device enrichDevice(Device device, PrivacyContext privacyContext) { final String ipAddress = privacyContext.getIpAddress(); - final String country = getIfNotNull(privacyContext.getTcfContext().getGeoInfo(), GeoInfo::getCountry); + final IpAddress ip = ipAddressHelper.toIpAddress(ipAddress); - final String ipAddressInRequest = getIfNotNull(device, Device::getIp); + final String ipV4InRequest = getIfNotNull(device, Device::getIp); + final String ipV4 = ip != null && ip.getVersion() == IpAddress.IP.v4 ? ipAddress : null; + final boolean shouldUpdateIpV4 = ipV4 != null && !Objects.equals(ipV4InRequest, ipV4); - final Geo geo = getIfNotNull(device, Device::getGeo); - final String countryFromRequest = getIfNotNull(geo, Geo::getCountry); + final String ipV6InRequest = getIfNotNull(device, Device::getIpv6); + final String ipV6 = ip != null && ip.getVersion() == IpAddress.IP.v6 ? ipAddress : null; + final boolean shouldUpdateIpV6 = ipV6 != null && !Objects.equals(ipV6InRequest, ipV6); - final boolean shouldUpdateIp = ipAddress != null && !Objects.equals(ipAddressInRequest, ipAddress); - final boolean shouldUpdateCountry = country != null && !Objects.equals(countryFromRequest, country); + final Geo geo = getIfNotNull(device, Device::getGeo); + final String countryInRequest = getIfNotNull(geo, Geo::getCountry); + final String country = getIfNotNull(privacyContext.getTcfContext().getGeoInfo(), GeoInfo::getCountry); + final boolean shouldUpdateCountry = country != null && !Objects.equals(countryInRequest, country); - if (shouldUpdateIp || shouldUpdateCountry) { + if (shouldUpdateIpV4 || shouldUpdateIpV6 || shouldUpdateCountry) { final Device.DeviceBuilder deviceBuilder = device != null ? device.toBuilder() : Device.builder(); - if (shouldUpdateIp) { - deviceBuilder.ip(ipAddress); + if (shouldUpdateIpV4) { + deviceBuilder.ip(ipV4); + } + + if (shouldUpdateIpV6) { + deviceBuilder.ipv6(ipV6); } if (shouldUpdateCountry) { 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 26a06ec67d3..2c965862c1b 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -210,7 +210,8 @@ Ortb2RequestFactory openRtb2RequestFactory( TimeoutResolver timeoutResolver, TimeoutFactory timeoutFactory, StoredRequestProcessor storedRequestProcessor, - ApplicationSettings applicationSettings) { + ApplicationSettings applicationSettings, + IpAddressHelper ipAddressHelper) { final List blacklistedAccounts = splitToList(blacklistedAccountsString); @@ -222,7 +223,8 @@ Ortb2RequestFactory openRtb2RequestFactory( timeoutResolver, timeoutFactory, storedRequestProcessor, - applicationSettings); + applicationSettings, + ipAddressHelper); } @Bean diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java index 617eb9aa723..62d51923380 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java @@ -17,9 +17,11 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; +import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.StoredRequestProcessor; import org.prebid.server.auction.TimeoutResolver; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.IpAddress; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.cookie.proto.Uids; @@ -47,13 +49,18 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.UnaryOperator; import static java.util.Collections.emptyMap; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.never; @@ -79,6 +86,8 @@ public class Ortb2RequestFactoryTest extends VertxTest { private StoredRequestProcessor storedRequestProcessor; @Mock private ApplicationSettings applicationSettings; + @Mock + private IpAddressHelper ipAddressHelper; private Ortb2RequestFactory target; @@ -109,7 +118,8 @@ public void setUp() { timeoutResolver, timeoutFactory, storedRequestProcessor, - applicationSettings); + applicationSettings, + ipAddressHelper); } @Test @@ -123,7 +133,8 @@ public void shouldReturnFailedFutureIfAccountIsEnforcedAndIdIsNotProvided() { timeoutResolver, timeoutFactory, storedRequestProcessor, - applicationSettings); + applicationSettings, + ipAddressHelper); // when final Future result = target.fetchAccountAndCreateAuctionContext(routingContext, defaultBidRequest, null, @@ -149,17 +160,17 @@ public void shouldReturnFailedFutureIfAccountIsEnforcedAndFailedGetAccountById() timeoutResolver, timeoutFactory, storedRequestProcessor, - applicationSettings); + applicationSettings, + ipAddressHelper); given(applicationSettings.getAccountById(any(), any())) .willReturn(Future.failedFuture(new PreBidException("Not found"))); final String accountId = "absentId"; - final BidRequest bidRequest = BidRequest.builder() + final BidRequest bidRequest = givenBidRequest(builder -> builder .app(App.builder() .publisher(Publisher.builder().id(accountId).build()) - .build()) - .build(); + .build())); // when final Future result = target.fetchAccountAndCreateAuctionContext(routingContext, bidRequest, null, @@ -178,11 +189,10 @@ public void shouldReturnFailedFutureIfAccountIsEnforcedAndFailedGetAccountById() public void shouldReturnFailedFutureIfAccountIsInactive() { // given final String accountId = "accountId"; - final BidRequest bidRequest = BidRequest.builder() + final BidRequest bidRequest = givenBidRequest(builder -> builder .app(App.builder() .publisher(Publisher.builder().id(accountId).build()) - .build()) - .build(); + .build())); given(applicationSettings.getAccountById(any(), any())) .willReturn(Future.succeededFuture(Account.builder() @@ -204,10 +214,10 @@ public void shouldReturnFailedFutureIfAccountIsInactive() { @Test public void shouldReturnFailedFutureWhenAccountIdIsBlacklisted() { // given - final BidRequest bidRequest = BidRequest.builder() + final BidRequest bidRequest = givenBidRequest(builder -> builder .site(Site.builder() - .publisher(Publisher.builder().id("321").build()).build()) - .build(); + .publisher(Publisher.builder().id("321").build()) + .build())); // when final Future result = target.fetchAccountAndCreateAuctionContext(routingContext, bidRequest, @@ -226,13 +236,12 @@ public void shouldReturnFailedFutureWhenAccountIdIsBlacklisted() { public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherExt() { // given final String parentAccount = "parentAccount"; - final BidRequest bidRequest = BidRequest.builder() + final BidRequest bidRequest = givenBidRequest(builder -> builder .site(Site.builder() .publisher(Publisher.builder().id("accountId") .ext(ExtPublisher.of(ExtPublisherPrebid.of(parentAccount))) .build()) - .build()) - .build(); + .build())); final Account account = Account.builder().id(parentAccount).build(); given(applicationSettings.getAccountById(any(), any())) @@ -252,11 +261,10 @@ public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherExt() { public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherIdWhenExtIsNull() { // given final String accountId = "accountId"; - final BidRequest bidRequest = BidRequest.builder() + final BidRequest bidRequest = givenBidRequest(builder -> builder .site(Site.builder() .publisher(Publisher.builder().id(accountId).ext(null).build()) - .build()) - .build(); + .build())); final Account account = Account.builder().id(accountId).build(); given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account)); @@ -275,13 +283,12 @@ public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherIdWhenExtIs public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherIdWhenExtPublisherPrebidIsNull() { // given final String accountId = "accountId"; - final BidRequest bidRequest = BidRequest.builder() + final BidRequest bidRequest = givenBidRequest(builder -> builder .site(Site.builder() .publisher(Publisher.builder().id(accountId) .ext(ExtPublisher.empty()) .build()) - .build()) - .build(); + .build())); final Account account = Account.builder().id(accountId).build(); given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account)); @@ -299,13 +306,12 @@ public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherIdWhenExtPu @Test public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherIdWhenExtParentIsEmpty() { // given - final BidRequest bidRequest = BidRequest.builder() + final BidRequest bidRequest = givenBidRequest(builder -> builder .site(Site.builder() .publisher(Publisher.builder().id("accountId") .ext(ExtPublisher.of(ExtPublisherPrebid.of(""))) .build()) - .build()) - .build(); + .build())); final Account account = Account.builder().id("accountId").build(); given(applicationSettings.getAccountById(any(), any())) @@ -325,13 +331,12 @@ public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherIdWhenExtPa public void shouldReturnAuctionContextWithEmptyAccountIfNotFound() { // given final String parentAccount = "parentAccount"; - final BidRequest bidRequest = BidRequest.builder() + final BidRequest bidRequest = givenBidRequest(builder -> builder .site(Site.builder() .publisher(Publisher.builder().id("accountId") .ext(ExtPublisher.of(ExtPublisherPrebid.of(parentAccount))) .build()) - .build()) - .build(); + .build())); given(applicationSettings.getAccountById(any(), any())) .willReturn(Future.failedFuture(new PreBidException("not found"))); @@ -350,11 +355,10 @@ public void shouldReturnAuctionContextWithEmptyAccountIfNotFound() { public void shouldReturnAuctionContextWithEmptyAccountIfExceptionOccurred() { // given final String accountId = "accountId"; - final BidRequest bidRequest = BidRequest.builder() + final BidRequest bidRequest = givenBidRequest(builder -> builder .site(Site.builder() .publisher(Publisher.builder().id(accountId).build()) - .build()) - .build(); + .build())); given(applicationSettings.getAccountById(any(), any())) .willReturn(Future.failedFuture(new RuntimeException("error"))); @@ -372,7 +376,7 @@ public void shouldReturnAuctionContextWithEmptyAccountIfExceptionOccurred() { @Test public void shouldReturnAuctionContextWithEmptyAccountIfItIsMissingInRequest() { // given - final BidRequest bidRequest = BidRequest.builder().build(); + final BidRequest bidRequest = givenBidRequest(identity()); given(applicationSettings.getAccountById(any(), any())) .willReturn(Future.failedFuture(new RuntimeException("error"))); @@ -390,14 +394,13 @@ public void shouldReturnAuctionContextWithEmptyAccountIfItIsMissingInRequest() { @Test public void shouldFetchAccountFromStoredIfStoredLookupIsTrueAndAccountIsNotFoundPreviously() { // given - final BidRequest receivedBidRequest = BidRequest.builder().build(); + final BidRequest receivedBidRequest = givenBidRequest(identity()); final String accountId = "accountId"; - final BidRequest mergedBidRequest = BidRequest.builder() + final BidRequest mergedBidRequest = givenBidRequest(builder -> builder .site(Site.builder() .publisher(Publisher.builder().id(accountId).build()) - .build()) - .build(); + .build())); given(storedRequestProcessor.processStoredRequests(any(), any())) .willReturn(Future.succeededFuture(mergedBidRequest)); @@ -420,12 +423,12 @@ public void shouldFetchAccountFromStoredIfStoredLookupIsTrueAndAccountIsNotFound @Test public void shouldFetchAccountFromStoredAndReturnFailedFutureWhenAccountIdIsBlacklisted() { // given - final BidRequest receivedBidRequest = BidRequest.builder().build(); + final BidRequest receivedBidRequest = givenBidRequest(identity()); - final BidRequest mergedBidRequest = BidRequest.builder() + final BidRequest mergedBidRequest = givenBidRequest(builder -> builder .site(Site.builder() - .publisher(Publisher.builder().id("321").build()).build()) - .build(); + .publisher(Publisher.builder().id("321").build()).build())); + given(storedRequestProcessor.processStoredRequests(any(), any())) .willReturn(Future.succeededFuture(mergedBidRequest)); @@ -455,9 +458,10 @@ public void shouldFetchAccountFromStoredAndReturnFailedFutureIfValidIsEnforcedAn timeoutResolver, timeoutFactory, storedRequestProcessor, - applicationSettings); + applicationSettings, + ipAddressHelper); - final BidRequest receivedBidRequest = BidRequest.builder().build(); + final BidRequest receivedBidRequest = givenBidRequest(identity()); given(storedRequestProcessor.processStoredRequests(any(), any())) .willReturn(Future.failedFuture(new RuntimeException("error"))); @@ -476,7 +480,7 @@ public void shouldFetchAccountFromStoredAndReturnFailedFutureIfValidIsEnforcedAn @Test public void shouldFetchAccountFromStoredAndReturnEmptyAccountIfStoredLookupIsFailed() { // given - final BidRequest receivedBidRequest = BidRequest.builder().build(); + final BidRequest receivedBidRequest = givenBidRequest(identity()); given(storedRequestProcessor.processStoredRequests(any(), any())) .willReturn(Future.failedFuture(new RuntimeException("error"))); @@ -497,12 +501,11 @@ public void shouldReturnExpectedAuctionContext() { // given final String accountId = "123"; final long tmax = 1000L; - final BidRequest bidRequest = BidRequest.builder() + final BidRequest bidRequest = givenBidRequest(builder -> builder .app(App.builder() .publisher(Publisher.builder().id(accountId).build()) .build()) - .tmax(tmax) - .build(); + .tmax(tmax)); final Account account = Account.builder() .id(accountId) @@ -554,7 +557,7 @@ public void validateRequestShouldThrowInvalidRequestExceptionIfRequestIsInvalid( // given given(requestValidator.validate(any())).willReturn(ValidationResult.error("error")); - final BidRequest bidRequest = BidRequest.builder().build(); + final BidRequest bidRequest = givenBidRequest(identity()); // when and then assertThatExceptionOfType(InvalidRequestException.class) @@ -569,7 +572,7 @@ public void validateRequestShouldReturnSameBidRequest() { // given given(requestValidator.validate(any())).willReturn(ValidationResult.success()); - final BidRequest bidRequest = BidRequest.builder().build(); + final BidRequest bidRequest = givenBidRequest(identity()); // when final BidRequest result = target.validateRequest(bidRequest); @@ -584,13 +587,12 @@ public void validateRequestShouldReturnSameBidRequest() { public void enrichBidRequestWithAccountAndPrivacyDataShouldReturnIntegrationFromAccount() { // given final String accountId = "accId"; - final BidRequest bidRequest = BidRequest.builder() + final BidRequest bidRequest = givenBidRequest(builder -> builder .imp(new ArrayList<>()) .site(Site.builder() .publisher(Publisher.builder().id(accountId).build()) .build()) - .ext(ExtRequest.of(ExtRequestPrebid.builder().build())) - .build(); + .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))); final PrivacyContext privacyContext = PrivacyContext.of( Privacy.of("", "", Ccpa.EMPTY, 0), @@ -611,15 +613,15 @@ public void enrichBidRequestWithAccountAndPrivacyDataShouldReturnIntegrationFrom } @Test - public void enrichBidRequestWithAccountAndPrivacyDataShouldAddIpAddressAndCountryFromPrivacy() { + public void enrichBidRequestWithAccountAndPrivacyDataShouldAddCountryFromPrivacy() { // given - final BidRequest bidRequest = BidRequest.builder().build(); + final BidRequest bidRequest = givenBidRequest(identity()); final PrivacyContext privacyContext = PrivacyContext.of( Privacy.of("", "", Ccpa.EMPTY, 0), TcfContext.builder() .geoInfo(GeoInfo.builder().vendor("v").country("ua").build()) .build(), - "ip"); + null); final Account account = Account.empty("id"); @@ -627,13 +629,60 @@ public void enrichBidRequestWithAccountAndPrivacyDataShouldAddIpAddressAndCountr final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(bidRequest, account, privacyContext); // then - final Device expectedDevice = Device.builder() - .ip("ip") - .geo(Geo.builder().country("ua").build()) - .build(); + assertThat(singleton(result)) + .extracting(BidRequest::getDevice) + .extracting(Device::getGeo) + .extracting(Geo::getCountry) + .containsOnly("ua"); + } - assertThat(result) + @Test + public void enrichBidRequestWithAccountAndPrivacyDataShouldAddIpAddressV4FromPrivacy() { + // given + given(ipAddressHelper.toIpAddress(anyString())).willReturn(IpAddress.of("ignored", IpAddress.IP.v4)); + + final BidRequest bidRequest = givenBidRequest(identity()); + final PrivacyContext privacyContext = PrivacyContext.of( + Privacy.of("", "", Ccpa.EMPTY, 0), + TcfContext.builder().build(), + "ipv4"); + + final Account account = Account.empty("id"); + + // when + final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(bidRequest, account, privacyContext); + + // then + assertThat(singleton(result)) .extracting(BidRequest::getDevice) - .containsOnly(expectedDevice); + .extracting(Device::getIp, Device::getIpv6) + .containsOnly(tuple("ipv4", null)); + } + + @Test + public void enrichBidRequestWithAccountAndPrivacyDataShouldAddIpAddressV6FromPrivacy() { + // given + given(ipAddressHelper.toIpAddress(anyString())).willReturn(IpAddress.of("ignored", IpAddress.IP.v6)); + + final BidRequest bidRequest = givenBidRequest(identity()); + final PrivacyContext privacyContext = PrivacyContext.of( + Privacy.of("", "", Ccpa.EMPTY, 0), + TcfContext.builder().build(), + "ipv6"); + + final Account account = Account.empty("id"); + + // when + final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(bidRequest, account, privacyContext); + + // then + assertThat(singleton(result)) + .extracting(BidRequest::getDevice) + .extracting(Device::getIp, Device::getIpv6) + .containsOnly(tuple(null, "ipv6")); + } + + private static BidRequest givenBidRequest(UnaryOperator requestCustomizer) { + return requestCustomizer.apply(BidRequest.builder()).build(); } }