diff --git a/src/main/java/org/prebid/server/cookie/PrioritizedCoopSyncProvider.java b/src/main/java/org/prebid/server/cookie/PrioritizedCoopSyncProvider.java index d3c0dae7150..7ee18499527 100644 --- a/src/main/java/org/prebid/server/cookie/PrioritizedCoopSyncProvider.java +++ b/src/main/java/org/prebid/server/cookie/PrioritizedCoopSyncProvider.java @@ -74,8 +74,4 @@ public boolean isPrioritizedFamily(String cookieFamilyName) { final String bidder = prioritizedCookieFamilyNameToBidderName.get(cookieFamilyName); return prioritizedBidders.contains(bidder); } - - public boolean hasPrioritizedBidders() { - return !prioritizedBidders.isEmpty(); - } } diff --git a/src/main/java/org/prebid/server/cookie/UidsCookie.java b/src/main/java/org/prebid/server/cookie/UidsCookie.java index 274e1ad12c0..ce7354c45f5 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookie.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookie.java @@ -102,7 +102,7 @@ public UidsCookie updateOptout(boolean optout) { /** * Converts {@link Uids} to JSON string. */ - String toJson() { + public String toJson() { return mapper.encodeToString(uids); } diff --git a/src/main/java/org/prebid/server/cookie/UidsCookieService.java b/src/main/java/org/prebid/server/cookie/UidsCookieService.java index 6b608f06307..8bfe414884c 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookieService.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookieService.java @@ -6,7 +6,6 @@ import io.vertx.ext.web.RoutingContext; import org.apache.commons.lang3.StringUtils; import org.prebid.server.cookie.model.UidWithExpiry; -import org.prebid.server.cookie.model.UidsCookieUpdateResult; import org.prebid.server.cookie.proto.Uids; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -14,12 +13,16 @@ import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.model.UpdateResult; import org.prebid.server.util.HttpUtil; import java.time.Duration; +import java.util.ArrayList; import java.util.Base64; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -34,7 +37,10 @@ public class UidsCookieService { private static final Logger logger = LoggerFactory.getLogger(UidsCookieService.class); private static final String COOKIE_NAME = "uids"; + private static final String COOKIE_NAME_FORMAT = "uids%d"; private static final int MIN_COOKIE_SIZE_BYTES = 500; + private static final int MIN_NUMBER_OF_UID_COOKIES = 1; + private static final int MAX_NUMBER_OF_UID_COOKIES = 30; private final String optOutCookieName; private final String optOutCookieValue; @@ -42,7 +48,9 @@ public class UidsCookieService { private final String hostCookieName; private final String hostCookieDomain; private final long ttlSeconds; + private final int maxCookieSizeBytes; + private final int numberOfUidCookies; private final PrioritizedCoopSyncProvider prioritizedCoopSyncProvider; private final Metrics metrics; @@ -55,6 +63,7 @@ public UidsCookieService(String optOutCookieName, String hostCookieDomain, int ttlDays, int maxCookieSizeBytes, + int numberOfUidCookies, PrioritizedCoopSyncProvider prioritizedCoopSyncProvider, Metrics metrics, JacksonMapper mapper) { @@ -64,6 +73,12 @@ public UidsCookieService(String optOutCookieName, "Configured cookie size is less than allowed minimum size of " + MIN_COOKIE_SIZE_BYTES); } + if (numberOfUidCookies < MIN_NUMBER_OF_UID_COOKIES || numberOfUidCookies > MAX_NUMBER_OF_UID_COOKIES) { + throw new IllegalArgumentException( + "Configured number of uid cookies should be in the range from %d to %d" + .formatted(MIN_NUMBER_OF_UID_COOKIES, MAX_NUMBER_OF_UID_COOKIES)); + } + this.optOutCookieName = optOutCookieName; this.optOutCookieValue = optOutCookieValue; this.hostCookieFamily = hostCookieFamily; @@ -71,6 +86,7 @@ public UidsCookieService(String optOutCookieName, this.hostCookieDomain = StringUtils.isNotBlank(hostCookieDomain) ? hostCookieDomain : null; this.ttlSeconds = Duration.ofDays(ttlDays).getSeconds(); this.maxCookieSizeBytes = maxCookieSizeBytes; + this.numberOfUidCookies = numberOfUidCookies; this.prioritizedCoopSyncProvider = Objects.requireNonNull(prioritizedCoopSyncProvider); this.metrics = Objects.requireNonNull(metrics); this.mapper = Objects.requireNonNull(mapper); @@ -105,19 +121,12 @@ public UidsCookie parseFromRequest(HttpRequestContext httpRequest) { */ UidsCookie parseFromCookies(Map cookies) { final Uids parsedUids = parseUids(cookies); + final boolean isOptedOut = isOptedOut(cookies); - final Boolean optout; - final Map uidsMap; - - if (isOptedOut(cookies)) { - optout = true; - uidsMap = Collections.emptyMap(); - } else { - optout = parsedUids != null ? parsedUids.getOptout() : null; - uidsMap = enrichAndSanitizeUids(parsedUids, cookies); - } - - final Uids uids = Uids.builder().uids(uidsMap).optout(optout).build(); + final Uids uids = Uids.builder() + .uids(isOptedOut ? Collections.emptyMap() : enrichAndSanitizeUids(parsedUids, cookies)) + .optout(isOptedOut) + .build(); return new UidsCookie(uids, mapper); } @@ -125,37 +134,53 @@ UidsCookie parseFromCookies(Map cookies) { /** * Parses cookies {@link Map} and composes {@link Uids} model. */ - public Uids parseUids(Map cookies) { - if (cookies.containsKey(COOKIE_NAME)) { - final String cookieValue = cookies.get(COOKIE_NAME); + private Uids parseUids(Map cookies) { + final Map uids = new HashMap<>(); + + for (Map.Entry cookie : cookies.entrySet()) { + final String cookieKey = cookie.getKey(); + if (!cookieKey.startsWith(COOKIE_NAME)) { + continue; + } + try { - return mapper.decodeValue(Buffer.buffer(Base64.getUrlDecoder().decode(cookieValue)), Uids.class); + final Uids parsedUids = mapper.decodeValue( + Buffer.buffer(Base64.getUrlDecoder().decode(cookie.getValue())), Uids.class); + if (parsedUids != null && parsedUids.getUids() != null) { + parsedUids.getUids().forEach((key, value) -> uids.merge(key, value, (newValue, oldValue) -> + newValue.getExpires().compareTo(oldValue.getExpires()) > 0 ? newValue : oldValue)); + } } catch (IllegalArgumentException | DecodeException e) { - logger.debug("Could not decode or parse {} cookie value {}", e, COOKIE_NAME, cookieValue); + logger.debug("Could not decode or parse {} cookie value {}", e, COOKIE_NAME, cookie.getValue()); } } - return null; + + return Uids.builder().uids(uids).build(); } /** * Creates a {@link Cookie} with 'uids' as a name and encoded JSON string representing supplied {@link UidsCookie} * as a value. */ - public Cookie toCookie(UidsCookie uidsCookie) { - return makeCookie(uidsCookie); + public Cookie aliveCookie(String cookieName, UidsCookie uidsCookie) { + final String value = Base64.getUrlEncoder().encodeToString(uidsCookie.toJson().getBytes()); + return makeCookie(cookieName, value, ttlSeconds); + } + + public Cookie aliveCookie(UidsCookie uidsCookie) { + return aliveCookie(COOKIE_NAME, uidsCookie); } - private int cookieBytesLength(UidsCookie uidsCookie) { - return makeCookie(uidsCookie).encode().getBytes().length; + public Cookie expiredCookie(String cookieName) { + return makeCookie(cookieName, StringUtils.EMPTY, 0); } - private Cookie makeCookie(UidsCookie uidsCookie) { - return Cookie - .cookie(COOKIE_NAME, Base64.getUrlEncoder().encodeToString(uidsCookie.toJson().getBytes())) + private Cookie makeCookie(String cookieName, String value, long maxAge) { + return Cookie.cookie(cookieName, value) .setPath("/") .setSameSite(CookieSameSite.NONE) .setSecure(true) - .setMaxAge(ttlSeconds) + .setMaxAge(maxAge) .setDomain(hostCookieDomain); } @@ -221,20 +246,18 @@ private static boolean facebookSentinelOrEmpty(Map.Entry /*** * Removes expired {@link Uids}, updates {@link UidsCookie} with new uid for family name according to priority - * and trims it to the limit */ - public UidsCookieUpdateResult updateUidsCookie(UidsCookie uidsCookie, String familyName, String uid) { - final UidsCookie initialCookie = trimToLimit(removeExpiredUids(uidsCookie)); // if already exceeded limit - - if (StringUtils.isBlank(uid)) { - return UidsCookieUpdateResult.unaltered(initialCookie.deleteUid(familyName)); - } else if (UidsCookie.isFacebookSentinel(familyName, uid)) { - // At the moment, Facebook calls /setuid with a UID of 0 if the user isn't logged into Facebook. - // They shouldn't be sending us a sentinel value... but since they are, we're refusing to save that ID. - return UidsCookieUpdateResult.unaltered(initialCookie); + public UpdateResult updateUidsCookie(UidsCookie uidsCookie, String familyName, String uid) { + final UidsCookie initialCookie = removeExpiredUids(uidsCookie); + + // At the moment, Facebook calls /setuid with a UID of 0 if the user isn't logged into Facebook. + // They shouldn't be sending us a sentinel value... but since they are, we're refusing to save that ID. + if (StringUtils.isBlank(uid) || UidsCookie.isFacebookSentinel(familyName, uid)) { + return UpdateResult.unaltered(initialCookie); } - return updateUidsCookieByPriority(initialCookie, familyName, uid); + final UidsCookie updatedCookie = initialCookie.updateUid(familyName, uid); + return UpdateResult.updated(updatedCookie); } private static UidsCookie removeExpiredUids(UidsCookie uidsCookie) { @@ -250,47 +273,58 @@ private static UidsCookie removeExpiredUids(UidsCookie uidsCookie) { return updatedCookie; } - private UidsCookieUpdateResult updateUidsCookieByPriority(UidsCookie uidsCookie, String familyName, String uid) { - final UidsCookie updatedCookie = uidsCookie.updateUid(familyName, uid); - if (!cookieExceededMaxLength(updatedCookie)) { - return UidsCookieUpdateResult.updated(updatedCookie); - } + public List splitUidsIntoCookies(UidsCookie uidsCookie) { + final Uids cookieUids = uidsCookie.getCookieUids(); + final Map uids = cookieUids.getUids(); + final boolean hasOptout = !uidsCookie.allowsSync(); - if (!prioritizedCoopSyncProvider.hasPrioritizedBidders() - || prioritizedCoopSyncProvider.isPrioritizedFamily(familyName)) { - return UidsCookieUpdateResult.updated(trimToLimit(updatedCookie)); - } else { - metrics.updateUserSyncSizeBlockedMetric(familyName); - return UidsCookieUpdateResult.unaltered(uidsCookie); - } - } + final Iterator cookieFamilies = cookieFamilyNamesByDescPriorityAndExpiration(uidsCookie); + final List splitCookies = new ArrayList<>(); - private boolean cookieExceededMaxLength(UidsCookie uidsCookie) { - return maxCookieSizeBytes > 0 && cookieBytesLength(uidsCookie) > maxCookieSizeBytes; - } + final int cookieSchemaSize = UidsCookieSize.schemaSize(makeCookie(COOKIE_NAME, StringUtils.EMPTY, ttlSeconds)); + String nextCookieFamily = null; + for (int i = 0; i < numberOfUidCookies; i++) { + final int digits = i < 10 ? Integer.signum(i) : 2; + final UidsCookieSize uidsCookieSize = new UidsCookieSize(cookieSchemaSize + digits, maxCookieSizeBytes); - private UidsCookie trimToLimit(UidsCookie uidsCookie) { - if (!cookieExceededMaxLength(uidsCookie)) { - return uidsCookie; - } + final Map tempUids = new HashMap<>(); + while (nextCookieFamily != null || cookieFamilies.hasNext()) { + nextCookieFamily = nextCookieFamily == null ? cookieFamilies.next() : nextCookieFamily; + final UidWithExpiry uidWithExpiry = uids.get(nextCookieFamily); - UidsCookie trimmedUids = uidsCookie; - final Iterator familyToRemoveIterator = cookieFamilyNamesByAscendingPriority(uidsCookie); + uidsCookieSize.addUid(nextCookieFamily, uidWithExpiry.getUid()); + if (!uidsCookieSize.isValid()) { + break; + } + + tempUids.put(nextCookieFamily, uidWithExpiry); + nextCookieFamily = null; + } + + final String uidsName = i == 0 ? COOKIE_NAME : COOKIE_NAME_FORMAT.formatted(i + 1); + + if (tempUids.isEmpty()) { + splitCookies.add(expiredCookie(uidsName)); + } else { + splitCookies.add(aliveCookie( + uidsName, + new UidsCookie(Uids.builder().uids(tempUids).optout(hasOptout).build(), mapper))); + } + } - while (familyToRemoveIterator.hasNext() && cookieExceededMaxLength(trimmedUids)) { - final String familyToRemove = familyToRemoveIterator.next(); - metrics.updateUserSyncSizedOutMetric(familyToRemove); - trimmedUids = trimmedUids.deleteUid(familyToRemove); + if (nextCookieFamily != null) { + updateSyncSizeMetrics(nextCookieFamily); } - return trimmedUids; + cookieFamilies.forEachRemaining(this::updateSyncSizeMetrics); + + return splitCookies; } - private Iterator cookieFamilyNamesByAscendingPriority(UidsCookie uidsCookie) { + private Iterator cookieFamilyNamesByDescPriorityAndExpiration(UidsCookie uidsCookie) { return uidsCookie.getCookieUids().getUids().entrySet().stream() .sorted(this::compareCookieFamilyNames) .map(Map.Entry::getKey) - .toList() .iterator(); } @@ -303,9 +337,17 @@ private int compareCookieFamilyNames(Map.Entry left, if ((leftPrioritized && rightPrioritized) || (!leftPrioritized && !rightPrioritized)) { return left.getValue().getExpires().compareTo(right.getValue().getExpires()); } else if (leftPrioritized) { - return 1; - } else { // right is prioritized return -1; + } else { // right is prioritized + return 1; + } + } + + private void updateSyncSizeMetrics(String nextCookieFamily) { + if (prioritizedCoopSyncProvider.isPrioritizedFamily(nextCookieFamily)) { + metrics.updateUserSyncSizedOutMetric(nextCookieFamily); + } else { + metrics.updateUserSyncSizeBlockedMetric(nextCookieFamily); } } diff --git a/src/main/java/org/prebid/server/cookie/UidsCookieSize.java b/src/main/java/org/prebid/server/cookie/UidsCookieSize.java new file mode 100644 index 00000000000..c98d74297b2 --- /dev/null +++ b/src/main/java/org/prebid/server/cookie/UidsCookieSize.java @@ -0,0 +1,73 @@ +package org.prebid.server.cookie; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.vertx.core.http.Cookie; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.json.ObjectMapperProvider; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public class UidsCookieSize { + + // {"tempUIDs":{},"optout":false} + private static final int TEMP_UIDS_BASE64_BYTES = "eyJ0ZW1wVUlEcyI6e30sIm9wdG91dCI6ZmFsc2V9".length(); + private static final int UID_TEMPLATE_BYTES; + + static { + try { + UID_TEMPLATE_BYTES = "\"\":{\"uid\":\"\",\"expires\":\"%s\"}," + .formatted(ObjectMapperProvider.mapper().writeValueAsString( + ZonedDateTime.ofInstant(Instant.ofEpochSecond(0, 1), ZoneId.of("UTC")))) + .length(); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private final int cookieSchemaSize; + private final int maxSize; + private int encodedUidsSize; + + public UidsCookieSize(int cookieSchemaSize, int maxSize) { + this.cookieSchemaSize = cookieSchemaSize; + this.maxSize = maxSize; + + encodedUidsSize = 0; + } + + public static int schemaSize(Cookie cookieSchema) { + return cookieSchema.setValue(StringUtils.EMPTY).encode().length(); + } + + public boolean isValid() { + return maxSize <= 0 || totalSize() <= maxSize; + } + + public int totalSize() { + return cookieSchemaSize + + TEMP_UIDS_BASE64_BYTES + + Base64Size.base64Size(encodedUidsSize); + } + + public void addUid(String cookieFamily, String uid) { + final int uidSize = UID_TEMPLATE_BYTES + cookieFamily.length() + uid.length(); + encodedUidsSize = Base64Size.encodeSize(Base64Size.decodeSize(encodedUidsSize) + uidSize); + } + + private static class Base64Size { + + public static int encodeSize(int size) { + return size / 3 * 4 + size % 3; + } + + public static int decodeSize(int encodedSize) { + return encodedSize / 4 * 3 + encodedSize % 4; + } + + private static int base64Size(int encodedSize) { + return (encodedSize & -4) + 4 * Integer.signum(encodedSize % 4); + } + } +} diff --git a/src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java b/src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java deleted file mode 100644 index 000b28c7018..00000000000 --- a/src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.prebid.server.cookie.model; - -import lombok.Value; -import org.prebid.server.cookie.UidsCookie; - -@Value(staticConstructor = "of") -public class UidsCookieUpdateResult { - - boolean successfullyUpdated; - - UidsCookie uidsCookie; - - public static UidsCookieUpdateResult updated(UidsCookie uidsCookie) { - return of(true, uidsCookie); - } - - public static UidsCookieUpdateResult unaltered(UidsCookie uidsCookie) { - return of(false, uidsCookie); - } -} diff --git a/src/main/java/org/prebid/server/handler/OptoutHandler.java b/src/main/java/org/prebid/server/handler/OptoutHandler.java index 48551d37b03..5d9885c70ec 100644 --- a/src/main/java/org/prebid/server/handler/OptoutHandler.java +++ b/src/main/java/org/prebid/server/handler/OptoutHandler.java @@ -106,7 +106,8 @@ private Cookie optCookie(boolean optout, RoutingContext routingContext) { final UidsCookie uidsCookie = uidsCookieService .parseFromRequest(routingContext) .updateOptout(optout); - return uidsCookieService.toCookie(uidsCookie); + + return uidsCookieService.aliveCookie(uidsCookie); } private String optUrl(boolean optout) { diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index 450cc1f7efa..bce568db2d7 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -35,7 +35,6 @@ import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.cookie.exception.UnauthorizedUidsException; import org.prebid.server.cookie.exception.UnavailableForLegalReasonsException; -import org.prebid.server.cookie.model.UidsCookieUpdateResult; import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.execution.timeout.Timeout; @@ -44,6 +43,7 @@ import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.model.Endpoint; +import org.prebid.server.model.UpdateResult; import org.prebid.server.privacy.HostVendorTcfDefinerService; import org.prebid.server.privacy.gdpr.model.HostVendorTcfResponse; import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; @@ -329,12 +329,13 @@ private void respondWithCookie(SetuidContext setuidContext) { final String uid = routingContext.request().getParam(UID_PARAM); final String bidder = setuidContext.getCookieName(); - final UidsCookieUpdateResult uidsCookieUpdateResult = - uidsCookieService.updateUidsCookie(setuidContext.getUidsCookie(), bidder, uid); - final Cookie updatedUidsCookie = uidsCookieService.toCookie(uidsCookieUpdateResult.getUidsCookie()); - addCookie(routingContext, updatedUidsCookie); + final UpdateResult uidsCookieUpdateResult = uidsCookieService.updateUidsCookie( + setuidContext.getUidsCookie(), bidder, uid); - if (uidsCookieUpdateResult.isSuccessfullyUpdated()) { + uidsCookieService.splitUidsIntoCookies(uidsCookieUpdateResult.getValue()) + .forEach(cookie -> addCookie(routingContext, cookie)); + + if (uidsCookieUpdateResult.isUpdated()) { metrics.updateUserSyncSetsMetric(bidder); } final int statusCode = HttpResponseStatus.OK.code(); @@ -345,7 +346,7 @@ private void respondWithCookie(SetuidContext setuidContext) { .status(statusCode) .bidder(bidder) .uid(uid) - .success(uidsCookieUpdateResult.isSuccessfullyUpdated()) + .success(uidsCookieUpdateResult.isUpdated()) .build(); analyticsDelegator.processEvent(setuidEvent, tcfContext); } 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 bc7352f5a51..24522e4cdd6 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -658,6 +658,7 @@ UidsCookieService uidsCookieService( @Value("${host-cookie.domain:#{null}}") String hostCookieDomain, @Value("${host-cookie.ttl-days}") Integer ttlDays, @Value("${host-cookie.max-cookie-size-bytes}") Integer maxCookieSizeBytes, + @Value("${setuid.number-of-uid-cookies:1}") int numberOfUidCookies, PrioritizedCoopSyncProvider prioritizedCoopSyncProvider, Metrics metrics, JacksonMapper mapper) { @@ -670,6 +671,7 @@ UidsCookieService uidsCookieService( hostCookieDomain, ttlDays, maxCookieSizeBytes, + numberOfUidCookies, prioritizedCoopSyncProvider, metrics, mapper); diff --git a/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy b/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy index 8bbda5dd297..d721255741d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy @@ -16,11 +16,11 @@ class UidsCookie { Map tempUIDs Boolean optout - static UidsCookie getDefaultUidsCookie(BidderName bidder = GENERIC) { + static UidsCookie getDefaultUidsCookie(BidderName bidder = GENERIC, Integer daysUntilExpiry = 2) { new UidsCookie().tap { uids = [(bidder): UUID.randomUUID().toString()] tempUIDs = [(bidder): new UidWithExpiry(uid: UUID.randomUUID().toString(), - expires: ZonedDateTime.now(Clock.systemUTC()).plusDays(2))] + expires: ZonedDateTime.now(Clock.systemUTC()).plusDays(daysUntilExpiry))] } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy index a40623d1f27..49553ded1ff 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy @@ -23,9 +23,9 @@ class SetuidRequest { String account static SetuidRequest getDefaultSetuidRequest() { - def request = new SetuidRequest() - request.bidder = GENERIC - request.gdpr = "0" - request + new SetuidRequest().tap { + bidder = GENERIC + gdpr = "0" + } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy b/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy index 2d9d7ee7d36..146f2724325 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy @@ -11,10 +11,10 @@ class UidWithExpiry { String uid ZonedDateTime expires - static UidWithExpiry getDefaultUidWithExpiry() { + static UidWithExpiry getDefaultUidWithExpiry(Integer daysUntilExpiry = 2) { new UidWithExpiry().tap { uid = UUID.randomUUID().toString() - expires = ZonedDateTime.now(Clock.systemUTC()).plusDays(2) + expires = ZonedDateTime.now(Clock.systemUTC()).plusDays(daysUntilExpiry) } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/amp/RawAmpResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/amp/RawAmpResponse.groovy index 9d037af80ff..18e7f705a1e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/amp/RawAmpResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/amp/RawAmpResponse.groovy @@ -6,5 +6,5 @@ import groovy.transform.ToString class RawAmpResponse { String responseBody - Map headers + Map> headers } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/RawAuctionResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/RawAuctionResponse.groovy index a34cc10ddc3..e9f8f01730f 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/RawAuctionResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/RawAuctionResponse.groovy @@ -7,5 +7,5 @@ import org.prebid.server.functional.model.ResponseModel class RawAuctionResponse implements ResponseModel { String responseBody - Map headers + Map> headers } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/cookiesync/RawCookieSyncResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/cookiesync/RawCookieSyncResponse.groovy index af240fa0b26..3854da82dde 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/cookiesync/RawCookieSyncResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/cookiesync/RawCookieSyncResponse.groovy @@ -6,5 +6,5 @@ import groovy.transform.ToString class RawCookieSyncResponse { String responseBody - Map headers + Map> headers } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy index bc35cd07d82..08a9adbb8fb 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy @@ -6,7 +6,7 @@ import org.prebid.server.functional.model.UidsCookie @ToString(includeNames = true, ignoreNulls = true) class SetuidResponse { - Map headers + Map> headers UidsCookie uidsCookie Byte[] responseBody } diff --git a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy index bac2badd46a..31df1efc8d5 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -167,12 +167,21 @@ class PrebidServerService implements ObjectMapperWrapper { } SetuidResponse sendSetUidRequest(SetuidRequest request, UidsCookie uidsCookie, Map header = [:]) { - def uidsCookieAsJson = encode(uidsCookie) - def uidsCookieAsEncodedJson = Base64.urlEncoder.encodeToString(uidsCookieAsJson.bytes) - def response = given(requestSpecification).cookie(UIDS_COOKIE_NAME, uidsCookieAsEncodedJson) - .queryParams(toMap(request)) - .headers(header) - .get(SET_UID_ENDPOINT) + sendSetUidRequest(request, [uidsCookie], header) + } + + SetuidResponse sendSetUidRequest(SetuidRequest request, List uidsCookies, Map header = [:]) { + def cookies = uidsCookies.withIndex().collectEntries { group, index -> + def uidsCookieAsJson = encode(group) + def uidsCookieAsEncodedJson = Base64.urlEncoder.encodeToString(uidsCookieAsJson.bytes) + ["${UIDS_COOKIE_NAME}${index > 0 ? index + 1 : ''}": uidsCookieAsEncodedJson] + } + + def response = given(requestSpecification) + .cookies(cookies) + .queryParams(toMap(request)) + .headers(header) + .get(SET_UID_ENDPOINT) checkResponseStatusCode(response) @@ -344,16 +353,32 @@ class PrebidServerService implements ObjectMapperWrapper { } } - private static Map getHeaders(Response response) { - response.headers().collectEntries { [it.name, it.value] } + private static Map> getHeaders(Response response) { + response.headers().groupBy { it.name }.collectEntries { [(it.key): it.value*.value] } } private static UidsCookie getDecodedUidsCookie(Response response) { - def uids = response.detailedCookie(UIDS_COOKIE_NAME)?.value - if (uids) { - return decode(new String(Base64.urlDecoder.decode(uids)), UidsCookie) - } else { - throw new IllegalStateException("uids cookie is missing in response") + def sortedCookies = response.detailedCookies() + .findAll { cookie -> !(cookie =~ /\buids\d*=\s*;/) } + .sort { a, b -> + def aMatch = (a.name =~ /uids(\d*)/)[0] + def bMatch = (b.name =~ /uids(\d*)/)[0] + + def aNumber = (aMatch?.getAt(1) ? aMatch[1].toInteger() : 0) + def bNumber = (bMatch?.getAt(1) ? bMatch[1].toInteger() : 0) + + aNumber <=> bNumber + } + + def decodedCookiesList = sortedCookies.collect { cookie -> + def uid = (cookie =~ /uids\d*=(\S+?);/)[0][1] + decodeWithBase64(uid as String, UidsCookie) + } + + decodedCookiesList.inject(new UidsCookie()) { uidsCookie, decodedCookie -> + uidsCookie.uids = (uidsCookie.uids ?: new LinkedHashMap()) + (decodedCookie.uids ?: new LinkedHashMap()) + uidsCookie.tempUIDs = (uidsCookie.tempUIDs ?: new LinkedHashMap()) + (decodedCookie.tempUIDs ?: new LinkedHashMap()) + uidsCookie } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy index 96a78df89a8..78d6b03016d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy @@ -32,7 +32,7 @@ class AmpSpec extends BaseSpec { def response = defaultPbsService.sendAmpRequestRaw(ampRequest) then: "Response header should contain PBS version" - assert response.headers["x-prebid"] == "pbs-java/$PBS_VERSION" + assert response.headers["x-prebid"] == ["pbs-java/$PBS_VERSION"] where: ampRequest || description 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 83a726d2035..f9b9688d4da 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy @@ -58,7 +58,7 @@ class AuctionSpec extends BaseSpec { def response = defaultPbsService.sendAuctionRequestRaw(bidRequest) then: "Response header should contain PBS version" - assert response.headers["x-prebid"] == "pbs-java/$PBS_VERSION" + assert response.headers["x-prebid"] == ["pbs-java/$PBS_VERSION"] where: bidRequest || description diff --git a/src/test/groovy/org/prebid/server/functional/tests/CookieSyncSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CookieSyncSpec.groovy index 5beaa13bac4..29542fd8326 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CookieSyncSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CookieSyncSpec.groovy @@ -2263,7 +2263,7 @@ class CookieSyncSpec extends BaseSpec { then: "Response should contain cookie header" assert removeExpiresValue(response.headers[SET_COOKIE_HEADER]) == - "receive-cookie-deprecation=1; Max-Age=${privacySandbox.cookieDeprecation.ttlSeconds}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned" + ["receive-cookie-deprecation=1; Max-Age=${privacySandbox.cookieDeprecation.ttlSeconds}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned"] where: privacySandbox << [PrivacySandbox.defaultPrivacySandbox, PrivacySandbox.getDefaultPrivacySandbox(true, -PBSUtils.randomNumber)] @@ -2290,7 +2290,7 @@ class CookieSyncSpec extends BaseSpec { then: "Response should contain cookie header" assert removeExpiresValue(response.headers[SET_COOKIE_HEADER]) == - "receive-cookie-deprecation=1; Max-Age=${TimeUnit.DAYS.toSeconds(7)}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned" + ["receive-cookie-deprecation=1; Max-Age=${TimeUnit.DAYS.toSeconds(7)}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned"] } def "PBS should set cookie deprecation header from the default account when default account contain privacy sandbox and request account is empty"() { @@ -2315,7 +2315,7 @@ class CookieSyncSpec extends BaseSpec { then: "Response should contain cookie header" assert removeExpiresValue(response.headers[SET_COOKIE_HEADER]) == - "receive-cookie-deprecation=1; Max-Age=${privacySandbox.cookieDeprecation.ttlSeconds}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned" + ["receive-cookie-deprecation=1; Max-Age=${privacySandbox.cookieDeprecation.ttlSeconds}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned"] } def "PBS shouldn't set cookie deprecation header when cookie sync request doesn't contain account"() { @@ -2346,7 +2346,7 @@ class CookieSyncSpec extends BaseSpec { .collectEntries { [it.bidder, it.error] } } - private static String removeExpiresValue(String cookie) { - cookie.replaceFirst(/Expires=[^;]+;/, "Expires=*;") + private static List removeExpiresValue(List cookies) { + cookies.collect { it.replaceFirst(/Expires=[^;]+;/, "Expires=*;") } } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy index caabebba8ff..2c6d1556a81 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy @@ -105,10 +105,11 @@ class HttpSettingsSpec extends BaseSpec { def "PBS should take account information from http data source on setuid request"() { given: "Pbs config with adapters.generic.usersync.redirect.*" - def prebidServerService = pbsServiceFactory.getService(PbsConfig.httpSettingsConfig + + def pbsConfig = PbsConfig.httpSettingsConfig + ["adapters.generic.usersync.redirect.url" : "$networkServiceContainer.rootUri/generic-usersync&redir={{redirect_url}}".toString(), "adapters.generic.usersync.redirect.support-cors" : "false", - "adapters.generic.usersync.redirect.format-override": "blank"]) + "adapters.generic.usersync.redirect.format-override": "blank"] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Get default SetuidRequest and set account, gdpr=1 " def request = SetuidRequest.defaultSetuidRequest @@ -123,14 +124,17 @@ class HttpSettingsSpec extends BaseSpec { when: "PBS processes setuid request" def response = prebidServerService.sendSetUidRequest(request, uidsCookie) - then: "Response should contain uids cookie" - assert !response.uidsCookie.tempUIDs + then: "Response should contain tempUIDs cookie" assert !response.uidsCookie.uids + assert response.uidsCookie.tempUIDs assert response.responseBody == ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") and: "There should be only one account request" assert httpSettings.getRequestCount(request.account) == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should take account information from http data source on vtrack request"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy index 1e33542e564..7e9eff9ebd3 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy @@ -3,19 +3,21 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.UidsCookie import org.prebid.server.functional.model.request.setuid.SetuidRequest import org.prebid.server.functional.model.response.cookiesync.UserSyncInfo +import org.prebid.server.functional.model.response.setuid.SetuidResponse import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.TcfConsent import org.prebid.server.util.ResourceUtil import spock.lang.Shared import java.time.Clock import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.ALIAS_CAMEL_CASE import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS -import static org.prebid.server.functional.model.bidder.BidderName.EMPTY import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.BidderName.GENERIC_CAMEL_CASE import static org.prebid.server.functional.model.bidder.BidderName.OPENX @@ -30,8 +32,11 @@ import static org.prebid.server.functional.util.privacy.TcfConsent.RUBICON_VENDO class SetUidSpec extends BaseSpec { private static final Integer MAX_COOKIE_SIZE = 500 + private static final Integer MAX_NUMBER_OF_UID_COOKIES = 30 + private static final Integer UPDATED_EXPIRE_DAYS = 14 private static final UserSyncInfo.Type USER_SYNC_TYPE = REDIRECT private static final boolean CORS_SUPPORT = false + private static final Integer RANDOM_EXPIRE_DAY = PBSUtils.getRandomNumber(1, 10) private static final String USER_SYNC_URL = "$networkServiceContainer.rootUri/generic-usersync" private static final Map PBS_CONFIG = ["host-cookie.max-cookie-size-bytes" : MAX_COOKIE_SIZE as String, @@ -43,13 +48,16 @@ class SetUidSpec extends BaseSpec { "adapters.${APPNEXUS.value}.usersync.cookie-family-name" : APPNEXUS.value, "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()] + private static final Map UID_COOKIES_CONFIG = ['setuid.number-of-uid-cookies': MAX_NUMBER_OF_UID_COOKIES.toString()] private static final Map GENERIC_ALIAS_CONFIG = ["adapters.generic.aliases.alias.enabled" : "true", - "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] private static final String TCF_ERROR_MESSAGE = "The gdpr_consent param prevents cookies from being saved" private static final int UNAVAILABLE_FOR_LEGAL_REASONS_CODE = 451 @Shared - PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GENERIC_ALIAS_CONFIG) + PrebidServerService singleCookiesPbsService = pbsServiceFactory.getService(PBS_CONFIG + GENERIC_ALIAS_CONFIG) + @Shared + PrebidServerService multipleCookiesPbsService = pbsServiceFactory.getService(PBS_CONFIG + UID_COOKIES_CONFIG + GENERIC_ALIAS_CONFIG) def "PBS should set uids cookie"() { given: "Default SetuidRequest" @@ -57,30 +65,49 @@ class SetUidSpec extends BaseSpec { def uidsCookie = UidsCookie.defaultUidsCookie when: "PBS processes setuid request" - def response = prebidServerService.sendSetUidRequest(request, uidsCookie) + def response = singleCookiesPbsService.sendSetUidRequest(request, uidsCookie) + + then: "Response should contain uid cookie" + assert response.uidsCookie.tempUIDs[GENERIC].uid + assert response.responseBody == + ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + } + + def "PBS should updated uids cookie when request parameters contain uid"() { + given: "Default SetuidRequest" + def requestUid = UUID.randomUUID().toString() + def request = SetuidRequest.defaultSetuidRequest.tap { + uid = requestUid + } + def uidsCookie = UidsCookie.defaultUidsCookie + + and: "Flush metrics" + flushMetrics(singleCookiesPbsService) + + when: "PBS processes setuid request" + def response = singleCookiesPbsService.sendSetUidRequest(request, uidsCookie) then: "Response should contain uids cookie" - assert !response.uidsCookie.tempUIDs - assert !response.uidsCookie.uids + assert daysDifference(response.uidsCookie.tempUIDs[GENERIC].expires) == UPDATED_EXPIRE_DAYS + assert response.uidsCookie.tempUIDs[GENERIC].uid == requestUid assert response.responseBody == ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + + and: "usersync.FAMILY.sets metric should be updated" + def metrics = singleCookiesPbsService.sendCollectedMetricsRequest() + assert metrics["usersync.${GENERIC.value}.sets"] == 1 } def "PBS setuid should remove expired uids cookie"() { given: "Default SetuidRequest" def request = SetuidRequest.defaultSetuidRequest - def uidsCookie = UidsCookie.defaultUidsCookie.tap { - def uidWithExpiry = defaultUidWithExpiry.tap { - expires = ZonedDateTime.now(Clock.systemUTC()).minusDays(2) - } - tempUIDs = [(RUBICON): uidWithExpiry] - } + def uidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON, -RANDOM_EXPIRE_DAY) when: "PBS processes setuid request" - def response = prebidServerService.sendSetUidRequest(request, uidsCookie) + def response = singleCookiesPbsService.sendSetUidRequest(request, uidsCookie) then: "Response shouldn't contain uids cookie" - assert !response.uidsCookie.tempUIDs[RUBICON] + assert !response.uidsCookie.tempUIDs } def "PBS setuid should return requested uids cookie when priority bidder not present in config"() { @@ -99,7 +126,7 @@ class SetUidSpec extends BaseSpec { when: "PBS processes setuid request" def response = prebidServerService.sendSetUidRequest(request, uidsCookie) - then: "Response should contain requested uids" + then: "Response should contain requested tempUIDs" assert response.uidsCookie.tempUIDs[GENERIC] assert response.uidsCookie.tempUIDs[RUBICON] @@ -120,8 +147,8 @@ class SetUidSpec extends BaseSpec { } def rubiconBidder = RUBICON def uidsCookie = UidsCookie.defaultUidsCookie.tap { - tempUIDs = [(APPNEXUS) : defaultUidWithExpiry, - (rubiconBidder): defaultUidWithExpiry] + tempUIDs = [(APPNEXUS) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1), + (rubiconBidder): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY)] } when: "PBS processes setuid request" @@ -135,7 +162,7 @@ class SetUidSpec extends BaseSpec { pbsServiceFactory.removeContainer(pbsConfig) } - def "PBS setuid should remove earliest expiration bidder when size is full"() { + def "PBS setuid should remove most distant expiration bidder when size is full"() { given: "PBS config" def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": GENERIC.value] def prebidServerService = pbsServiceFactory.getService(pbsConfig) @@ -159,7 +186,7 @@ class SetUidSpec extends BaseSpec { def response = prebidServerService.sendSetUidRequest(request, uidsCookie) then: "Response should contain uids cookies" - assert response.uidsCookie.tempUIDs[APPNEXUS] + assert response.uidsCookie.tempUIDs[RUBICON] assert response.uidsCookie.tempUIDs[GENERIC] cleanup: "Stop and remove pbs container" @@ -200,13 +227,12 @@ class SetUidSpec extends BaseSpec { def "PBS setuid should reject bidder when cookie's filled and requested bidder in pri and rejected by tcf"() { given: "Setuid request" - def bidderName = RUBICON def pbsConfig = PBS_CONFIG + ["gdpr.host-vendor-id": RUBICON_VENDOR_ID.toString(), - "cookie-sync.pri" : bidderName.value] + "cookie-sync.pri" : RUBICON.value] def prebidServerService = pbsServiceFactory.getService(pbsConfig) def request = SetuidRequest.defaultSetuidRequest.tap { - it.bidder = bidderName + it.bidder = RUBICON gdpr = "1" gdprConsent = new TcfConsent.Builder().build() } @@ -226,13 +252,13 @@ class SetUidSpec extends BaseSpec { and: "usersync.FAMILY.tcf.blocked metric should be updated" def metric = prebidServerService.sendCollectedMetricsRequest() - assert metric["usersync.${bidderName.value}.tcf.blocked"] == 1 + assert metric["usersync.${RUBICON.value}.tcf.blocked"] == 1 cleanup: "Stop and remove pbs container" pbsServiceFactory.removeContainer(pbsConfig) } - def "PBS setuid should remove oldest uid and log metric when cookie's filled and oldest uid's not on the pri"() { + def "PBS setuid should remove most distant expiration uid and log metric when cookie's filled and this uid's not on the pri"() { given: "PBS config" def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": GENERIC.value] def prebidServerService = pbsServiceFactory.getService(pbsConfig) @@ -245,21 +271,17 @@ class SetUidSpec extends BaseSpec { uid = UUID.randomUUID().toString() } - def bidderName = RUBICON def uidsCookie = UidsCookie.defaultUidsCookie.tap { - def uidWithExpiry = defaultUidWithExpiry.tap { - expires.plusDays(10) - } - tempUIDs = [(APPNEXUS) : defaultUidWithExpiry, - (bidderName): uidWithExpiry] + tempUIDs = [(APPNEXUS): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY), + (RUBICON) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1)] } when: "PBS processes setuid request" def response = prebidServerService.sendSetUidRequest(request, uidsCookie) - and: "usersync.FAMILY.sizedout metric should be updated" + and: "usersync.FAMILY.sizeblocked metric should be updated" def metrics = prebidServerService.sendCollectedMetricsRequest() - assert metrics["usersync.${bidderName.value}.sizedout"] == 1 + assert metrics["usersync.${RUBICON.value}.sizeblocked"] == 1 then: "Response should contain uids cookies" assert response.uidsCookie.tempUIDs[APPNEXUS] @@ -269,7 +291,7 @@ class SetUidSpec extends BaseSpec { pbsServiceFactory.removeContainer(pbsConfig) } - def "PBS SetUid should remove oldest bidder from uids cookie in favor of prioritized bidder"() { + def "PBS set uid should emit sizeblocked metric and remove most distant expiration bidder from uids cookie for non-prioritized bidder"() { given: "PBS config" def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": "$OPENX.value, $GENERIC.value" as String] def prebidServerService = pbsServiceFactory.getService(pbsConfig) @@ -282,8 +304,8 @@ class SetUidSpec extends BaseSpec { and: "Set up set uid cookie" def uidsCookie = UidsCookie.defaultUidsCookie.tap { - it.tempUIDs = [(APPNEXUS): defaultUidWithExpiry, - (RUBICON) : defaultUidWithExpiry] + tempUIDs = [(APPNEXUS): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1), + (RUBICON) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY)] } and: "Flush metrics" @@ -296,14 +318,14 @@ class SetUidSpec extends BaseSpec { assert response.uidsCookie.tempUIDs[OPENX] and: "Response set cookie header size should be lowest or the same as max cookie config size" - assert response.headers.get("Set-Cookie").split("Secure;")[0].length() <= MAX_COOKIE_SIZE + assert getSetUidsHeaders(response).first.split("Secure;")[0].length() <= MAX_COOKIE_SIZE and: "Request bidder should contain uid from Set uid request" assert response.uidsCookie.tempUIDs[OPENX].uid == request.uid - and: "usersync.FAMILY.sizedout metric should be updated" + and: "usersync.FAMILY.sizeblocked metric should be updated" def metricsRequest = prebidServerService.sendCollectedMetricsRequest() - assert metricsRequest["usersync.${APPNEXUS.value}.sizedout"] == 1 + assert metricsRequest["usersync.${APPNEXUS.value}.sizeblocked"] == 1 and: "usersync.FAMILY.sets metric should be updated" assert metricsRequest["usersync.${OPENX.value}.sets"] == 1 @@ -312,6 +334,42 @@ class SetUidSpec extends BaseSpec { pbsServiceFactory.removeContainer(pbsConfig) } + def "PBS set uid should emit sizedout metric and remove most distant expiration bidder from uids cookie in prioritized bidder"() { + given: "PBS config" + def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": "$OPENX.value, $APPNEXUS.value, $RUBICON.value" as String] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Set uid request" + def request = SetuidRequest.defaultSetuidRequest + + and: "Set up set uid cookie" + def uidsCookie = UidsCookie.defaultUidsCookie.tap { + tempUIDs = [(APPNEXUS): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1), + (OPENX) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY), + (RUBICON) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY)] + } + + and: "Flush metrics" + flushMetrics(prebidServerService) + + when: "PBS processes set uid request" + def response = prebidServerService.sendSetUidRequest(request, uidsCookie) + + then: "Response should contain pri bidder in uids cookies" + assert response.uidsCookie.tempUIDs[OPENX] + assert response.uidsCookie.tempUIDs[RUBICON] + + and: "Response set cookie header size should be lowest or the same as max cookie config size" + assert getSetUidsHeaders(response).first.split("Secure;")[0].length() <= MAX_COOKIE_SIZE + + and: "usersync.FAMILY.sizedout metric should be updated" + def metricsRequest = prebidServerService.sendCollectedMetricsRequest() + assert metricsRequest["usersync.${APPNEXUS.value}.sizedout"] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + def "PBS setuid should reject request when requested bidder mismatching with cookie-family-name"() { given: "Default SetuidRequest" def request = SetuidRequest.getDefaultSetuidRequest().tap { @@ -319,7 +377,7 @@ class SetUidSpec extends BaseSpec { } when: "PBS processes setuid request" - prebidServerService.sendSetUidRequest(request, UidsCookie.defaultUidsCookie) + singleCookiesPbsService.sendSetUidRequest(request, UidsCookie.defaultUidsCookie) then: "Request should fail with error" def exception = thrown(PrebidServerException) @@ -329,4 +387,145 @@ class SetUidSpec extends BaseSpec { where: bidderName << [UNKNOWN, WILDCARD, GENERIC_CAMEL_CASE, ALIAS, ALIAS_CAMEL_CASE] } + + def "PBS should throw an exception when incoming request have optout flag"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + + and: "PBS service with optout cookies" + def pbsConfig = PBS_CONFIG + ["host-cookie.optout-cookie.name" : "uids", + "host-cookie.optout-cookie.value": Base64.urlEncoder.encodeToString(encode(genericUidsCookie).bytes)] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + when: "PBS processes setuid request" + prebidServerService.sendSetUidRequest(request, [genericUidsCookie]) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 401 + assert exception.responseBody == 'Unauthorized: Sync is not allowed for this uids' + } + + def "PBS should merge cookies when incoming request have multiple uids cookies"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest.tap { + uid = UUID.randomUUID().toString() + } + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + def rubiconUidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [genericUidsCookie, rubiconUidsCookie]) + + then: "Response should contain requested tempUIDs" + assert response.uidsCookie.tempUIDs[GENERIC] + assert response.uidsCookie.tempUIDs[RUBICON] + + and: "Headers uids cookies should contain same cookie as response" + def setUidsHeaders = getSetUidsHeaders(response) + def uidsCookie = extractHeaderTempUIDs(setUidsHeaders.first) + assert setUidsHeaders.size() == 1 + assert uidsCookie.tempUIDs[GENERIC] + assert uidsCookie.tempUIDs[RUBICON] + } + + def "PBS should send multiple uids cookies by priority and expiration timestamp"() { + given: "PBS config" + def pbsConfig = PBS_CONFIG + + UID_COOKIES_CONFIG + + ["cookie-sync.pri": "$OPENX.value, $GENERIC.value" as String] + + ["host-cookie.max-cookie-size-bytes": MAX_COOKIE_SIZE as String] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + + and: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest + + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC, RANDOM_EXPIRE_DAY + 1) + def rubiconUidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON, RANDOM_EXPIRE_DAY + 2) + def openxUidsCookie = UidsCookie.getDefaultUidsCookie(OPENX, RANDOM_EXPIRE_DAY + 3) + def appnexusUidsCookie = UidsCookie.getDefaultUidsCookie(APPNEXUS, RANDOM_EXPIRE_DAY) + + when: "PBS processes setuid request" + def response = prebidServerService.sendSetUidRequest(request, [appnexusUidsCookie, genericUidsCookie, rubiconUidsCookie, openxUidsCookie]) + + then: "Response should contain requested tempUIDs" + assert response.uidsCookie.tempUIDs.keySet() == new LinkedHashSet([GENERIC, OPENX, APPNEXUS, RUBICON]) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should remove duplicates when incoming cookie-family already exists in the working list"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest + + and: "Duplicated uids cookies" + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC, RANDOM_EXPIRE_DAY) + def duplicateUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC, RANDOM_EXPIRE_DAY + 1) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [genericUidsCookie, duplicateUidsCookie]) + + then: "Response should contain single generic uid with most distant expiration timestamp" + assert response.uidsCookie.tempUIDs.size() == 1 + assert response.uidsCookie.tempUIDs[GENERIC].uid == duplicateUidsCookie.tempUIDs[GENERIC].uid + assert response.uidsCookie.tempUIDs[GENERIC].expires == duplicateUidsCookie.tempUIDs[GENERIC].expires + } + + def "PBS should shouldn't modify uids cookie when uid is empty"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest.tap { + it.uid = null + it.bidder = GENERIC + } + + and: "Specific uids cookies" + def uidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [uidsCookie]) + + then: "Response should contain single generic uid" + assert response.uidsCookie.tempUIDs.size() == 1 + assert response.uidsCookie.tempUIDs[GENERIC].uid == uidsCookie.tempUIDs[GENERIC].uid + assert response.uidsCookie.tempUIDs[GENERIC].expires == uidsCookie.tempUIDs[GENERIC].expires + } + + def "PBS should include all cookies even empty when incoming request have multiple uids cookies"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest.tap { + uid = UUID.randomUUID().toString() + } + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + def rubiconUidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [genericUidsCookie, rubiconUidsCookie]) + + then: "Response should contain requested tempUIDs" + assert response.uidsCookie.tempUIDs[GENERIC] + assert response.uidsCookie.tempUIDs[RUBICON] + + and: "Headers uids cookies should contain same cookie as response" + assert getSetUidsHeaders(response).size() == 1 + assert getSetUidsHeaders(response, true).size() == MAX_NUMBER_OF_UID_COOKIES + } + + List getSetUidsHeaders(SetuidResponse response, boolean includeEmpty = false) { + response.headers.get("Set-Cookie").findAll { cookie -> + includeEmpty || !(cookie =~ /\buids\d*=\s*;/) + } + } + + static UidsCookie extractHeaderTempUIDs(String header) { + def uid = (header =~ /uids\d*=(\S+?);/)[0][1] + decodeWithBase64(uid as String, UidsCookie) + } + + def daysDifference(ZonedDateTime inputDate) { + ZonedDateTime now = ZonedDateTime.now(Clock.systemUTC()).minusHours(1) + return ChronoUnit.DAYS.between(now, inputDate) + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/TopicsHeaderSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TopicsHeaderSpec.groovy index f94ae8772c9..ea2bbebb67e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TopicsHeaderSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TopicsHeaderSpec.groovy @@ -43,7 +43,7 @@ class TopicsHeaderSpec extends BaseSpec { assert bidderRequest.user.data[0].segment.id.sort().containsAll(firstSecBrowsingTopic.segments) and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS should populate headers with Observe-Browsing-Topics and emit warning when Sec-Browsing-Topics invalid header present in request"() { @@ -63,7 +63,7 @@ class TopicsHeaderSpec extends BaseSpec { assert !bidderRequest.user.data and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] and: "Response should contain Observe-Browsing-Topics header" assert response.responseBody.contains("\"warnings\":{\"prebid\":[{\"code\":999,\"message\":\"Invalid field " + @@ -94,7 +94,7 @@ class TopicsHeaderSpec extends BaseSpec { assert !bidderRequest.user.data and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] and: "Response should contain Observe-Browsing-Topics header" assert response.responseBody.contains("\"warnings\":{\"prebid\":[{\"code\":999,\"message\":\"Invalid field " + @@ -128,7 +128,7 @@ class TopicsHeaderSpec extends BaseSpec { assert bidderRequest.user.data.size() == 10 and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS shouldn't populate user.data when header Sec-Browsing-Topics contain 10 `p=` value and 11 valid"() { @@ -155,7 +155,7 @@ class TopicsHeaderSpec extends BaseSpec { assert response.responseBody.contains("Invalid field in Sec-Browsing-Topics header: ${header.replace(", ", "")} discarded due to limit reached.") and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS should update user.data when Sec-Browsing-Topics header present in request"() { @@ -193,7 +193,7 @@ class TopicsHeaderSpec extends BaseSpec { secBrowsingTopic.segments].sort()) and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS should overlap segments when Sec-Browsing-Topics header present in request"() { @@ -229,7 +229,7 @@ class TopicsHeaderSpec extends BaseSpec { [randomSegment as String, firstSecBrowsingTopic.segments[0], secondSecBrowsingTopic.segments[0]] and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS should multiple taxonomies when Sec-Browsing-Topics header present in request"() { @@ -255,7 +255,7 @@ class TopicsHeaderSpec extends BaseSpec { .containsAll([firstSecBrowsingTopic.segments[0], secondSecBrowsingTopic.segments[0]]) and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS should populate user.data with empty name when privacy sand box present with empty name"() { @@ -282,7 +282,7 @@ class TopicsHeaderSpec extends BaseSpec { assert bidderRequest.user.data[0].segment.id.sort() == secBrowsingTopic.segments.sort() and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] where: topicsdomain << [null, ""] diff --git a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy index e6b808cd2aa..3ab9e349ac9 100644 --- a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy @@ -29,6 +29,10 @@ trait ObjectMapperWrapper { mapper.readValue(jsonString, typeReference) } + final static T decodeWithBase64(String base64String, Class clazz) { + mapper.readValue(new String(Base64.decoder.decode(base64String)), clazz) + } + final static Map toMap(Object object) { mapper.convertValue(object, Map) } diff --git a/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java b/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java index bf3aa6fde14..4964d8ad4fa 100644 --- a/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java +++ b/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java @@ -17,7 +17,6 @@ import java.util.Set; import java.util.stream.Collectors; -import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -93,26 +92,6 @@ public void isPrioritizedFamilyShouldReturnFalseIfCookieFamilyDoesNotCorrespondT assertThat(target.isPrioritizedFamily("invalid-cookie-family")).isFalse(); } - @Test - public void hasPrioritizedBiddersShouldReturnTrueWhenThereArePrioritizedBiddersDefined() { - // given - givenValidBidderWithCookieSync("bidder"); - - target = new PrioritizedCoopSyncProvider(Set.of("bidder"), bidderCatalog); - - // when and then - assertThat(target.hasPrioritizedBidders()).isTrue(); - } - - @Test - public void hasPrioritizedBiddersShouldReturnFalseWhenThereAreNoPrioritizedBiddersDefined() { - // given - target = new PrioritizedCoopSyncProvider(emptySet(), bidderCatalog); - - // when and then - assertThat(target.hasPrioritizedBidders()).isFalse(); - } - private void givenValidBiddersWithCookieSync(String... bidders) { Arrays.stream(bidders).forEach(this::givenValidBidderWithCookieSync); given(bidderCatalog.usersyncReadyBidders()) diff --git a/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java b/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java index 6da5e11ebad..c73ee163935 100644 --- a/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java +++ b/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java @@ -4,6 +4,7 @@ import io.vertx.core.http.Cookie; import io.vertx.core.http.CookieSameSite; import io.vertx.ext.web.RoutingContext; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,9 +12,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.cookie.model.UidWithExpiry; -import org.prebid.server.cookie.model.UidsCookieUpdateResult; import org.prebid.server.cookie.proto.Uids; import org.prebid.server.metric.Metrics; +import org.prebid.server.model.UpdateResult; import java.io.IOException; import java.time.Instant; @@ -22,13 +23,16 @@ import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -52,11 +56,11 @@ public class UidsCookieServiceTest extends VertxTest { @Mock private Metrics metrics; - private UidsCookieService uidsCookieService; + private UidsCookieService target; @BeforeEach public void setUp() { - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", null, @@ -64,6 +68,7 @@ public void setUp() { "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -82,7 +87,7 @@ public void shouldReturnNonEmptyUidsCookieFromCookiesMap() { + "4xMDMzMjktMDM6MDAiIH0gfSB9"); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromCookies(cookies); + final UidsCookie uidsCookie = target.parseFromCookies(cookies); // then assertThat(uidsCookie).isNotNull(); @@ -90,6 +95,50 @@ public void shouldReturnNonEmptyUidsCookieFromCookiesMap() { assertThat(uidsCookie.uidFrom(ADNXS)).isEqualTo("12345"); } + @Test + public void shouldReturnNonEmptyUidsCookieFromCookiesMapWhenSeveralUidsCookiesArePresent() { + // given + final Map cookies = Map.of( + "uids", "eyJ0ZW1wVUlEcyI6eyJiaWRkZXJBIjp7InVpZCI6ImJpZGRlci1BLXVp" + + "ZCIsImV4cGlyZXMiOiIyMDIzLTEyLTA1VDE5OjAwOjA1LjEwMzMyOS0wMzowMCJ9L" + + "CJiaWRkZXJCIjp7InVpZCI6ImJpZGRlci1CLXVpZCIsImV4cGlyZXMiOiIyMDIzLTE" + + "yLTA1VDE5OjAwOjA1LjEwMzMyOS0wMzowMCJ9fX0=", + "uids2", "eyJ0ZW1wVUlEcyI6eyJiaWRkZXJDIjp7InVpZCI6ImJpZGRlci1DLXVpZCIsIm" + + "V4cGlyZXMiOiIyMDIzLTEyLTA1VDE5OjAwOjA1LjEwMzMyOS0wMzowMCJ9LCJiaWRkZXJEIjp7I" + + "nVpZCI6ImJpZGRlci1ELXVpZCIsImV4cGlyZXMiOiIyMDIzLTEyLTA1VDE5OjAwOjA1LjEwMzMy" + + "OS0wMzowMCJ9fX0"); + + // when + final UidsCookie uidsCookie = target.parseFromCookies(cookies); + + // then + assertThat(uidsCookie).isNotNull(); + assertThat(uidsCookie.uidFrom("bidderA")).isEqualTo("bidder-A-uid"); + assertThat(uidsCookie.uidFrom("bidderB")).isEqualTo("bidder-B-uid"); + assertThat(uidsCookie.uidFrom("bidderC")).isEqualTo("bidder-C-uid"); + assertThat(uidsCookie.uidFrom("bidderD")).isEqualTo("bidder-D-uid"); + } + + @Test + public void shouldReturnMergedUidsFromCookiesWithOldestUidWhenDuplicatesArePresent() { + // given + final Map cookies = Map.of( + "uids", "eyJ0ZW1wVUlEcyI6eyJiaWRkZXJBIjp7InVpZCI6ImJpZGRlci1BMS11aW" + + "QiLCJleHBpcmVzIjoiMjAyMy0xMi0wNVQxOTowMDowNS4xMDMzMjktMDM6MDAifSwiYml" + + "kZGVyQiI6eyJ1aWQiOiJiaWRkZXItQi11aWQiLCJleHBpcmVzIjoiMjAyMy0xMi0wNVQxOTo" + + "wMDowNS4xMDMzMjktMDM6MDAifX19", + "uids2", "eyJ0ZW1wVUlEcyI6eyJiaWRkZXJBIjp7InVpZCI6ImJpZGRlci1BMi11aWQiLCJleH" + + "BpcmVzIjoiMjAyNC0xMi0wNVQxOTowMDowNS4xMDMzMjktMDM6MDAifX19"); + + // when + final UidsCookie uidsCookie = target.parseFromCookies(cookies); + + // then + assertThat(uidsCookie).isNotNull(); + assertThat(uidsCookie.uidFrom("bidderA")).isEqualTo("bidder-A2-uid"); + assertThat(uidsCookie.uidFrom("bidderB")).isEqualTo("bidder-B-uid"); + } + @Test public void shouldReturnNonEmptyUidsCookie() { // given @@ -104,7 +153,7 @@ public void shouldReturnNonEmptyUidsCookie() { + "4xMDMzMjktMDM6MDAiIH0gfSB9"))); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie).isNotNull(); @@ -115,7 +164,7 @@ public void shouldReturnNonEmptyUidsCookie() { @Test public void shouldReturnNonNullUidsCookieIfUidsCookieIsMissing() { // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie).isNotNull(); @@ -127,7 +176,7 @@ public void shouldReturnNonNullUidsCookieIfUidsCookieIsNonBase64() { given(routingContext.cookieMap()).willReturn(singletonMap("uids", Cookie.cookie("uids", "abcde"))); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie).isNotNull(); @@ -140,7 +189,7 @@ public void shouldReturnNonNullUidsCookieIfUidsCookieIsNonJson() { given(routingContext.cookieMap()).willReturn(singletonMap("uids", Cookie.cookie("tempUIDs", "bm9uLWpzb24="))); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie).isNotNull(); @@ -153,7 +202,7 @@ public void shouldReturnUidsCookieWithOptoutTrueIfUidsCookieIsMissingAndOptoutCo singletonMap(OPT_OUT_COOKIE_NAME, Cookie.cookie(OPT_OUT_COOKIE_NAME, OPT_OUT_COOKIE_VALUE))); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie.allowsSync()).isFalse(); @@ -177,7 +226,7 @@ public void shouldReturnUidsCookieWithOptoutTrueIfUidsCookieIsPresentAndOptoutCo given(routingContext.cookieMap()).willReturn(cookies); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie.allowsSync()).isFalse(); @@ -186,7 +235,7 @@ public void shouldReturnUidsCookieWithOptoutTrueIfUidsCookieIsPresentAndOptoutCo } @Test - public void toCookieShouldSetSameSiteNone() { + public void aliveCookieShouldSetSameSiteNone() { // given final Uids uids = Uids.builder() .uids(Map.of(RUBICON, UidWithExpiry.live("test"))) @@ -195,14 +244,14 @@ public void toCookieShouldSetSameSiteNone() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then assertThat(cookie.getSameSite()).isEqualTo(CookieSameSite.NONE); } @Test - public void toCookieShouldSetSecure() { + public void aliveCookieShouldSetSecure() { // given final Uids uids = Uids.builder() .uids(Map.of(RUBICON, UidWithExpiry.live("test"))) @@ -211,14 +260,14 @@ public void toCookieShouldSetSecure() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then assertThat(cookie.isSecure()).isTrue(); } @Test - public void toCookieShouldSetPath() { + public void aliveCookieShouldSetPath() { // given final Uids uids = Uids.builder() .uids(Map.of(RUBICON, UidWithExpiry.live("test"))) @@ -227,7 +276,7 @@ public void toCookieShouldSetPath() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then assertThat(cookie.getPath()).isEqualTo("/"); @@ -251,7 +300,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieHasNotExpectedVal given(routingContext.cookieMap()).willReturn(cookies); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie.allowsSync()).isTrue(); @@ -262,7 +311,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieHasNotExpectedVal @Test public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieNameNotSpecified() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( null, "true", null, @@ -270,6 +319,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieNameNotSpecified( "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -277,7 +327,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieNameNotSpecified( singletonMap(OPT_OUT_COOKIE_NAME, Cookie.cookie("trp_optout", "true"))); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie.allowsSync()).isTrue(); @@ -286,7 +336,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieNameNotSpecified( @Test public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieValueNotSpecified() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", null, null, @@ -294,6 +344,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieValueNotSpecified "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -301,7 +352,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieValueNotSpecified singletonMap(OPT_OUT_COOKIE_NAME, Cookie.cookie("trp_optout", "true"))); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie.allowsSync()).isTrue(); @@ -310,7 +361,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieValueNotSpecified @Test public void shouldReturnRubiconCookieValueFromHostCookieWhenUidValueIsAbsent() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", "rubicon", @@ -318,13 +369,14 @@ public void shouldReturnRubiconCookieValueFromHostCookieWhenUidValueIsAbsent() { "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); given(routingContext.cookieMap()).willReturn(singletonMap("khaos", Cookie.cookie("khaos", "abc123"))); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie.uidFrom(RUBICON)).isEqualTo("abc123"); @@ -333,7 +385,7 @@ public void shouldReturnRubiconCookieValueFromHostCookieWhenUidValueIsAbsent() { @Test public void shouldReturnRubiconCookieValueFromHostCookieWhenUidValueIsPresentButDiffers() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", "rubicon", @@ -341,6 +393,7 @@ public void shouldReturnRubiconCookieValueFromHostCookieWhenUidValueIsPresentBut "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -359,7 +412,7 @@ public void shouldReturnRubiconCookieValueFromHostCookieWhenUidValueIsPresentBut given(routingContext.cookieMap()).willReturn(cookies); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie.uidFrom(RUBICON)).isEqualTo("abc123"); @@ -377,7 +430,7 @@ public void shouldSkipFacebookSentinelFromUidsCookie() throws JsonProcessingExce given(routingContext.cookieMap()).willReturn(singletonMap("uids", Cookie.cookie("uids", encodedUids))); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie).isNotNull(); @@ -386,7 +439,7 @@ public void shouldSkipFacebookSentinelFromUidsCookie() throws JsonProcessingExce } @Test - public void toCookieShouldReturnCookieWithExpectedValue() throws IOException { + public void aliveCookieShouldReturnCookieWithExpectedValue() { // given final UidsCookie uidsCookie = new UidsCookie( Uids.builder().uids(new HashMap<>()).build(), jacksonMapper) @@ -394,7 +447,7 @@ public void toCookieShouldReturnCookieWithExpectedValue() throws IOException { .updateUid(ADNXS, "adnxsUid"); // when - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then final Map uids = decodeUids(cookie.getValue()).getUids(); @@ -410,22 +463,46 @@ public void toCookieShouldReturnCookieWithExpectedValue() throws IOException { } @Test - public void toCookieShouldReturnCookieWithExpectedExpiration() { - // when + public void aliveCookieShouldReturnCookieWithExpectedExpiration() { + // given final UidsCookie uidsCookie = new UidsCookie( - Uids.builder().uids(new HashMap<>()).build(), jacksonMapper); - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + Uids.builder().uids(new HashMap<>()).build(), jacksonMapper) + .updateUid(RUBICON, "rubiconUid") + .updateUid(ADNXS, "adnxsUid"); + + // when + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then assertThat(cookie.encode()).containsSequence("Max-Age=7776000; Expires="); } @Test - public void toCookieShouldReturnCookieWithExpectedDomain() { + public void expiredCookieShouldReturnCookieWithZeroMaxAge() { + // when + final Cookie cookie = target.expiredCookie("uids"); + + // then + assertThat(cookie.encode()).containsSequence("Max-Age=0; Expires="); + } + + @Test + public void expiredCookieShouldReturnCookieWithEmptyValue() { // when + final Cookie cookie = target.expiredCookie("uids"); + + // then + assertThat(cookie.encode()).containsSequence("uids=;"); + } + + @Test + public void aliveCookieShouldReturnCookieWithExpectedDomain() { + // given final UidsCookie uidsCookie = new UidsCookie( Uids.builder().uids(new HashMap<>()).build(), jacksonMapper); - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + + // when + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then assertThat(cookie.getDomain()).isEqualTo(HOST_COOKIE_DOMAIN); @@ -434,7 +511,7 @@ public void toCookieShouldReturnCookieWithExpectedDomain() { @Test public void shouldParseHostCookie() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", null, @@ -442,12 +519,13 @@ public void shouldParseHostCookie() { "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); // when - final String hostCookie = uidsCookieService.parseHostCookie(singletonMap("khaos", "userId")); + final String hostCookie = target.parseHostCookie(singletonMap("khaos", "userId")); // then assertThat(hostCookie).isEqualTo("userId"); @@ -456,7 +534,7 @@ public void shouldParseHostCookie() { @Test public void shouldNotReadHostCookieIfNameNotSpecified() { // when - final String hostCookie = uidsCookieService.parseHostCookie(emptyMap()); + final String hostCookie = target.parseHostCookie(emptyMap()); // then verifyNoInteractions(routingContext); @@ -466,7 +544,7 @@ public void shouldNotReadHostCookieIfNameNotSpecified() { @Test public void shouldReturnNullIfHostCookieIsNotPresent() { // when - final String hostCookie = uidsCookieService.parseHostCookie(singletonMap("khaos", null)); + final String hostCookie = target.parseHostCookie(singletonMap("khaos", null)); // then assertThat(hostCookie).isNull(); @@ -475,7 +553,7 @@ public void shouldReturnNullIfHostCookieIsNotPresent() { @Test public void hostCookieUidToSyncShouldReturnNullWhenCookieFamilyNameDiffersFromHostCookieFamily() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -483,12 +561,13 @@ public void hostCookieUidToSyncShouldReturnNullWhenCookieFamilyNameDiffersFromHo "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); // when - final String result = uidsCookieService.hostCookieUidToSync(routingContext, "cookie-family"); + final String result = target.hostCookieUidToSync(routingContext, "cookie-family"); // then assertThat(result).isNull(); @@ -497,7 +576,7 @@ public void hostCookieUidToSyncShouldReturnNullWhenCookieFamilyNameDiffersFromHo @Test public void hostCookieUidToSyncShouldReturnHostCookieUidWhenHostCookieUidIsAbsent() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -505,6 +584,7 @@ public void hostCookieUidToSyncShouldReturnHostCookieUidWhenHostCookieUidIsAbsen "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -521,7 +601,7 @@ public void hostCookieUidToSyncShouldReturnHostCookieUidWhenHostCookieUidIsAbsen given(routingContext.cookieMap()).willReturn(cookieMap); // when - final String result = uidsCookieService.hostCookieUidToSync(routingContext, RUBICON); + final String result = target.hostCookieUidToSync(routingContext, RUBICON); // then assertThat(result).isEqualTo("hostCookieUid"); @@ -530,7 +610,7 @@ public void hostCookieUidToSyncShouldReturnHostCookieUidWhenHostCookieUidIsAbsen @Test public void hostCookieUidToSyncShouldReturnNullWhenUidsCookieHasNoUidForHostCookieFamily() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -538,6 +618,7 @@ public void hostCookieUidToSyncShouldReturnNullWhenUidsCookieHasNoUidForHostCook "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -545,7 +626,7 @@ public void hostCookieUidToSyncShouldReturnNullWhenUidsCookieHasNoUidForHostCook given(routingContext.cookieMap()).willReturn(emptyMap()); // when - final String result = uidsCookieService.hostCookieUidToSync(routingContext, RUBICON); + final String result = target.hostCookieUidToSync(routingContext, RUBICON); // then assertThat(result).isNull(); @@ -554,7 +635,7 @@ public void hostCookieUidToSyncShouldReturnNullWhenUidsCookieHasNoUidForHostCook @Test public void hostCookieUidToSyncShouldReturnNullWhenUidInUidsCookieSameAsUidInHostCookie() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -562,6 +643,7 @@ public void hostCookieUidToSyncShouldReturnNullWhenUidInUidsCookieSameAsUidInHos "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -578,7 +660,7 @@ public void hostCookieUidToSyncShouldReturnNullWhenUidInUidsCookieSameAsUidInHos given(routingContext.cookieMap()).willReturn(cookieMap); // when - final String result = uidsCookieService.hostCookieUidToSync(routingContext, RUBICON); + final String result = target.hostCookieUidToSync(routingContext, RUBICON); // then assertThat(result).isNull(); @@ -593,12 +675,13 @@ public void updateUidsCookieShouldRemoveAllExpiredUids() { "family3", UidWithExpiry.expired("uid3"))); // when - final UidsCookieUpdateResult result = uidsCookieService.updateUidsCookie(uidsCookie, "family4", "uid4"); + final UpdateResult result = target.updateUidsCookie(uidsCookie, "family4", "uid4"); + + // then + assertThat(result.isUpdated()).isTrue(); - // the - assertThat(result.isSuccessfullyUpdated()).isTrue(); - assertThat(result.getUidsCookie()) - .extracting(UidsCookie::getCookieUids) + final UidsCookie actualUidsCookies = result.getValue(); + assertThat(actualUidsCookies.getCookieUids()) .extracting(Uids::getUids) .extracting(Map::values) .extracting(ArrayList::new) @@ -609,63 +692,69 @@ public void updateUidsCookieShouldRemoveAllExpiredUids() { } @Test - public void updateUidsCookieShouldRemoveUidWhenBlank() { + public void updateUidsCookieShouldNotAddIncomingCookieFamilyWhenItHasBlankUid() { // given - final UidsCookie uidsCookie = givenUidsCookie(Map.of("family", UidWithExpiry.live("uid"))); + final UidsCookie uidsCookie = givenUidsCookie( + Map.of("family1", UidWithExpiry.expired("uid1"), + "family2", UidWithExpiry.live("uid2"), + "family3", UidWithExpiry.expired("uid3"))); // when - final UidsCookieUpdateResult result = uidsCookieService.updateUidsCookie(uidsCookie, "family", null); + final UpdateResult result = target.updateUidsCookie(uidsCookie, "family", null); // then - assertThat(result.isSuccessfullyUpdated()).isFalse(); - assertThat(result.getUidsCookie()) - .extracting(UidsCookie::getCookieUids) - .extracting(Uids::getUids) - .extracting(Map::keySet) - .extracting(ArrayList::new) - .asList() - .isEmpty(); + assertThat(result.isUpdated()).isFalse(); + assertThat(result.getValue().getCookieUids().getUids().keySet()).containsOnly("family2"); } @Test - public void updateUidsCookieShouldIgnoreFacebookSentinel() { + public void updateUidsCookieShouldNotAddIncomingCookieFamilyWhenItIsFacebookSentinel() { // given - final UidsCookie uidsCookie = givenUidsCookie(Map.of("family", UidWithExpiry.live("uid"))); + final UidsCookie uidsCookie = givenUidsCookie( + Map.of("family1", UidWithExpiry.expired("uid1"), + "family2", UidWithExpiry.live("uid2"), + "family3", UidWithExpiry.expired("uid3"))); // when - final UidsCookieUpdateResult result = uidsCookieService.updateUidsCookie( + final UpdateResult result = target.updateUidsCookie( uidsCookie, "audienceNetwork", "0"); // then - assertThat(result).isEqualTo(UidsCookieUpdateResult.unaltered(uidsCookie)); + assertThat(result.isUpdated()).isFalse(); + assertThat(result.getValue().getCookieUids().getUids().keySet()).containsOnly("family2"); } @Test - public void updateUidsCookieShouldUpdateCookieAndNotTrimIfSizeNotExceededLimit() { + public void updateUidsCookieShouldUpdateCookieAndNotSplitCookieWhenLimitIsNotExceeded() { // given + target = new UidsCookieService( + "trp_optout", + "true", + null, + null, + "cookie-domain", + 90, + MAX_COOKIE_SIZE_BYTES, + 2, + prioritizedCoopSyncProvider, + metrics, + jacksonMapper); + final UidsCookie uidsCookie = givenUidsCookie(Map.of("family", UidWithExpiry.live("uid"))); // when - final UidsCookieUpdateResult result = uidsCookieService.updateUidsCookie( + final UpdateResult result = target.updateUidsCookie( uidsCookie, "another-family", "uid"); // then - assertThat(result.isSuccessfullyUpdated()).isTrue(); - assertThat(result) - .extracting(UidsCookieUpdateResult::getUidsCookie) - .extracting(UidsCookie::getCookieUids) - .extracting(Uids::getUids) - .extracting(Map::keySet) - .extracting(ArrayList::new) - .asList() - .flatExtracting(identity()) - .containsExactly("family", "another-family"); + assertThat(result.isUpdated()).isTrue(); + assertThat(result.getValue().getCookieUids().getUids().keySet()).containsOnly("another-family", "family"); } @Test - public void updateUidsCookieShouldNotUpdateNonPrioritizedFamilyWhenSizeExceedsLimitAndLogMetric() { + public void updateUidsCookieShouldNotFitNonPrioritizedFamilyWhenSizeExceedsLimitAndLogMetric() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -673,30 +762,35 @@ public void updateUidsCookieShouldNotUpdateNonPrioritizedFamilyWhenSizeExceedsLi "cookie-domain", 90, 500, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); - given(prioritizedCoopSyncProvider.hasPrioritizedBidders()).willReturn(true); given(prioritizedCoopSyncProvider.isPrioritizedFamily("family")).willReturn(false); + given(prioritizedCoopSyncProvider.isPrioritizedFamily("very-very-very-very-long-family")).willReturn(true); + given(prioritizedCoopSyncProvider.isPrioritizedFamily("another-very-very-very-long-family")).willReturn(true); // cookie of encoded size 450 bytes final UidsCookie uidsCookie = givenUidsCookie(Map.of( "very-very-very-very-long-family", UidWithExpiry.live("some-very-very-very-long-uid"), - "another-very-very-very-long-family", UidWithExpiry.live("another-very-very-very-long-uid"))); + "another-very-very-very-long-family", UidWithExpiry.live("another-very-very-very-long-uid"), + "family", UidWithExpiry.live("uid"))); // when - final UidsCookieUpdateResult result = uidsCookieService.updateUidsCookie( - uidsCookie, "family", "uid"); + final List result = target.splitUidsIntoCookies(uidsCookie); // then verify(metrics).updateUserSyncSizeBlockedMetric("family"); - assertThat(result).isEqualTo(UidsCookieUpdateResult.unaltered(uidsCookie)); + + assertThat(result).hasSize(1).extracting(Cookie::getName).containsOnly("uids"); + assertThat(decodeUids(result.getFirst().getValue()).getUids().keySet()) + .containsExactly("very-very-very-very-long-family", "another-very-very-very-long-family"); } @Test - public void updateUidsCookieShouldUpdatePrioritizedFamilyWhenSizeExceedsLimitByTrimmingAndIncrementMetric() { + public void updateUidsCookieShouldNotFitPrioritizedFamilyWhenSizeExceedsLimitAndIncrementMetric() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -704,39 +798,33 @@ public void updateUidsCookieShouldUpdatePrioritizedFamilyWhenSizeExceedsLimitByT "cookie-domain", 90, 500, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); - given(prioritizedCoopSyncProvider.hasPrioritizedBidders()).willReturn(true); - given(prioritizedCoopSyncProvider.isPrioritizedFamily("family")).willReturn(true); + given(prioritizedCoopSyncProvider.isPrioritizedFamily(any())).willReturn(true); // cookie of encoded size 450 bytes final UidsCookie uidsCookie = givenUidsCookie(Map.of( "very-very-very-very-long-family", UidWithExpiry.live("some-very-very-very-long-uid"), - "another-very-very-very-long-family", UidWithExpiry.live("another-very-very-very-long-uid"))); + "another-very-very-very-long-family", UidWithExpiry.live("another-very-very-very-long-uid"), + "family", UidWithExpiry.live("uid"))); // when - final UidsCookieUpdateResult result = uidsCookieService.updateUidsCookie( - uidsCookie, "family", "uid"); + final List result = target.splitUidsIntoCookies(uidsCookie); // then - verify(metrics).updateUserSyncSizedOutMetric("very-very-very-very-long-family"); - assertThat(result.isSuccessfullyUpdated()).isTrue(); - assertThat(result) - .extracting(UidsCookieUpdateResult::getUidsCookie) - .extracting(UidsCookie::getCookieUids) - .extracting(Uids::getUids) - .extracting(Map::keySet) - .extracting(ArrayList::new) - .asList() - .flatExtracting(identity()) - .containsExactlyInAnyOrder("family", "another-very-very-very-long-family"); + verify(metrics).updateUserSyncSizedOutMetric("family"); + + assertThat(result).hasSize(1).extracting(Cookie::getName).containsOnly("uids"); + assertThat(decodeUids(result.getFirst().getValue()).getUids().keySet()) + .containsExactly("very-very-very-very-long-family", "another-very-very-very-long-family"); } @Test - public void updateUidsCookieShouldUpdateNonPrioritizedFamilyWhenSizeExceedsLimitAndPrioritiesAbsentByTrimming() { + public void updateUidsCookieShouldFitPrioritizedFamily() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -744,31 +832,117 @@ public void updateUidsCookieShouldUpdateNonPrioritizedFamilyWhenSizeExceedsLimit "cookie-domain", 90, 500, + 2, prioritizedCoopSyncProvider, metrics, jacksonMapper); - given(prioritizedCoopSyncProvider.hasPrioritizedBidders()).willReturn(false); + given(prioritizedCoopSyncProvider.isPrioritizedFamily(any())).willReturn(true); // cookie of encoded size 450 bytes final UidsCookie uidsCookie = givenUidsCookie(Map.of( "very-very-very-very-long-family", UidWithExpiry.live("some-very-very-very-long-uid"), - "another-very-very-very-long-family", UidWithExpiry.live("another-very-very-very-long-uid"))); + "another-very-very-very-long-family", UidWithExpiry.live("another-very-very-very-long-uid"), + "family", UidWithExpiry.live("uid"))); // when - final UidsCookieUpdateResult result = uidsCookieService.updateUidsCookie( - uidsCookie, "family", "uid"); + final List result = target.splitUidsIntoCookies(uidsCookie); // then - assertThat(result.isSuccessfullyUpdated()).isTrue(); - assertThat(result) - .extracting(UidsCookieUpdateResult::getUidsCookie) - .extracting(UidsCookie::getCookieUids) - .extracting(Uids::getUids) - .extracting(Map::keySet) - .extracting(ArrayList::new) - .asList() - .flatExtracting(identity()) - .containsExactlyInAnyOrder("family", "another-very-very-very-long-family"); + verifyNoInteractions(metrics); + + assertThat(result).hasSize(2).extracting(Cookie::getName).containsOnly("uids", "uids2"); + assertThat(decodeUids(result.getFirst().getValue()).getUids().keySet()) + .containsExactly("very-very-very-very-long-family", "another-very-very-very-long-family"); + assertThat(decodeUids(result.getLast().getValue()).getUids().keySet()) + .containsExactly("family"); + } + + @Test + public void updateUidsCookieShouldFitNonPrioritizedFamily() { + // given + target = new UidsCookieService( + "trp_optout", + "true", + RUBICON, + "khaos", + "cookie-domain", + 90, + 500, + 5, + prioritizedCoopSyncProvider, + metrics, + jacksonMapper); + given(prioritizedCoopSyncProvider.isPrioritizedFamily(any())).willReturn(false); + + // cookie of encoded size 450 bytes + final UidsCookie uidsCookie = givenUidsCookie(Map.of( + "very-very-very-very-long-family", UidWithExpiry.live("some-very-very-very-long-uid"), + "another-very-very-very-long-family", UidWithExpiry.live("another-very-very-very-long-uid"), + "family", UidWithExpiry.live("uid"))); + + // when + final List result = target.splitUidsIntoCookies(uidsCookie); + + // then + verifyNoInteractions(metrics); + + final Map actualCookies = result.stream() + .collect(Collectors.toMap(Cookie::getName, identity())); + + assertThat(actualCookies.keySet()).hasSize(5) + .containsOnly("uids", "uids2", "uids3", "uids4", "uids5"); + + assertThat(decodeUids(actualCookies.get("uids").getValue()).getUids().keySet()) + .containsOnly("very-very-very-very-long-family", "another-very-very-very-long-family"); + assertThat(actualCookies.get("uids").getMaxAge()).isEqualTo(7776000L); + + assertThat(decodeUids(actualCookies.get("uids2").getValue()).getUids().keySet()) + .containsOnly("family"); + assertThat(actualCookies.get("uids2").getMaxAge()).isEqualTo(7776000L); + + assertThat(actualCookies.get("uids3").getValue()).isEmpty(); + assertThat(actualCookies.get("uids3").getMaxAge()).isEqualTo(0); + assertThat(actualCookies.get("uids4").getValue()).isEmpty(); + assertThat(actualCookies.get("uids4").getMaxAge()).isEqualTo(0); + assertThat(actualCookies.get("uids5").getValue()).isEmpty(); + assertThat(actualCookies.get("uids5").getMaxAge()).isEqualTo(0); + } + + @Test + public void updateUidsCookieShouldDisallowSyncForAllCookiesWhenOptoutSetTrue() { + // given + target = new UidsCookieService( + "trp_optout", + "true", + RUBICON, + "khaos", + "cookie-domain", + 90, + 500, + 2, + prioritizedCoopSyncProvider, + metrics, + jacksonMapper); + given(prioritizedCoopSyncProvider.isPrioritizedFamily(any())).willReturn(false); + + // cookie of encoded size 450 bytes + final Map givenUids = Map.of( + "very-very-very-very-long-family", UidWithExpiry.live("some-very-very-very-long-uid"), + "another-very-very-very-long-family", UidWithExpiry.live("another-very-very-very-long-uid"), + "family", UidWithExpiry.live("uid")); + + final UidsCookie uidsCookie = new UidsCookie( + Uids.builder().uids(givenUids).optout(true).build(), jacksonMapper); + + // when + final List result = target.splitUidsIntoCookies(uidsCookie); + + // then + verifyNoInteractions(metrics); + + assertThat(result).hasSize(2).extracting(Cookie::getName).containsOnly("uids", "uids2"); + assertThat(decodeUids(result.getFirst().getValue()).getOptout()).isTrue(); + assertThat(decodeUids(result.getLast().getValue()).getOptout()).isTrue(); } private UidsCookie givenUidsCookie(Map uids) { @@ -779,7 +953,12 @@ private static String encodeUids(Uids uids) throws JsonProcessingException { return Base64.getUrlEncoder().encodeToString(mapper.writeValueAsBytes(uids)); } - private static Uids decodeUids(String value) throws IOException { - return mapper.readValue(Base64.getUrlDecoder().decode(value), Uids.class); + private static Uids decodeUids(String value) { + try { + return mapper.readValue(Base64.getUrlDecoder().decode(value), Uids.class); + } catch (IOException e) { + Assertions.fail(e.getMessage()); + throw new RuntimeException("Fail decoding cookie value"); + } } } diff --git a/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java b/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java index 9f0700dc192..b40da76c04b 100644 --- a/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java @@ -57,7 +57,7 @@ public void setUp() { given(googleRecaptchaVerifier.verify(anyString())).willReturn(Future.succeededFuture()); - given(uidsCookieService.toCookie(any())).willReturn(Cookie.cookie("cookie", "value")); + given(uidsCookieService.aliveCookie(any())).willReturn(Cookie.cookie("cookie", "value")); given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index 583c06e508b..e83b75b83d2 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -29,7 +29,6 @@ import org.prebid.server.cookie.UidsCookie; import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.cookie.model.UidWithExpiry; -import org.prebid.server.cookie.model.UidsCookieUpdateResult; import org.prebid.server.cookie.proto.Uids; import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; @@ -52,12 +51,15 @@ import java.time.Instant; import java.time.ZoneId; import java.util.Base64; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import static java.util.Collections.emptyMap; import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyInt; @@ -69,6 +71,8 @@ import static org.mockito.Mockito.anySet; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.prebid.server.model.UpdateResult.unaltered; +import static org.prebid.server.model.UpdateResult.updated; @ExtendWith(MockitoExtension.class) public class SetuidHandlerTest extends VertxTest { @@ -139,7 +143,11 @@ public void setUp() { given(httpResponse.putHeader(any(CharSequence.class), any(CharSequence.class))).willReturn(httpResponse); given(httpResponse.closed()).willReturn(false); - given(uidsCookieService.toCookie(any())).willReturn(Cookie.cookie("test", "test")); + given(uidsCookieService.splitUidsIntoCookies(any())).willAnswer(invocation -> singletonList( + Cookie.cookie( + "uids", + Base64.getUrlEncoder().encodeToString(((UidsCookie) invocation.getArgument(0)) + .toJson().getBytes())))); given(bidderCatalog.usersyncReadyBidders()).willReturn(Set.of(RUBICON, FACEBOOK, APPNEXUS)); given(bidderCatalog.isAlias(any())).willReturn(false); @@ -302,7 +310,7 @@ public void shouldPassUnsuccessfulEventToAnalyticsReporterIfUidMissingInRequest( given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(UidsCookieUpdateResult.unaltered(uidsCookie)); + .willReturn(unaltered(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -396,7 +404,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsFound() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(updated(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("account")).willReturn("accId"); @@ -425,7 +433,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsNotFound() given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(updated(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("account")).willReturn("accId"); @@ -446,12 +454,9 @@ public void shouldRespondWithCookieFromRequestParam() throws IOException { final UidsCookie uidsCookie = emptyUidsCookie(); given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); - given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) + .willReturn(updated(uidsCookie.updateUid(RUBICON, "J5VLCWQP-26-CWFT"))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); @@ -474,11 +479,7 @@ public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreD given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, ADNXS, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); - - // {"tempUIDs":{"adnxs":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiJKNVZMQ1dRUC0yNi1DV0ZUIn19fQ==")); + .willReturn(updated(uidsCookie.updateUid(ADNXS, "J5VLCWQP-26-CWFT"))); given(httpRequest.getParam("bidder")).willReturn(ADNXS); given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); @@ -500,11 +501,8 @@ public void shouldSendPixelWhenFParamIsEqualToIWhenTypeIsIframe() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .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(uidsCookieService.updateUidsCookie(any(), any(), any())) - .willReturn(UidsCookieUpdateResult.updated(emptyUidsCookie())); + .willReturn(updated(emptyUidsCookie())); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("f")).willReturn("i"); @@ -527,11 +525,7 @@ public void shouldSendEmptyResponseWhenFParamIsEqualToBWhenTypeIsRedirect() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); - - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + .willReturn(updated(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("f")).willReturn("b"); @@ -572,11 +566,7 @@ public void shouldSendEmptyResponseWhenFParamNotDefinedAndTypeIsIframe() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); - - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + .willReturn(updated(uidsCookie)); given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn( Optional.of(Usersyncer.of(RUBICON, iframeMethod(), null))); @@ -616,11 +606,8 @@ public void shouldSendPixelWhenFParamNotDefinedAndTypeIsRedirect() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(updated(uidsCookie)); - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(bidderCatalog.usersyncReadyBidders()).willReturn(singleton(RUBICON)); given(bidderCatalog.usersyncerByName(any())) @@ -659,17 +646,11 @@ public void shouldInCookieWithRequestValue() throws IOException { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie.updateUid(RUBICON, "updatedUid"))); + .willReturn(updated(uidsCookie.updateUid(RUBICON, "updatedUid"))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("updatedUid"); - // {"tempUIDs":{"adnxs":{"uid":"12345"}, "rubicon":{"uid":"updatedUid"}}} - given(uidsCookieService.toCookie(any())) - .willReturn(Cookie.cookie("uids", - "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9LCAicnViaWNvbiI6eyJ1aWQiOiJ1cGRhdGVkVW" - + "lkIn19fQ==")); - // when setuidHandler.handle(routingContext); @@ -684,6 +665,54 @@ public void shouldInCookieWithRequestValue() throws IOException { assertThat(decodedUids.getUids().get(ADNXS).getUid()).isEqualTo("12345"); } + @Test + public void shouldReturnMultipleCookies() throws IOException { + // given + final Map uids = Map.of( + RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"), + ADNXS, UidWithExpiry.live("12345")); + final UidsCookie uidsCookie = new UidsCookie(Uids.builder().uids(uids).build(), jacksonMapper); + + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) + .willReturn(uidsCookie); + final UidsCookie givenUidsCookie = uidsCookie + .updateUid(RUBICON, "updatedUid") + .updateUid(ADNXS, "12345"); + + given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) + .willReturn(updated(givenUidsCookie)); + + given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(httpRequest.getParam("uid")).willReturn("updatedUid"); + + // {"tempUIDs":{"adnxs":{"uid":"12345"}, "rubicon":{"uid":"updatedUid"}}} + given(uidsCookieService.splitUidsIntoCookies(givenUidsCookie)).willReturn(List.of( + Cookie.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6InVwZGF0ZWRVaWQifX19"), + Cookie.cookie("uids2", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9fX0"))); + + // when + setuidHandler.handle(routingContext); + + // then + verify(httpResponse).sendFile(any()); + verify(routingContext, never()).addCookie(any(Cookie.class)); + + final Map encodedUidsCookie = httpResponse.headers().getAll("Set-Cookie").stream() + .collect(Collectors.toMap(value -> value.split("=")[0], value -> value.split("=")[1])); + + assertThat(encodedUidsCookie).hasSize(2); + final Uids decodedUids1 = mapper.readValue(Base64.getUrlDecoder() + .decode(encodedUidsCookie.get("uids")), Uids.class); + final Uids decodedUids2 = mapper.readValue(Base64.getUrlDecoder() + .decode(encodedUidsCookie.get("uids2")), Uids.class); + + assertThat(decodedUids1.getUids()).hasSize(1); + assertThat(decodedUids1.getUids().get(RUBICON).getUid()).isEqualTo("updatedUid"); + + assertThat(decodedUids2.getUids()).hasSize(1); + assertThat(decodedUids2.getUids().get(ADNXS).getUid()).isEqualTo("12345"); + } + @Test public void shouldRespondWithCookieIfUserIsNotInGdprScope() throws IOException { // given @@ -694,11 +723,7 @@ public void shouldRespondWithCookieIfUserIsNotInGdprScope() throws IOException { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); - - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + .willReturn(updated(uidsCookie.updateUid(RUBICON, "J5VLCWQP-26-CWFT"))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); @@ -739,11 +764,7 @@ public void shouldSkipTcfChecksAndRespondWithCookieIfHostVendorIdNotDefined() th given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); - - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + .willReturn(updated(uidsCookie.updateUid(RUBICON, "J5VLCWQP-26-CWFT"))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); @@ -769,7 +790,7 @@ public void shouldNotSendResponseIfClientClosedConnection() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "uid")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(updated(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("uid"); @@ -793,7 +814,7 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(updated(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("updatedUid"); @@ -847,7 +868,13 @@ public void shouldThrowExceptionInCaseOfBaseBidderCookieFamilyNameDuplicates() { Assertions.assertThrows(IllegalArgumentException.class, exceptionSource); //then - assertThat(exception).hasMessage("Duplicated \"cookie-family-name\" found, values: audienceNetwork, rubicon"); + final String expectedPrefix = "Duplicated \"cookie-family-name\" found, values: "; + final String actualMessage = exception.getMessage(); + + assertThat(actualMessage).startsWith(expectedPrefix); + + final String[] values = actualMessage.substring(expectedPrefix.length()).split(", "); + assertThat(values).containsExactlyInAnyOrder("audienceNetwork", "rubicon"); } private String getUidsCookie() {