diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 752aa5e80d5..ba915e25991 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -81,6 +81,7 @@ import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.UpdateResult; 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.ExtBidderConfigOrtb; @@ -167,6 +168,7 @@ public class ExchangeService { private final SupplyChainResolver supplyChainResolver; private final DebugResolver debugResolver; private final MediaTypeProcessor mediaTypeProcessor; + private final UidUpdater uidUpdater; private final TimeoutResolver timeoutResolver; private final TimeoutFactory timeoutFactory; private final BidRequestOrtbVersionConversionManager ortbVersionConversionManager; @@ -195,6 +197,7 @@ public ExchangeService(int timeoutAdjustmentFactor, SupplyChainResolver supplyChainResolver, DebugResolver debugResolver, MediaTypeProcessor mediaTypeProcessor, + UidUpdater uidUpdater, TimeoutResolver timeoutResolver, TimeoutFactory timeoutFactory, BidRequestOrtbVersionConversionManager ortbVersionConversionManager, @@ -226,6 +229,7 @@ public ExchangeService(int timeoutAdjustmentFactor, this.supplyChainResolver = Objects.requireNonNull(supplyChainResolver); this.debugResolver = Objects.requireNonNull(debugResolver); this.mediaTypeProcessor = Objects.requireNonNull(mediaTypeProcessor); + this.uidUpdater = Objects.requireNonNull(uidUpdater); this.timeoutResolver = Objects.requireNonNull(timeoutResolver); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.ortbVersionConversionManager = Objects.requireNonNull(ortbVersionConversionManager); @@ -547,17 +551,13 @@ private Future> makeAuctionParticipation( Map bidderToMultiBid) { final BidRequest bidRequest = context.getBidRequest(); - final User user = bidRequest.getUser(); - final ExtUser extUser = user != null ? user.getExt() : null; - final Map uidsBody = uidsFromBody(extUser); final ExtRequest requestExt = bidRequest.getExt(); final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); final Map biddersToConfigs = getBiddersToConfigs(prebid); final Map> eidPermissions = getEidPermissions(prebid); final Map bidderToUser = - prepareUsers(bidders, context, aliases, bidRequest, extUser, uidsBody, biddersToConfigs, - eidPermissions); + prepareUsers(bidders, context, aliases, biddersToConfigs, eidPermissions); return privacyEnforcementService .mask(context, bidderToUser, bidders, aliases) @@ -626,12 +626,10 @@ private static List firstPartyDataBidders(ExtRequest requestExt) { private Map prepareUsers(List bidders, AuctionContext context, BidderAliases aliases, - BidRequest bidRequest, - ExtUser extUser, - Map uidsBody, Map biddersToConfigs, Map> eidPermissions) { + final BidRequest bidRequest = context.getBidRequest(); final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); final Map bidderToUser = new HashMap<>(); @@ -640,8 +638,8 @@ private Map prepareUsers(List bidders, biddersToConfigs.get(ALL_BIDDERS_CONFIG)); final boolean useFirstPartyData = firstPartyDataBidders == null || firstPartyDataBidders.contains(bidder); - final User preparedUser = prepareUser(bidRequest.getUser(), extUser, bidder, aliases, uidsBody, - context.getUidsCookie(), useFirstPartyData, fpdConfig, eidPermissions); + final User preparedUser = prepareUser( + bidder, context, aliases, useFirstPartyData, fpdConfig, eidPermissions); bidderToUser.put(bidder, preparedUser); } return bidderToUser; @@ -654,17 +652,17 @@ private Map prepareUsers(List bidders, * Also, removes user.ext.prebid (if present), user.ext.data and user.data (in case bidder does not use first * party data). */ - private User prepareUser(User user, - ExtUser extUser, - String bidder, + private User prepareUser(String bidder, + AuctionContext context, BidderAliases aliases, - Map uidsBody, - UidsCookie uidsCookie, boolean useFirstPartyData, ExtBidderConfigOrtb fpdConfig, Map> eidPermissions) { - final String updatedBuyerUid = updateUserBuyerUid(user, bidder, aliases, uidsBody, uidsCookie); + final User user = context.getBidRequest().getUser(); + final ExtUser extUser = user != null ? user.getExt() : null; + + final UpdateResult buyerUidUpdateResult = uidUpdater.updateUid(bidder, context, aliases); final List userEids = extractUserEids(user); final List allowedUserEids = resolveAllowedEids(userEids, bidder, eidPermissions); final boolean shouldUpdateUserEids = allowedUserEids.size() != CollectionUtils.emptyIfNull(userEids).size(); @@ -674,11 +672,9 @@ private User prepareUser(User user, final boolean shouldCleanData = user != null && user.getData() != null && !useFirstPartyData; User maskedUser = user; - if (updatedBuyerUid != null || shouldUpdateUserEids || shouldUpdateUserExt || shouldCleanData) { + if (buyerUidUpdateResult.isUpdated() || shouldUpdateUserEids || shouldUpdateUserExt || shouldCleanData) { final User.UserBuilder userBuilder = user == null ? User.builder() : user.toBuilder(); - if (updatedBuyerUid != null) { - userBuilder.buyeruid(updatedBuyerUid); - } + userBuilder.buyeruid(buyerUidUpdateResult.getValue()); if (shouldUpdateUserEids) { userBuilder.eids(nullIfEmpty(allowedUserEids)); @@ -704,19 +700,6 @@ private User prepareUser(User user, : maskedUser; } - /** - * Returns updated buyerUid or null if it doesn't need to be updated. - */ - private String updateUserBuyerUid(User user, String bidder, BidderAliases aliases, - Map uidsBody, UidsCookie uidsCookie) { - final String buyerUidFromBodyOrCookie = extractUid(uidsBody, uidsCookie, aliases.resolveBidder(bidder)); - final String buyerUidFromUser = user != null ? user.getBuyeruid() : null; - - return StringUtils.isBlank(buyerUidFromUser) && StringUtils.isNotBlank(buyerUidFromBodyOrCookie) - ? buyerUidFromBodyOrCookie - : null; - } - private List extractUserEids(User user) { return user != null ? user.getEids() : null; } @@ -743,24 +726,6 @@ private boolean isUserEidAllowed(String source, Map> eidPer || allowedBidders.contains(bidder); } - /** - * Extracts UID from uids from body or {@link UidsCookie}. - */ - private String extractUid(Map uidsBody, UidsCookie uidsCookie, String bidder) { - final String uid = uidsBody.get(bidder); - return StringUtils.isNotBlank(uid) ? uid : uidsCookie.uidFrom(resolveCookieFamilyName(bidder)); - } - - /** - * Extract cookie family name from bidder's {@link Usersyncer} if it is enabled. If not - return null. - */ - private String resolveCookieFamilyName(String bidder) { - return bidderCatalog.usersyncerByName(bidder) - .filter(usersyncer -> bidderCatalog.isActive(bidder)) - .map(Usersyncer::getCookieFamilyName) - .orElse(null); - } - /** * Returns shuffled list of {@link AuctionParticipation} with {@link BidRequest}. */ diff --git a/src/main/java/org/prebid/server/auction/UidUpdater.java b/src/main/java/org/prebid/server/auction/UidUpdater.java new file mode 100644 index 00000000000..1b482f1fd68 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/UidUpdater.java @@ -0,0 +1,71 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.User; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.cookie.UidsCookieService; +import org.prebid.server.model.UpdateResult; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +public class UidUpdater { + + private final String hostCookieFamily; + private final BidderCatalog bidderCatalog; + private final UidsCookieService uidsCookieService; + + public UidUpdater(String hostCookieFamily, BidderCatalog bidderCatalog, UidsCookieService uidsCookieService) { + this.hostCookieFamily = hostCookieFamily; + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + this.uidsCookieService = Objects.requireNonNull(uidsCookieService); + } + + public UpdateResult updateUid(String bidder, AuctionContext auctionContext, BidderAliases aliases) { + final User user = auctionContext.getBidRequest().getUser(); + + final String uidFromUser = user != null ? user.getBuyeruid() : null; + if (StringUtils.isNotBlank(uidFromUser)) { + return UpdateResult.unaltered(uidFromUser); + } + + final String resolvedBidder = aliases.resolveBidder(bidder); + + final String uidFromExt = uidFromExtUser(user, resolvedBidder); + final String uidFromUidsCookie = uidFromUidsCookie(auctionContext.getUidsCookie(), resolvedBidder); + final String uidFromHostCookie = uidFromHostCookie(auctionContext, resolvedBidder); + + return Stream.of(uidFromExt, uidFromUidsCookie, uidFromHostCookie) + .filter(StringUtils::isNotBlank) + .map(UpdateResult::updated) + .findFirst() + .orElse(UpdateResult.unaltered(null)); + } + + private static String uidFromExtUser(User user, String bidder) { + return Optional.ofNullable(user) + .map(User::getExt) + .map(ExtUser::getPrebid) + .map(ExtUserPrebid::getBuyeruids) + .map(uids -> uids.get(bidder)) + .orElse(null); + } + + private String uidFromUidsCookie(UidsCookie uidsCookie, String bidder) { + return bidderCatalog.cookieFamilyName(bidder) + .map(uidsCookie::uidFrom) + .orElse(null); + } + + private String uidFromHostCookie(AuctionContext auctionContext, String bidder) { + return bidderCatalog.cookieFamilyName(bidder) + .filter(cookieFamily -> StringUtils.equals(cookieFamily, hostCookieFamily)) + .map(cookieFamily -> uidsCookieService.parseHostCookie(auctionContext.getHttpRequest())) + .orElse(null); + } +} diff --git a/src/main/java/org/prebid/server/cookie/UidsCookieService.java b/src/main/java/org/prebid/server/cookie/UidsCookieService.java index b5c20413833..8a3d03257d4 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookieService.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookieService.java @@ -171,6 +171,13 @@ public String parseHostCookie(Map cookies) { return hostCookieName != null ? cookies.get(hostCookieName) : null; } + /** + * Lookups host cookie value from request context by configured host cookie name. + */ + public String parseHostCookie(HttpRequestContext httpRequest) { + return parseHostCookie(HttpUtil.cookiesAsMap(httpRequest)); + } + /** * Checks incoming request if it matches pre-configured opted-out cookie name, value and de-activates * UIDs cookie sync. diff --git a/src/main/java/org/prebid/server/model/UpdateResult.java b/src/main/java/org/prebid/server/model/UpdateResult.java new file mode 100644 index 00000000000..3c28be53ebe --- /dev/null +++ b/src/main/java/org/prebid/server/model/UpdateResult.java @@ -0,0 +1,19 @@ +package org.prebid.server.model; + +import lombok.Value; + +@Value +public class UpdateResult { + + boolean updated; + + T value; + + public static UpdateResult unaltered(T value) { + return new UpdateResult<>(false, value); + } + + public static UpdateResult updated(T value) { + return new UpdateResult<>(true, value); + } +} 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 57c9e380080..19af1504d6a 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -25,6 +25,7 @@ import org.prebid.server.auction.StoredResponseProcessor; import org.prebid.server.auction.SupplyChainResolver; import org.prebid.server.auction.TimeoutResolver; +import org.prebid.server.auction.UidUpdater; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.VideoStoredRequestProcessor; import org.prebid.server.auction.WinningBidComparatorFactory; @@ -581,6 +582,15 @@ UidsCookieService uidsCookieService( mapper); } + @Bean + UidUpdater uidUpdater( + @Value("${host-cookie.family:#{null}}") String hostCookieFamily, + BidderCatalog bidderCatalog, + UidsCookieService uidsCookieService) { + + return new UidUpdater(hostCookieFamily, bidderCatalog, uidsCookieService); + } + @Bean CoopSyncProvider coopSyncProvider( BidderCatalog bidderCatalog, @@ -726,6 +736,7 @@ ExchangeService exchangeService( SupplyChainResolver supplyChainResolver, DebugResolver debugResolver, MediaTypeProcessor mediaTypeProcessor, + UidUpdater uidUpdater, TimeoutResolver timeoutResolver, TimeoutFactory timeoutFactory, BidRequestOrtbVersionConversionManager bidRequestOrtbVersionConversionManager, @@ -755,6 +766,7 @@ ExchangeService exchangeService( supplyChainResolver, debugResolver, mediaTypeProcessor, + uidUpdater, timeoutResolver, timeoutFactory, bidRequestOrtbVersionConversionManager, diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy index 37390850b9f..4158ffa9c25 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy @@ -10,4 +10,5 @@ class UserExt { List fcapids UserTime time UserExtData data + UserExtPrebid prebid } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExtPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExtPrebid.groovy new file mode 100644 index 00000000000..038ed9ca422 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExtPrebid.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName + +@ToString(includeNames = true, ignoreNulls = true) +class UserExtPrebid { + + Map buyeruids +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy index 0945caf7e2f..e92c4607ae2 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy @@ -1,16 +1,40 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.UidsCookie import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.model.request.auction.User +import org.prebid.server.functional.model.request.auction.UserExt +import org.prebid.server.functional.model.request.auction.UserExtPrebid +import org.prebid.server.functional.model.response.cookiesync.UserSyncInfo import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.HttpUtil import org.prebid.server.functional.util.PBSUtils +import spock.lang.Shared import static org.prebid.server.functional.model.AccountStatus.INACTIVE +import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.response.cookiesync.UserSyncInfo.Type.REDIRECT +import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer import static org.prebid.server.functional.util.SystemProperties.PBS_VERSION class AuctionSpec extends BaseSpec { + private static final String USER_SYNC_URL = "$networkServiceContainer.rootUri/generic-usersync" + private static final boolean CORS_SUPPORT = false + private static final UserSyncInfo.Type USER_SYNC_TYPE = REDIRECT + private static final int DEFAULT_TIMEOUT = getRandomTimeout() + private static final Map PBS_CONFIG = ["auction.max-timeout-ms" : MAX_TIMEOUT as String, + "auction.default-timeout-ms": DEFAULT_TIMEOUT as String] + private static final Map GENERIC_CONFIG = [ + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] + @Shared + PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG) + def "PBS should return version in response header for auction request for #description"() { when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequestRaw(bidRequest) @@ -106,4 +130,166 @@ class AuctionSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.ext.prebid.passThrough == passThrough } + + def "PBS should populate bidder request buyeruid from buyeruids when buyeruids with appropriate bidder present in request"() { + given: "Bid request with buyeruids" + def buyeruid = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(prebid: new UserExtPrebid(buyeruids: [(GENERIC): buyeruid]))) + } + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain buyeruid from the user.ext.prebid.buyeruids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.user?.buyeruid == buyeruid + } + + def "PBS shouldn't populate bidder request buyeruid from buyeruids when buyeruids without appropriate bidder present in request"() { + given: "Bid request with buyeruids" + def buyeruid = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(prebid: new UserExtPrebid(buyeruids: [(APPNEXUS): buyeruid]))) + } + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain buyeruid from the user.ext.prebid.buyeruids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user + } + + def "PBS should populate buyeruid from uids cookie when buyeruids with appropriate bidder but without value present in request"() { + given: "PBS config" + def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + + ["adapters.${GENERIC.value}.usersync.${REDIRECT.value}.url" : USER_SYNC_URL, + "adapters.${GENERIC.value}.usersync.${REDIRECT.value}.support-cors": "false"]) + + and: "Bid request with buyeruids" + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(prebid: new UserExtPrebid(buyeruids: [(GENERIC): ""]))) + } + + and: "Cookies headers" + def uidsCookie = UidsCookie.defaultUidsCookie + def cookieHeader = HttpUtil.getCookieHeader(uidsCookie) + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest, cookieHeader) + + then: "Bidder request should contain buyeruid from the uids cookie" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.user?.buyeruid == uidsCookie.tempUIDs[GENERIC].uid + } + + def "PBS shouldn't populate buyeruid from uids cookie when buyeruids with appropriate bidder but without value present in request"() { + given: "PBS config" + def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + + ["adapters.${GENERIC.value}.usersync.${REDIRECT.value}.url" : USER_SYNC_URL, + "adapters.${GENERIC.value}.usersync.${REDIRECT.value}.support-cors": "false"]) + + and: "Bid request with buyeruids" + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(prebid: new UserExtPrebid(buyeruids: [(GENERIC): ""]))) + } + + and: "Empty cookies headers" + def cookieHeader = HttpUtil.getCookieHeader(null) + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest, cookieHeader) + + then: "Bidder request shouldn't contain buyeruid from the uids cookie" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user + } + + def "PBS should take precedence buyeruids whenever present valid uid cookie"() { + given: "Bid request with buyeruids" + def buyeruid = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(prebid: new UserExtPrebid(buyeruids: [(GENERIC): buyeruid]))) + } + + and: "Cookies headers" + def uidsCookie = UidsCookie.defaultUidsCookie + def cookieHeader = HttpUtil.getCookieHeader(uidsCookie) + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest, cookieHeader) + + then: "Bidder request should contain buyeruid from the buyeruids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.user?.buyeruid == buyeruid + } + + def "PBS should populate buyeruid from host cookie name config when host cookie family matched with requested bidder"() { + given: "PBS config" + def cookieName = PBSUtils.randomString + def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GENERIC_CONFIG + + ["host-cookie.family" : GENERIC.value, + "host-cookie.cookie-name" : cookieName, + "adapters.generic.usersync.cookie-family-name": GENERIC.value]) + + and: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Host cookie" + def hostCookieUid = UUID.randomUUID().toString() + def cookies = HttpUtil.getCookieHeader(cookieName, hostCookieUid) + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest, cookies) + + then: "Bidder request should contain buyeruid from cookieName" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.user?.buyeruid == hostCookieUid + } + + def "PBS shouldn't populate buyeruid from cookie name config when host cookie family not matched with requested cookie-family-name"() { + given: "PBS config" + def cookieName = PBSUtils.randomString + def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GENERIC_CONFIG + + ["host-cookie.family" : APPNEXUS.value, + "host-cookie.cookie-name" : cookieName, + "adapters.generic.usersync.cookie-family-name": GENERIC.value]) + + and: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Host cookie" + def hostCookieUid = UUID.randomUUID().toString() + def cookies = HttpUtil.getCookieHeader(cookieName, hostCookieUid) + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest, cookies) + + then: "Bidder request shouldn't contain buyeruid from cookieName" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user + } + + def "PBS shouldn't populate buyeruid from cookie when cookie-name in cookie and config are diferent"() { + given: "PBS config" + def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GENERIC_CONFIG + + ["host-cookie.family" : GENERIC.value, + "host-cookie.cookie-name" : PBSUtils.randomString, + "adapters.generic.usersync.cookie-family-name": GENERIC.value]) + + and: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Host cookie" + def hostCookieUid = UUID.randomUUID().toString() + def cookies = HttpUtil.getCookieHeader(PBSUtils.randomString, hostCookieUid) + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest, cookies) + + then: "Bidder request shouldn't contain buyeruid from cookieName" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user + } } diff --git a/src/test/groovy/org/prebid/server/functional/util/HttpUtil.groovy b/src/test/groovy/org/prebid/server/functional/util/HttpUtil.groovy index da432596c8f..843c230fc4c 100644 --- a/src/test/groovy/org/prebid/server/functional/util/HttpUtil.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/HttpUtil.groovy @@ -28,6 +28,10 @@ class HttpUtil implements ObjectMapperWrapper { [(COOKIE_HEADER): makeUidsCookieHeaderValue(encode(uidsCookie))] } + static HashMap getCookieHeader(String value1, String value2) { + [(COOKIE_HEADER): "$value1=$value2"] + } + private static String decodeUrl(String url) { URLDecoder.decode(url, UTF_8) } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 86354e25f78..59858743c1e 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -100,6 +100,7 @@ import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.model.Endpoint; import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.model.UpdateResult; 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.ExtBidderConfig; @@ -229,6 +230,9 @@ public class ExchangeServiceTest extends VertxTest { @Mock private MediaTypeProcessor mediaTypeProcessor; + @Mock + private UidUpdater uidUpdater; + @Mock private TimeoutResolver timeoutResolver; @@ -353,6 +357,13 @@ public void setUp() { given(currencyService.convertCurrency(any(), any(), any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); + given(uidUpdater.updateUid(any(), any(), any())) + .willAnswer(inv -> Optional.ofNullable((AuctionContext) inv.getArgument(1)) + .map(AuctionContext::getBidRequest) + .map(BidRequest::getUser) + .map(user -> UpdateResult.updated(null)) + .orElse(UpdateResult.unaltered(null))); + given(storedResponseProcessor.getStoredResponseResult(any(), any())) .willAnswer(inv -> Future.succeededFuture(StoredResponseResult.of(inv.getArgument(0), emptyList(), emptyMap()))); @@ -400,6 +411,7 @@ public void setUp() { supplyChainResolver, debugResolver, new NoOpMediaTypeProcessor(), + uidUpdater, timeoutResolver, timeoutFactory, ortbVersionConversionManager, @@ -433,6 +445,7 @@ public void creationShouldFailOnNegativeTimeoutAdjustmentFactor() { supplyChainResolver, debugResolver, new NoOpMediaTypeProcessor(), + uidUpdater, timeoutResolver, timeoutFactory, ortbVersionConversionManager, @@ -715,6 +728,7 @@ public void shouldExtractRequestsWithoutFilteredPgDealsOnlyBidders() { supplyChainResolver, debugResolver, new NoOpMediaTypeProcessor(), + uidUpdater, timeoutResolver, timeoutFactory, ortbVersionConversionManager, @@ -1705,39 +1719,16 @@ public void shouldReturnFailedFutureWhenStoredResponseProcessorMergeBidderRespon assertThat(result.cause()).isInstanceOf(PreBidException.class).hasMessage("Error"); } - @Test - public void shouldNotModifyUserFromRequestIfNoBuyeridInCookie() { - // given - givenBidder(givenEmptySeatBid()); - - // this is not required but stated for clarity's sake. The case when bidder is disabled. - given(bidderCatalog.isActive(anyString())).willReturn(false); - given(uidsCookie.uidFrom(any())).willReturn(null); - - final User user = User.builder().id("userId").build(); - final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), - builder -> builder.user(user)); - - // when - exchangeService.holdAuction(givenRequestContext(bidRequest)); - - // then - verify(uidsCookie).uidFrom(isNull()); - - final BidRequest capturedBidRequest = captureBidRequest(); - assertThat(capturedBidRequest.getUser()).isSameAs(user); - } - @Test public void shouldHonorBuyeridFromRequestAndClearBuyerIdsFromUserExtPrebidIfContains() { // given givenBidder(givenEmptySeatBid()); - given(uidsCookie.uidFrom(anyString())).willReturn("buyeridFromCookie"); + given(uidUpdater.updateUid(any(), any(), any())).willReturn(UpdateResult.updated("buyerid")); final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), builder -> builder.user(User.builder() - .buyeruid("buyeridFromRequest") + .buyeruid("buyerid") .ext(ExtUser.builder() .prebid(ExtUserPrebid.of(singletonMap("someBidder", "uidval"))) .build()) @@ -1749,7 +1740,7 @@ public void shouldHonorBuyeridFromRequestAndClearBuyerIdsFromUserExtPrebidIfCont // then final User capturedBidRequestUser = captureBidRequest().getUser(); assertThat(capturedBidRequestUser).isEqualTo(User.builder() - .buyeruid("buyeridFromRequest") + .buyeruid("buyerid") .build()); } @@ -1898,34 +1889,6 @@ public void shouldPassImpExtSkadnToEachImpression() { .containsOnly(new TextNode("skadnValue")); } - @Test - public void shouldSetUserBuyerIdsFromUserExtPrebidAndClearPrebidBuyerIdsAfterwards() { - // given - givenBidder(givenEmptySeatBid()); - - given(uidsCookie.uidFrom(anyString())).willReturn("buyeridFromCookie"); - - final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), - builder -> builder - .user(User.builder() - .ext(ExtUser.builder() - .prebid(ExtUserPrebid.of(singletonMap("someBidder", "uidval"))) - .build()) - .build()) - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(singletonList("someBidder"), null)) - .build()))); - - // when - exchangeService.holdAuction(givenRequestContext(bidRequest)); - - // then - final User capturedBidRequestUser = captureBidRequest().getUser(); - assertThat(capturedBidRequestUser).isEqualTo(User.builder() - .buyeruid("uidval") - .build()); - } - @Test public void shouldCleanRequestExtPrebidData() { // given @@ -2658,7 +2621,8 @@ public void shouldUseConcreteOverGeneralUserWithExtPrebidBidderConfig() { public void shouldAddBuyeridToUserFromRequest() { // given givenBidder(givenEmptySeatBid()); - given(uidsCookie.uidFrom(eq("cookieFamily"))).willReturn("buyerid"); + given(uidUpdater.updateUid(any(), any(), any())) + .willReturn(UpdateResult.updated("buyerid")); final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), builder -> builder.user(User.builder().id("userId").build())); @@ -2672,11 +2636,12 @@ public void shouldAddBuyeridToUserFromRequest() { } @Test - public void shouldCreateUserIfMissingInRequestAndBuyeridPresentInCookie() { + public void shouldCreateUserIfMissingInRequestAndBuyeridPresentForBidder() { // given givenBidder(givenEmptySeatBid()); - given(uidsCookie.uidFrom(eq("cookieFamily"))).willReturn("buyerid"); + given(uidUpdater.updateUid(eq("someBidder"), any(), any())) + .willReturn(UpdateResult.updated("buyerid")); final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1))); @@ -4427,6 +4392,7 @@ public void shouldResponseWithEmptySeatBidIfBidderNotSupportProvidedMediaTypes() supplyChainResolver, debugResolver, mediaTypeProcessor, + uidUpdater, timeoutResolver, timeoutFactory, ortbVersionConversionManager, diff --git a/src/test/java/org/prebid/server/auction/UidUpdaterTest.java b/src/test/java/org/prebid/server/auction/UidUpdaterTest.java new file mode 100644 index 00000000000..43ea72518a0 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/UidUpdaterTest.java @@ -0,0 +1,193 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.User; +import io.vertx.core.http.Cookie; +import io.vertx.core.http.CookieSameSite; +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.model.AuctionContext; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.cookie.UidsCookieService; +import org.prebid.server.cookie.model.UidWithExpiry; +import org.prebid.server.cookie.proto.Uids; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.model.UpdateResult; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; + +import java.util.Base64; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; + +public class UidUpdaterTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + private static final String HOST_COOKIE_FAMILY = "host-cookie-family"; + private static final String HOST_COOKIE_NAME = "host-cookie-name"; + private static final String HOST_COOKIE_DOMAIN = "host-cookie-domain"; + + private UidUpdater uidUpdater; + + @Mock + BidderCatalog bidderCatalog; + + @Mock + UidsCookieService uidsCookieService; + + @Mock + BidderAliases bidderAliases; + + @Before + public void setUp() { + uidUpdater = new UidUpdater(HOST_COOKIE_FAMILY, bidderCatalog, uidsCookieService); + + given(bidderAliases.resolveBidder(any())) + .willAnswer(inv -> inv.getArgument(0)); + given(bidderCatalog.cookieFamilyName(eq("bidder"))) + .willReturn(Optional.of("bidder-cookie-family")); + } + + @Test + public void updateShouldReturnUnalteredUidWhenPresentInUser() { + // given + final User user = User.builder() + .buyeruid("buyeruid-from-user") + .ext(ExtUser.builder() + .prebid(ExtUserPrebid.of(Map.of("bidder", "buyeruid-from-ext"))) + .build()) + .build(); + + final AuctionContext auctionContext = AuctionContext.builder() + .httpRequest(givenHttpRequest("buyeruid-from-host-cookie")) + .bidRequest(BidRequest.builder().user(user).build()) + .uidsCookie(givenUidsCookie(Map.of("bidder-cookie-family", "buyeruid-from-uids-cookie"))) + .build(); + + // when + final UpdateResult result = uidUpdater.updateUid("bidder", auctionContext, bidderAliases); + + // then + assertThat(result).isEqualTo(UpdateResult.unaltered("buyeruid-from-user")); + } + + @Test + public void updateShouldReturnUpdatedUidWhenPresentInUserExtAndAbsentInUser() { + // given + final User user = User.builder() + .ext(ExtUser.builder() + .prebid(ExtUserPrebid.of(Map.of("bidder", "buyeruid-from-ext"))) + .build()) + .build(); + + final AuctionContext auctionContext = AuctionContext.builder() + .httpRequest(givenHttpRequest("buyeruid-from-host-cookie")) + .bidRequest(BidRequest.builder().user(user).build()) + .uidsCookie(givenUidsCookie(Map.of("bidder-cookie-family", "buyeruid-from-uids-cookie"))) + .build(); + + // when + final UpdateResult result = uidUpdater.updateUid("bidder", auctionContext, bidderAliases); + + // then + assertThat(result).isEqualTo(UpdateResult.updated("buyeruid-from-ext")); + } + + @Test + public void updateShouldReturnUpdatedUidWhenPresentInUidsCookieAndAbsentInUserExtAndUser() { + // given + final AuctionContext auctionContext = AuctionContext.builder() + .httpRequest(givenHttpRequest("buyeruid-from-host-cookie")) + .bidRequest(BidRequest.builder().user(User.builder().build()).build()) + .uidsCookie(givenUidsCookie(Map.of("bidder-cookie-family", "buyeruid-from-uids-cookie"))) + .build(); + + // when + final UpdateResult result = uidUpdater.updateUid("bidder", auctionContext, bidderAliases); + + // then + assertThat(result).isEqualTo(UpdateResult.updated("buyeruid-from-uids-cookie")); + } + + @Test + public void updateShouldReturnUpdatedUidWhenPresentInHostCookieAndAbsentInUserExtAndUserAndUidsCookie() { + // given + given(bidderCatalog.cookieFamilyName("bidder")).willReturn(Optional.of(HOST_COOKIE_FAMILY)); + + final HttpRequestContext httpRequest = givenHttpRequest("buyeruid-from-host-cookie"); + given(uidsCookieService.parseHostCookie(httpRequest)).willReturn("buyeruid-from-host-cookie"); + + final AuctionContext auctionContext = AuctionContext.builder() + .httpRequest(httpRequest) + .uidsCookie(givenUidsCookie(emptyMap())) + .bidRequest(BidRequest.builder().user(User.builder().build()).build()) + .build(); + + // when + final UpdateResult result = uidUpdater.updateUid("bidder", auctionContext, bidderAliases); + + // then + assertThat(result).isEqualTo(UpdateResult.updated("buyeruid-from-host-cookie")); + } + + @Test + public void updateShouldReturnUnalteredUidWhenAbsentInUserAndUserExtAndUidsCookieAndFamilyIsNotHostCookieFamily() { + // given + final HttpRequestContext httpRequest = givenHttpRequest("buyeruid-from-host-cookie"); + given(uidsCookieService.parseHostCookie(httpRequest)).willReturn("buyeruid-from-host-cookie"); + + final AuctionContext auctionContext = AuctionContext.builder() + .httpRequest(httpRequest) + .uidsCookie(givenUidsCookie(emptyMap())) + .bidRequest(BidRequest.builder().user(User.builder().build()).build()) + .build(); + + // when + final UpdateResult result = uidUpdater.updateUid("bidder", auctionContext, bidderAliases); + + // then + assertThat(result).isEqualTo(UpdateResult.unaltered(null)); + } + + private static HttpRequestContext givenHttpRequest(String hostCookieValue) { + final Cookie hostCookie = givenHostCookie(hostCookieValue); + + return HttpRequestContext.builder() + .headers(CaseInsensitiveMultiMap.builder().add("Cookie", hostCookie.encode()).build()) + .build(); + } + + private static Cookie givenHostCookie(String value) { + return Cookie + .cookie(HOST_COOKIE_NAME, Base64.getUrlEncoder().encodeToString(value.getBytes())) + .setPath("/") + .setSameSite(CookieSameSite.NONE) + .setSecure(true) + .setMaxAge(10000) + .setDomain(HOST_COOKIE_DOMAIN); + } + + private static UidsCookie givenUidsCookie(Map uidValues) { + final Map uids = uidValues.entrySet().stream() + .map(entry -> Map.entry(entry.getKey(), UidWithExpiry.live(entry.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + return new UidsCookie(Uids.builder().uids(uids).build(), jacksonMapper); + } +}