From 1efbc2e668917be46772d41e39997362dd8ad7de Mon Sep 17 00:00:00 2001 From: antonbabak Date: Thu, 9 Jan 2025 10:35:47 +0100 Subject: [PATCH 01/10] Multiple Uids Cookies Support --- .../cookie/PrioritizedCoopSyncProvider.java | 4 - .../server/cookie/UidsCookieService.java | 167 +++++--- .../cookie/model/UidsCookieUpdateResult.java | 12 +- .../prebid/server/handler/OptoutHandler.java | 13 +- .../prebid/server/handler/SetuidHandler.java | 10 +- .../spring/config/ServiceConfiguration.java | 2 + .../PrioritizedCoopSyncProviderTest.java | 21 - .../server/cookie/UidsCookieServiceTest.java | 396 ++++++++++++++---- .../server/handler/OptoutHandlerTest.java | 2 +- .../server/handler/SetuidHandlerTest.java | 93 +++- 10 files changed, 497 insertions(+), 223 deletions(-) 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/UidsCookieService.java b/src/main/java/org/prebid/server/cookie/UidsCookieService.java index 6b608f06307..78871c4e130 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookieService.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookieService.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.Base64; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Objects; @@ -34,7 +35,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 +46,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 +61,7 @@ public UidsCookieService(String optOutCookieName, String hostCookieDomain, int ttlDays, int maxCookieSizeBytes, + int numberOfUidCookies, PrioritizedCoopSyncProvider prioritizedCoopSyncProvider, Metrics metrics, JacksonMapper mapper) { @@ -64,6 +71,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 +84,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 +119,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,33 +132,39 @@ 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); - try { - return mapper.decodeValue(Buffer.buffer(Base64.getUrlDecoder().decode(cookieValue)), Uids.class); - } catch (IllegalArgumentException | DecodeException e) { - logger.debug("Could not decode or parse {} cookie value {}", e, COOKIE_NAME, cookieValue); + 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)) { + try { + 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, 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); - } - - private int cookieBytesLength(UidsCookie uidsCookie) { - return makeCookie(uidsCookie).encode().getBytes().length; + public Cookie toCookie(String cookieName, UidsCookie uidsCookie) { + return makeCookie(cookieName, uidsCookie); } - private Cookie makeCookie(UidsCookie uidsCookie) { + private Cookie makeCookie(String cookieName, UidsCookie uidsCookie) { return Cookie - .cookie(COOKIE_NAME, Base64.getUrlEncoder().encodeToString(uidsCookie.toJson().getBytes())) + .cookie(cookieName, Base64.getUrlEncoder().encodeToString(uidsCookie.toJson().getBytes())) .setPath("/") .setSameSite(CookieSameSite.NONE) .setSecure(true) @@ -221,20 +234,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); + 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 UidsCookieUpdateResult.failure(splitUids(initialCookie)); } - return updateUidsCookieByPriority(initialCookie, familyName, uid); + final UidsCookie updatedCookie = initialCookie.updateUid(familyName, uid); + return UidsCookieUpdateResult.success(splitUids(updatedCookie)); } private static UidsCookie removeExpiredUids(UidsCookie uidsCookie) { @@ -250,43 +261,53 @@ 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); - } - - if (!prioritizedCoopSyncProvider.hasPrioritizedBidders() - || prioritizedCoopSyncProvider.isPrioritizedFamily(familyName)) { - return UidsCookieUpdateResult.updated(trimToLimit(updatedCookie)); - } else { - metrics.updateUserSyncSizeBlockedMetric(familyName); - return UidsCookieUpdateResult.unaltered(uidsCookie); - } - } - - private boolean cookieExceededMaxLength(UidsCookie uidsCookie) { - return maxCookieSizeBytes > 0 && cookieBytesLength(uidsCookie) > maxCookieSizeBytes; - } + public Map splitUids(UidsCookie uidsCookie) { + final Uids cookieUids = uidsCookie.getCookieUids(); + final Map uids = cookieUids.getUids(); + final boolean hasOptout = !uidsCookie.allowsSync(); + + final Iterator cookieFamilyIterator = cookieFamilyNamesByDescPriorityAndExpiration(uidsCookie); + final Map splitCookies = new HashMap<>(); + + int uidsIndex = 0; + String nextCookieFamily = null; + + while (uidsIndex < numberOfUidCookies) { + final String uidsName = uidsIndex == 0 ? COOKIE_NAME : COOKIE_NAME_FORMAT.formatted(uidsIndex + 1); + final UidsCookie tempUidsCookie = splitCookies.computeIfAbsent( + uidsName, + key -> new UidsCookie(Uids.builder().uids(new HashMap<>()).optout(hasOptout).build(), mapper)); + final Map tempUids = tempUidsCookie.getCookieUids().getUids(); + + while (nextCookieFamily != null || cookieFamilyIterator.hasNext()) { + nextCookieFamily = nextCookieFamily == null ? cookieFamilyIterator.next() : nextCookieFamily; + tempUids.put(nextCookieFamily, uids.get(nextCookieFamily)); + if (cookieExceededMaxLength(uidsName, tempUidsCookie)) { + tempUids.remove(nextCookieFamily); + break; + } + + nextCookieFamily = null; + } - private UidsCookie trimToLimit(UidsCookie uidsCookie) { - if (!cookieExceededMaxLength(uidsCookie)) { - return uidsCookie; + uidsIndex++; } - UidsCookie trimmedUids = uidsCookie; - final Iterator familyToRemoveIterator = cookieFamilyNamesByAscendingPriority(uidsCookie); + while (nextCookieFamily != null || cookieFamilyIterator.hasNext()) { + nextCookieFamily = nextCookieFamily == null ? cookieFamilyIterator.next() : nextCookieFamily; + if (prioritizedCoopSyncProvider.isPrioritizedFamily(nextCookieFamily)) { + metrics.updateUserSyncSizedOutMetric(nextCookieFamily); + } else { + metrics.updateUserSyncSizeBlockedMetric(nextCookieFamily); + } - while (familyToRemoveIterator.hasNext() && cookieExceededMaxLength(trimmedUids)) { - final String familyToRemove = familyToRemoveIterator.next(); - metrics.updateUserSyncSizedOutMetric(familyToRemove); - trimmedUids = trimmedUids.deleteUid(familyToRemove); + nextCookieFamily = null; } - return trimmedUids; + 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) @@ -303,12 +324,20 @@ 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 boolean cookieExceededMaxLength(String name, UidsCookie uidsCookie) { + return maxCookieSizeBytes > 0 && cookieBytesLength(name, uidsCookie) > maxCookieSizeBytes; + } + + private int cookieBytesLength(String cookieName, UidsCookie uidsCookie) { + return makeCookie(cookieName, uidsCookie).encode().getBytes().length; + } + public String hostCookieUidToSync(RoutingContext routingContext, String cookieFamilyName) { if (!StringUtils.equals(cookieFamilyName, hostCookieFamily)) { return null; diff --git a/src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java b/src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java index 000b28c7018..cd504e7b923 100644 --- a/src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java +++ b/src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java @@ -3,18 +3,20 @@ import lombok.Value; import org.prebid.server.cookie.UidsCookie; +import java.util.Map; + @Value(staticConstructor = "of") public class UidsCookieUpdateResult { boolean successfullyUpdated; - UidsCookie uidsCookie; + Map uidsCookies; - public static UidsCookieUpdateResult updated(UidsCookie uidsCookie) { - return of(true, uidsCookie); + public static UidsCookieUpdateResult success(Map uidsCookies) { + return of(true, uidsCookies); } - public static UidsCookieUpdateResult unaltered(UidsCookie uidsCookie) { - return of(false, uidsCookie); + public static UidsCookieUpdateResult failure(Map uidsCookies) { + return of(false, uidsCookies); } } diff --git a/src/main/java/org/prebid/server/handler/OptoutHandler.java b/src/main/java/org/prebid/server/handler/OptoutHandler.java index 48551d37b03..7fcae2ee9e7 100644 --- a/src/main/java/org/prebid/server/handler/OptoutHandler.java +++ b/src/main/java/org/prebid/server/handler/OptoutHandler.java @@ -68,7 +68,7 @@ private void handleVerification(RoutingContext routingContext, AsyncResult cookies, String url) { HttpUtil.executeSafely(routingContext, Endpoint.optout, response -> response .setStatusCode(HttpResponseStatus.MOVED_PERMANENTLY.code()) .putHeader(HttpUtil.LOCATION_HEADER, url) - .putHeader(HttpUtil.SET_COOKIE_HEADER, cookie.encode()) + .putHeader(HttpUtil.SET_COOKIE_HEADER.toString(), cookies.stream().map(Cookie::encode).toList()) .end()); } @@ -102,11 +102,14 @@ private static boolean isOptout(RoutingContext routingContext) { return StringUtils.isNotEmpty(optoutValue); } - private Cookie optCookie(boolean optout, RoutingContext routingContext) { + private List optCookies(boolean optout, RoutingContext routingContext) { final UidsCookie uidsCookie = uidsCookieService .parseFromRequest(routingContext) .updateOptout(optout); - return uidsCookieService.toCookie(uidsCookie); + + return uidsCookieService.splitUids(uidsCookie).entrySet().stream() + .map(entry -> uidsCookieService.toCookie(entry.getKey(), entry.getValue())) + .toList(); } 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 1f6bed55e5f..86f0612ad26 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -310,10 +310,12 @@ 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 UidsCookieUpdateResult uidsCookieUpdateResult = uidsCookieService.updateUidsCookie( + setuidContext.getUidsCookie(), bidder, uid); + + uidsCookieUpdateResult.getUidsCookies().entrySet().stream() + .map(e -> uidsCookieService.toCookie(e.getKey(), e.getValue())) + .forEach(uidsCookie -> addCookie(routingContext, uidsCookie)); if (uidsCookieUpdateResult.isSuccessfullyUpdated()) { metrics.updateUserSyncSetsMetric(bidder); diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index deaa768320c..fd7b18280a4 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/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..b7ecdbb1eea 100644 --- a/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java +++ b/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java @@ -29,6 +29,7 @@ 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 +53,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 +65,7 @@ public void setUp() { "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -82,7 +84,7 @@ public void shouldReturnNonEmptyUidsCookieFromCookiesMap() { + "4xMDMzMjktMDM6MDAiIH0gfSB9"); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromCookies(cookies); + final UidsCookie uidsCookie = target.parseFromCookies(cookies); // then assertThat(uidsCookie).isNotNull(); @@ -90,6 +92,98 @@ public void shouldReturnNonEmptyUidsCookieFromCookiesMap() { assertThat(uidsCookie.uidFrom(ADNXS)).isEqualTo("12345"); } + @Test + public void shouldReturnNonEmptyUidsCookieFromCookiesMapWhenSeveralUidsCookiesArePresent() { + // given + final String uidsCookieValue = """ + { + "tempUIDs": { + "bidderA": { + "uid": "bidder-A-uid", + "expires": "2023-12-05T19:00:05.103329-03:00" + }, + "bidderB": { + "uid": "bidder-B-uid", + "expires": "2023-12-05T19:00:05.103329-03:00" + } + } + } + """; + + final String uids2CookieValue = """ + { + "tempUIDs": { + "bidderC": { + "uid": "bidder-C-uid", + "expires": "2023-12-05T19:00:05.103329-03:00" + }, + "bidderD": { + "uid": "bidder-D-uid", + "expires": "2023-12-05T19:00:05.103329-03:00" + } + } + } + """; + + final String encodedUidsCookie = Base64.getEncoder().encodeToString(uidsCookieValue.getBytes()); + final String encodedUids2Cookie = Base64.getEncoder().encodeToString(uids2CookieValue.getBytes()); + + final Map cookies = Map.of("uids", encodedUidsCookie, "uids2", encodedUids2Cookie); + + // 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 String uidsCookieValue = """ + { + "tempUIDs": { + "bidderA": { + "uid": "bidder-A1-uid", + "expires": "2023-12-05T19:00:05.103329-03:00" + }, + "bidderB": { + "uid": "bidder-B-uid", + "expires": "2023-12-05T19:00:05.103329-03:00" + } + } + } + """; + + final String uids2CookieValue = """ + { + "tempUIDs": { + "bidderA": { + "uid": "bidder-A2-uid", + "expires": "2024-12-05T19:00:05.103329-03:00" + } + } + } + """; + + final String encodedUidsCookie = Base64.getEncoder().encodeToString(uidsCookieValue.getBytes()); + final String encodedUids2Cookie = Base64.getEncoder().encodeToString(uids2CookieValue.getBytes()); + + final Map cookies = Map.of("uids", encodedUidsCookie, "uids2", encodedUids2Cookie); + + // 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 +198,7 @@ public void shouldReturnNonEmptyUidsCookie() { + "4xMDMzMjktMDM6MDAiIH0gfSB9"))); // when - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final UidsCookie uidsCookie = target.parseFromRequest(routingContext); // then assertThat(uidsCookie).isNotNull(); @@ -115,7 +209,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 +221,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 +234,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 +247,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 +271,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(); @@ -195,7 +289,7 @@ public void toCookieShouldSetSameSiteNone() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + final Cookie cookie = target.toCookie("uids", uidsCookie); // then assertThat(cookie.getSameSite()).isEqualTo(CookieSameSite.NONE); @@ -211,7 +305,7 @@ public void toCookieShouldSetSecure() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + final Cookie cookie = target.toCookie("uids", uidsCookie); // then assertThat(cookie.isSecure()).isTrue(); @@ -227,7 +321,7 @@ public void toCookieShouldSetPath() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + final Cookie cookie = target.toCookie("uids", uidsCookie); // then assertThat(cookie.getPath()).isEqualTo("/"); @@ -251,7 +345,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 +356,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieHasNotExpectedVal @Test public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieNameNotSpecified() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( null, "true", null, @@ -270,6 +364,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieNameNotSpecified( "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -277,7 +372,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 +381,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieNameNotSpecified( @Test public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieValueNotSpecified() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", null, null, @@ -294,6 +389,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieValueNotSpecified "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -301,7 +397,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 +406,7 @@ public void shouldReturnUidsCookieWithOptoutFalseIfOptoutCookieValueNotSpecified @Test public void shouldReturnRubiconCookieValueFromHostCookieWhenUidValueIsAbsent() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", "rubicon", @@ -318,13 +414,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 +430,7 @@ public void shouldReturnRubiconCookieValueFromHostCookieWhenUidValueIsAbsent() { @Test public void shouldReturnRubiconCookieValueFromHostCookieWhenUidValueIsPresentButDiffers() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", "rubicon", @@ -341,6 +438,7 @@ public void shouldReturnRubiconCookieValueFromHostCookieWhenUidValueIsPresentBut "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -359,7 +457,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 +475,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(); @@ -394,7 +492,7 @@ public void toCookieShouldReturnCookieWithExpectedValue() throws IOException { .updateUid(ADNXS, "adnxsUid"); // when - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + final Cookie cookie = target.toCookie("uids", uidsCookie); // then final Map uids = decodeUids(cookie.getValue()).getUids(); @@ -414,7 +512,7 @@ public void toCookieShouldReturnCookieWithExpectedExpiration() { // when final UidsCookie uidsCookie = new UidsCookie( Uids.builder().uids(new HashMap<>()).build(), jacksonMapper); - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + final Cookie cookie = target.toCookie("uids", uidsCookie); // then assertThat(cookie.encode()).containsSequence("Max-Age=7776000; Expires="); @@ -425,7 +523,7 @@ public void toCookieShouldReturnCookieWithExpectedDomain() { // when final UidsCookie uidsCookie = new UidsCookie( Uids.builder().uids(new HashMap<>()).build(), jacksonMapper); - final Cookie cookie = uidsCookieService.toCookie(uidsCookie); + final Cookie cookie = target.toCookie("uids", uidsCookie); // then assertThat(cookie.getDomain()).isEqualTo(HOST_COOKIE_DOMAIN); @@ -434,7 +532,7 @@ public void toCookieShouldReturnCookieWithExpectedDomain() { @Test public void shouldParseHostCookie() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", null, @@ -442,12 +540,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 +555,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 +565,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 +574,7 @@ public void shouldReturnNullIfHostCookieIsNotPresent() { @Test public void hostCookieUidToSyncShouldReturnNullWhenCookieFamilyNameDiffersFromHostCookieFamily() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -483,12 +582,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 +597,7 @@ public void hostCookieUidToSyncShouldReturnNullWhenCookieFamilyNameDiffersFromHo @Test public void hostCookieUidToSyncShouldReturnHostCookieUidWhenHostCookieUidIsAbsent() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -505,6 +605,7 @@ public void hostCookieUidToSyncShouldReturnHostCookieUidWhenHostCookieUidIsAbsen "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -521,7 +622,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 +631,7 @@ public void hostCookieUidToSyncShouldReturnHostCookieUidWhenHostCookieUidIsAbsen @Test public void hostCookieUidToSyncShouldReturnNullWhenUidsCookieHasNoUidForHostCookieFamily() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -538,6 +639,7 @@ public void hostCookieUidToSyncShouldReturnNullWhenUidsCookieHasNoUidForHostCook "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -545,7 +647,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 +656,7 @@ public void hostCookieUidToSyncShouldReturnNullWhenUidsCookieHasNoUidForHostCook @Test public void hostCookieUidToSyncShouldReturnNullWhenUidInUidsCookieSameAsUidInHostCookie() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -562,6 +664,7 @@ public void hostCookieUidToSyncShouldReturnNullWhenUidInUidsCookieSameAsUidInHos "cookie-domain", 90, MAX_COOKIE_SIZE_BYTES, + 1, prioritizedCoopSyncProvider, metrics, jacksonMapper); @@ -578,7 +681,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,11 +696,14 @@ public void updateUidsCookieShouldRemoveAllExpiredUids() { "family3", UidWithExpiry.expired("uid3"))); // when - final UidsCookieUpdateResult result = uidsCookieService.updateUidsCookie(uidsCookie, "family4", "uid4"); + final UidsCookieUpdateResult result = target.updateUidsCookie(uidsCookie, "family4", "uid4"); - // the + // then assertThat(result.isSuccessfullyUpdated()).isTrue(); - assertThat(result.getUidsCookie()) + + final Map actualUidsCookies = result.getUidsCookies(); + assertThat(actualUidsCookies.keySet()).containsOnly("uids"); + assertThat(actualUidsCookies.get("uids")) .extracting(UidsCookie::getCookieUids) .extracting(Uids::getUids) .extracting(Map::values) @@ -609,63 +715,73 @@ 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 UidsCookieUpdateResult 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.getUidsCookies().get("uids").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 UidsCookieUpdateResult result = target.updateUidsCookie( uidsCookie, "audienceNetwork", "0"); // then - assertThat(result).isEqualTo(UidsCookieUpdateResult.unaltered(uidsCookie)); + assertThat(result.isSuccessfullyUpdated()).isFalse(); + assertThat(result.getUidsCookies().get("uids").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 UidsCookieUpdateResult 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"); + + final Map actualUidsCookies = result.getUidsCookies(); + assertThat(actualUidsCookies.keySet()).containsOnly("uids", "uids2"); + assertThat(actualUidsCookies.get("uids").getCookieUids().getUids().keySet()) + .containsExactly("another-family", "family"); } @Test - public void updateUidsCookieShouldNotUpdateNonPrioritizedFamilyWhenSizeExceedsLimitAndLogMetric() { + public void updateUidsCookieShouldNotFitNonPrioritizedFamilyWhenSizeExceedsLimitAndLogMetric() { // given - uidsCookieService = new UidsCookieService( + target = new UidsCookieService( "trp_optout", "true", RUBICON, @@ -673,11 +789,13 @@ 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( @@ -685,18 +803,23 @@ public void updateUidsCookieShouldNotUpdateNonPrioritizedFamilyWhenSizeExceedsLi "another-very-very-very-long-family", UidWithExpiry.live("another-very-very-very-long-uid"))); // when - final UidsCookieUpdateResult result = uidsCookieService.updateUidsCookie( + final UidsCookieUpdateResult result = target.updateUidsCookie( uidsCookie, "family", "uid"); // then verify(metrics).updateUserSyncSizeBlockedMetric("family"); - assertThat(result).isEqualTo(UidsCookieUpdateResult.unaltered(uidsCookie)); + assertThat(result.isSuccessfullyUpdated()).isTrue(); + + final Map actualUidsCookies = result.getUidsCookies(); + assertThat(actualUidsCookies.keySet()).containsOnly("uids"); + assertThat(actualUidsCookies.get("uids").getCookieUids().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,11 +827,11 @@ 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( @@ -716,27 +839,23 @@ public void updateUidsCookieShouldUpdatePrioritizedFamilyWhenSizeExceedsLimitByT "another-very-very-very-long-family", UidWithExpiry.live("another-very-very-very-long-uid"))); // when - final UidsCookieUpdateResult result = uidsCookieService.updateUidsCookie( + final UidsCookieUpdateResult result = target.updateUidsCookie( uidsCookie, "family", "uid"); // then - verify(metrics).updateUserSyncSizedOutMetric("very-very-very-very-long-family"); + verify(metrics).updateUserSyncSizedOutMetric("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"); + + final Map actualUidsCookies = result.getUidsCookies(); + assertThat(actualUidsCookies.keySet()).containsOnly("uids"); + assertThat(actualUidsCookies.get("uids").getCookieUids().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,10 +863,11 @@ 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( @@ -755,20 +875,112 @@ public void updateUidsCookieShouldUpdateNonPrioritizedFamilyWhenSizeExceedsLimit "another-very-very-very-long-family", UidWithExpiry.live("another-very-very-very-long-uid"))); // when - final UidsCookieUpdateResult result = uidsCookieService.updateUidsCookie( + final UidsCookieUpdateResult result = target.updateUidsCookie( uidsCookie, "family", "uid"); // then + verifyNoInteractions(metrics); assertThat(result.isSuccessfullyUpdated()).isTrue(); - assertThat(result) - .extracting(UidsCookieUpdateResult::getUidsCookie) + + final Map actualUidsCookies = result.getUidsCookies(); + assertThat(actualUidsCookies.keySet()).containsOnly("uids", "uids2"); + assertThat(actualUidsCookies.get("uids")) .extracting(UidsCookie::getCookieUids) .extracting(Uids::getUids) .extracting(Map::keySet) .extracting(ArrayList::new) .asList() .flatExtracting(identity()) - .containsExactlyInAnyOrder("family", "another-very-very-very-long-family"); + .containsExactly("very-very-very-very-long-family", "another-very-very-very-long-family"); + + assertThat(actualUidsCookies.get("uids2")) + .extracting(UidsCookie::getCookieUids) + .extracting(Uids::getUids) + .extracting(Map::keySet) + .extracting(ArrayList::new) + .asList() + .flatExtracting(identity()) + .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"))); + + // when + final UidsCookieUpdateResult result = target.updateUidsCookie( + uidsCookie, "family", "uid"); + + // then + verifyNoInteractions(metrics); + assertThat(result.isSuccessfullyUpdated()).isTrue(); + + final Map actualUidsCookies = result.getUidsCookies(); + assertThat(actualUidsCookies.keySet()).containsOnly("uids", "uids2", "uids3", "uids4", "uids5"); + assertThat(actualUidsCookies.get("uids").getCookieUids().getUids().keySet()) + .containsExactly("very-very-very-very-long-family", "another-very-very-very-long-family"); + assertThat(actualUidsCookies.get("uids2").getCookieUids().getUids().keySet()) + .containsOnly("family"); + assertThat(actualUidsCookies.get("uids3").getCookieUids().getUids()).isEmpty(); + assertThat(actualUidsCookies.get("uids4").getCookieUids().getUids()).isEmpty(); + assertThat(actualUidsCookies.get("uids5").getCookieUids().getUids()).isEmpty(); + } + + @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")); + + final UidsCookie uidsCookie = new UidsCookie( + Uids.builder().uids(givenUids).optout(true).build(), jacksonMapper); + + // when + final UidsCookieUpdateResult result = target.updateUidsCookie( + uidsCookie, "family", "uid"); + + // then + verifyNoInteractions(metrics); + assertThat(result.isSuccessfullyUpdated()).isTrue(); + + final Map actualUidsCookies = result.getUidsCookies(); + assertThat(actualUidsCookies.keySet()).containsOnly("uids", "uids2"); + assertThat(actualUidsCookies.get("uids").allowsSync()).isFalse(); + assertThat(actualUidsCookies.get("uids2").allowsSync()).isFalse(); } private UidsCookie givenUidsCookie(Map uids) { diff --git a/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java b/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java index 9f0700dc192..fb8676ec5cd 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.toCookie(any(), 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 8cb03cda9b5..def6761261f 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -55,6 +55,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import static java.util.Arrays.asList; import static java.util.Collections.emptyMap; @@ -138,7 +139,7 @@ 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.toCookie(any(), any())).willReturn(Cookie.cookie("test", "test")); given(bidderCatalog.names()).willReturn(new HashSet<>(asList("rubicon", "audienceNetwork"))); given(bidderCatalog.isActive(any())).willReturn(true); @@ -294,7 +295,7 @@ public void shouldPassUnsuccessfulEventToAnalyticsReporterIfUidMissingInRequest( given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(UidsCookieUpdateResult.unaltered(uidsCookie)); + .willReturn(UidsCookieUpdateResult.failure(Map.of("uids", uidsCookie))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -388,7 +389,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsFound() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("account")).willReturn("accId"); @@ -417,7 +418,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsNotFound() given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("account")).willReturn("accId"); @@ -439,10 +440,10 @@ public void shouldRespondWithCookieFromRequestParam() throws IOException { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie + given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -466,10 +467,10 @@ public void shouldSendPixelWhenFParamIsEqualToIWhenTypeIsIframe() { .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie + given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(uidsCookieService.updateUidsCookie(any(), any(), any())) - .willReturn(UidsCookieUpdateResult.updated(emptyUidsCookie())); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", emptyUidsCookie()))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("f")).willReturn("i"); @@ -492,10 +493,10 @@ public void shouldSendEmptyResponseWhenFParamIsEqualToBWhenTypeIsRedirect() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie + given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -537,10 +538,10 @@ public void shouldSendEmptyResponseWhenFParamNotDefinedAndTypeIsIframe() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie + given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn( @@ -581,10 +582,10 @@ public void shouldSendPixelWhenFParamNotDefinedAndTypeIsRedirect() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie + given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(bidderCatalog.names()).willReturn(singleton(RUBICON)); @@ -624,13 +625,14 @@ 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(UidsCookieUpdateResult.success( + Map.of("uids", 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())) + given(uidsCookieService.toCookie(any(), any())) .willReturn(Cookie.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9LCAicnViaWNvbiI6eyJ1aWQiOiJ1cGRhdGVkVW" + "lkIn19fQ==")); @@ -649,6 +651,53 @@ 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); + given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) + .willReturn(UidsCookieUpdateResult.success(Map.of( + "uids", uidsCookie.updateUid(RUBICON, "updatedUid"), + "uids2", uidsCookie.updateUid(ADNXS, "12345")))); + + given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(httpRequest.getParam("uid")).willReturn("updatedUid"); + + // {"tempUIDs":{"adnxs":{"uid":"12345"}, "rubicon":{"uid":"updatedUid"}}} + given(uidsCookieService.toCookie(eq("uids"), any())) + .willReturn(Cookie.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6InVwZGF0ZWRVaWQifX19")); + given(uidsCookieService.toCookie(eq("uids2"), any())) + .willReturn(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 @@ -659,10 +708,10 @@ 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)); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie + given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -704,10 +753,10 @@ public void shouldSkipTcfChecksAndRespondWithCookieIfHostVendorIdNotDefined() th given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie + given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -734,7 +783,7 @@ public void shouldNotSendResponseIfClientClosedConnection() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "uid")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("uid"); @@ -758,7 +807,7 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("updatedUid"); From b14ee5a64d67460e2a5028e49454aacf484c926f Mon Sep 17 00:00:00 2001 From: antonbabak Date: Mon, 20 Jan 2025 13:39:48 +0100 Subject: [PATCH 02/10] Fix compilation error --- .../java/org/prebid/server/handler/SetuidHandlerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index 198bc5fcf6d..7ec41e255cc 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -475,10 +475,10 @@ public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreD given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, ADNXS, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.updated(uidsCookie)); + .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); // {"tempUIDs":{"adnxs":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any())).willReturn(Cookie + given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiJKNVZMQ1dRUC0yNi1DV0ZUIn19fQ==")); given(httpRequest.getParam("bidder")).willReturn(ADNXS); From 917a57ad806b60b5a2663d782dbfc370a0d67bed Mon Sep 17 00:00:00 2001 From: antonbabak Date: Tue, 21 Jan 2025 15:24:26 +0100 Subject: [PATCH 03/10] Removing cookie when uids are empty --- .../server/cookie/UidsCookieService.java | 16 ++- .../prebid/server/handler/OptoutHandler.java | 2 +- .../prebid/server/handler/SetuidHandler.java | 8 +- .../server/cookie/UidsCookieServiceTest.java | 52 +++++++--- .../server/handler/OptoutHandlerTest.java | 2 +- .../server/handler/SetuidHandlerTest.java | 99 +++++++++++++------ 6 files changed, 128 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/prebid/server/cookie/UidsCookieService.java b/src/main/java/org/prebid/server/cookie/UidsCookieService.java index 78871c4e130..3e2fe30d1ee 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookieService.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookieService.java @@ -158,11 +158,7 @@ private Uids parseUids(Map cookies) { * Creates a {@link Cookie} with 'uids' as a name and encoded JSON string representing supplied {@link UidsCookie} * as a value. */ - public Cookie toCookie(String cookieName, UidsCookie uidsCookie) { - return makeCookie(cookieName, uidsCookie); - } - - private Cookie makeCookie(String cookieName, UidsCookie uidsCookie) { + public Cookie makeCookie(String cookieName, UidsCookie uidsCookie) { return Cookie .cookie(cookieName, Base64.getUrlEncoder().encodeToString(uidsCookie.toJson().getBytes())) .setPath("/") @@ -172,6 +168,16 @@ private Cookie makeCookie(String cookieName, UidsCookie uidsCookie) { .setDomain(hostCookieDomain); } + public Cookie removeCookie(String cookieName) { + return Cookie + .cookie(cookieName, StringUtils.EMPTY) + .setPath("/") + .setSameSite(CookieSameSite.NONE) + .setSecure(true) + .setMaxAge(0) + .setDomain(hostCookieDomain); + } + /** * Lookups host cookie value from cookies map by configured host cookie name. */ diff --git a/src/main/java/org/prebid/server/handler/OptoutHandler.java b/src/main/java/org/prebid/server/handler/OptoutHandler.java index 7fcae2ee9e7..38c93d68174 100644 --- a/src/main/java/org/prebid/server/handler/OptoutHandler.java +++ b/src/main/java/org/prebid/server/handler/OptoutHandler.java @@ -108,7 +108,7 @@ private List optCookies(boolean optout, RoutingContext routingContext) { .updateOptout(optout); return uidsCookieService.splitUids(uidsCookie).entrySet().stream() - .map(entry -> uidsCookieService.toCookie(entry.getKey(), entry.getValue())) + .map(entry -> uidsCookieService.makeCookie(entry.getKey(), entry.getValue())) .toList(); } diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index 93713272274..8ce32ea0db8 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -332,7 +332,7 @@ private void respondWithCookie(SetuidContext setuidContext) { setuidContext.getUidsCookie(), bidder, uid); uidsCookieUpdateResult.getUidsCookies().entrySet().stream() - .map(e -> uidsCookieService.toCookie(e.getKey(), e.getValue())) + .map(entry -> toCookie(entry.getKey(), entry.getValue())) .forEach(uidsCookie -> addCookie(routingContext, uidsCookie)); if (uidsCookieUpdateResult.isSuccessfullyUpdated()) { @@ -351,6 +351,12 @@ private void respondWithCookie(SetuidContext setuidContext) { analyticsDelegator.processEvent(setuidEvent, tcfContext); } + private Cookie toCookie(String cookieName, UidsCookie uidsCookie) { + return uidsCookie.getCookieUids().getUids().isEmpty() + ? uidsCookieService.removeCookie(cookieName) + : uidsCookieService.makeCookie(cookieName, uidsCookie); + } + private Consumer buildCookieResponseConsumer(SetuidContext setuidContext, int responseStatusCode) { diff --git a/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java b/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java index b7ecdbb1eea..ec97ddf4598 100644 --- a/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java +++ b/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java @@ -280,7 +280,7 @@ public void shouldReturnUidsCookieWithOptoutTrueIfUidsCookieIsPresentAndOptoutCo } @Test - public void toCookieShouldSetSameSiteNone() { + public void makeCookieShouldSetSameSiteNone() { // given final Uids uids = Uids.builder() .uids(Map.of(RUBICON, UidWithExpiry.live("test"))) @@ -289,14 +289,14 @@ public void toCookieShouldSetSameSiteNone() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = target.toCookie("uids", uidsCookie); + final Cookie cookie = target.makeCookie("uids", uidsCookie); // then assertThat(cookie.getSameSite()).isEqualTo(CookieSameSite.NONE); } @Test - public void toCookieShouldSetSecure() { + public void makeCookieShouldSetSecure() { // given final Uids uids = Uids.builder() .uids(Map.of(RUBICON, UidWithExpiry.live("test"))) @@ -305,14 +305,14 @@ public void toCookieShouldSetSecure() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = target.toCookie("uids", uidsCookie); + final Cookie cookie = target.makeCookie("uids", uidsCookie); // then assertThat(cookie.isSecure()).isTrue(); } @Test - public void toCookieShouldSetPath() { + public void makeCookieShouldSetPath() { // given final Uids uids = Uids.builder() .uids(Map.of(RUBICON, UidWithExpiry.live("test"))) @@ -321,7 +321,7 @@ public void toCookieShouldSetPath() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = target.toCookie("uids", uidsCookie); + final Cookie cookie = target.makeCookie("uids", uidsCookie); // then assertThat(cookie.getPath()).isEqualTo("/"); @@ -484,7 +484,7 @@ public void shouldSkipFacebookSentinelFromUidsCookie() throws JsonProcessingExce } @Test - public void toCookieShouldReturnCookieWithExpectedValue() throws IOException { + public void makeCookieShouldReturnCookieWithExpectedValue() throws IOException { // given final UidsCookie uidsCookie = new UidsCookie( Uids.builder().uids(new HashMap<>()).build(), jacksonMapper) @@ -492,7 +492,7 @@ public void toCookieShouldReturnCookieWithExpectedValue() throws IOException { .updateUid(ADNXS, "adnxsUid"); // when - final Cookie cookie = target.toCookie("uids", uidsCookie); + final Cookie cookie = target.makeCookie("uids", uidsCookie); // then final Map uids = decodeUids(cookie.getValue()).getUids(); @@ -508,22 +508,46 @@ public void toCookieShouldReturnCookieWithExpectedValue() throws IOException { } @Test - public void toCookieShouldReturnCookieWithExpectedExpiration() { - // when + public void makeCookieShouldReturnCookieWithExpectedExpiration() { + // given final UidsCookie uidsCookie = new UidsCookie( - Uids.builder().uids(new HashMap<>()).build(), jacksonMapper); - final Cookie cookie = target.toCookie("uids", uidsCookie); + Uids.builder().uids(new HashMap<>()).build(), jacksonMapper) + .updateUid(RUBICON, "rubiconUid") + .updateUid(ADNXS, "adnxsUid"); + + // when + final Cookie cookie = target.makeCookie("uids", uidsCookie); // then assertThat(cookie.encode()).containsSequence("Max-Age=7776000; Expires="); } @Test - public void toCookieShouldReturnCookieWithExpectedDomain() { + public void removeCookieShouldReturnCookieWithZeroMaxAge() { + // when + final Cookie cookie = target.removeCookie("uids"); + + // then + assertThat(cookie.encode()).containsSequence("Max-Age=0; Expires="); + } + + @Test + public void removeCookieShouldReturnCookieWithEmptyValue() { // when + final Cookie cookie = target.removeCookie("uids"); + + // then + assertThat(cookie.encode()).containsSequence("uids=;"); + } + + @Test + public void makeCookieShouldReturnCookieWithExpectedDomain() { + // given final UidsCookie uidsCookie = new UidsCookie( Uids.builder().uids(new HashMap<>()).build(), jacksonMapper); - final Cookie cookie = target.toCookie("uids", uidsCookie); + + // when + final Cookie cookie = target.makeCookie("uids", uidsCookie); // then assertThat(cookie.getDomain()).isEqualTo(HOST_COOKIE_DOMAIN); diff --git a/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java b/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java index fb8676ec5cd..a11d24b0133 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(), any())).willReturn(Cookie.cookie("cookie", "value")); + given(uidsCookieService.makeCookie(any(), 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 7ec41e255cc..925fe0dc308 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -7,6 +7,7 @@ import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,7 +30,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; @@ -70,6 +70,8 @@ import static org.mockito.Mockito.anySet; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.prebid.server.cookie.model.UidsCookieUpdateResult.failure; +import static org.prebid.server.cookie.model.UidsCookieUpdateResult.success; @ExtendWith(MockitoExtension.class) public class SetuidHandlerTest extends VertxTest { @@ -140,7 +142,9 @@ public void setUp() { given(httpResponse.putHeader(any(CharSequence.class), any(CharSequence.class))).willReturn(httpResponse); given(httpResponse.closed()).willReturn(false); - given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie.cookie("test", "test")); + given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie.cookie("test", "test")); + given(uidsCookieService.removeCookie(any())) + .willAnswer(invocation -> Cookie.cookie(invocation.getArgument(0), StringUtils.EMPTY)); given(bidderCatalog.usersyncReadyBidders()).willReturn(Set.of(RUBICON, FACEBOOK, APPNEXUS)); given(bidderCatalog.isAlias(any())).willReturn(false); @@ -303,7 +307,7 @@ public void shouldPassUnsuccessfulEventToAnalyticsReporterIfUidMissingInRequest( given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(UidsCookieUpdateResult.failure(Map.of("uids", uidsCookie))); + .willReturn(failure(Map.of("uids", uidsCookie))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -397,7 +401,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsFound() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); + .willReturn(success(Map.of("uids", uidsCookie))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("account")).willReturn("accId"); @@ -426,7 +430,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsNotFound() given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); + .willReturn(success(Map.of("uids", uidsCookie))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("account")).willReturn("accId"); @@ -447,11 +451,12 @@ 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.success(Map.of("uids", uidsCookie))); + .willReturn(success(Map.of("uids", uidsCookie.updateUid(RUBICON, "J5VLCWQP-26-CWFT")))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie + given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -475,10 +480,10 @@ public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreD given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, ADNXS, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); + .willReturn(success(Map.of("uids", uidsCookie.updateUid(ADNXS, "J5VLCWQP-26-CWFT")))); // {"tempUIDs":{"adnxs":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie + given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiJKNVZMQ1dRUC0yNi1DV0ZUIn19fQ==")); given(httpRequest.getParam("bidder")).willReturn(ADNXS); @@ -502,10 +507,10 @@ public void shouldSendPixelWhenFParamIsEqualToIWhenTypeIsIframe() { .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie + given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(uidsCookieService.updateUidsCookie(any(), any(), any())) - .willReturn(UidsCookieUpdateResult.success(Map.of("uids", emptyUidsCookie()))); + .willReturn(success(Map.of("uids", emptyUidsCookie()))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("f")).willReturn("i"); @@ -528,10 +533,10 @@ public void shouldSendEmptyResponseWhenFParamIsEqualToBWhenTypeIsRedirect() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); + .willReturn(success(Map.of("uids", uidsCookie))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie + given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -573,10 +578,10 @@ public void shouldSendEmptyResponseWhenFParamNotDefinedAndTypeIsIframe() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); + .willReturn(success(Map.of("uids", uidsCookie))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie + given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn( @@ -617,10 +622,10 @@ public void shouldSendPixelWhenFParamNotDefinedAndTypeIsRedirect() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); + .willReturn(success(Map.of("uids", uidsCookie))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie + given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(bidderCatalog.usersyncReadyBidders()).willReturn(singleton(RUBICON)); @@ -660,14 +665,13 @@ public void shouldInCookieWithRequestValue() throws IOException { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) - .willReturn(UidsCookieUpdateResult.success( - Map.of("uids", uidsCookie.updateUid(RUBICON, "updatedUid")))); + .willReturn(success(Map.of("uids", 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(), any())) + given(uidsCookieService.makeCookie(any(), any())) .willReturn(Cookie.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9LCAicnViaWNvbiI6eyJ1aWQiOiJ1cGRhdGVkVW" + "lkIn19fQ==")); @@ -697,7 +701,7 @@ public void shouldReturnMultipleCookies() throws IOException { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) - .willReturn(UidsCookieUpdateResult.success(Map.of( + .willReturn(success(Map.of( "uids", uidsCookie.updateUid(RUBICON, "updatedUid"), "uids2", uidsCookie.updateUid(ADNXS, "12345")))); @@ -705,9 +709,9 @@ public void shouldReturnMultipleCookies() throws IOException { given(httpRequest.getParam("uid")).willReturn("updatedUid"); // {"tempUIDs":{"adnxs":{"uid":"12345"}, "rubicon":{"uid":"updatedUid"}}} - given(uidsCookieService.toCookie(eq("uids"), any())) + given(uidsCookieService.makeCookie(eq("uids"), any())) .willReturn(Cookie.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6InVwZGF0ZWRVaWQifX19")); - given(uidsCookieService.toCookie(eq("uids2"), any())) + given(uidsCookieService.makeCookie(eq("uids2"), any())) .willReturn(Cookie.cookie("uids2", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9fX0")); // when @@ -733,6 +737,43 @@ public void shouldReturnMultipleCookies() throws IOException { assertThat(decodedUids2.getUids().get(ADNXS).getUid()).isEqualTo("12345"); } + @Test + public void shouldCleanUpEmptyUidsCookies() { + // 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); + given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) + .willReturn(success(Map.of( + "uids", uidsCookie.updateUid(RUBICON, "updatedUid"), + "uids2", uidsCookie.updateUid(ADNXS, "12345"), + "uids3", emptyUidsCookie()))); + + given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(httpRequest.getParam("uid")).willReturn("updatedUid"); + + // {"tempUIDs":{"adnxs":{"uid":"12345"}, "rubicon":{"uid":"updatedUid"}}} + given(uidsCookieService.makeCookie(eq("uids"), any())) + .willReturn(Cookie.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6InVwZGF0ZWRVaWQifX19")); + given(uidsCookieService.makeCookie(eq("uids2"), any())) + .willReturn(Cookie.cookie("uids2", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9fX0")); + + // when + setuidHandler.handle(routingContext); + + // then + verify(httpResponse).sendFile(any()); + verify(routingContext, never()).addCookie(any(Cookie.class)); + + verify(uidsCookieService).makeCookie(eq("uids"), any()); + verify(uidsCookieService).makeCookie(eq("uids2"), any()); + verify(uidsCookieService).removeCookie("uids3"); + } + @Test public void shouldRespondWithCookieIfUserIsNotInGdprScope() throws IOException { // given @@ -743,10 +784,10 @@ public void shouldRespondWithCookieIfUserIsNotInGdprScope() throws IOException { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); + .willReturn(success(Map.of("uids", uidsCookie.updateUid(RUBICON, "J5VLCWQP-26-CWFT")))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie + given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -788,10 +829,10 @@ public void shouldSkipTcfChecksAndRespondWithCookieIfHostVendorIdNotDefined() th given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); + .willReturn(success(Map.of("uids", uidsCookie.updateUid(RUBICON, "J5VLCWQP-26-CWFT")))); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.toCookie(any(), any())).willReturn(Cookie + given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -818,7 +859,7 @@ public void shouldNotSendResponseIfClientClosedConnection() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "uid")) - .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); + .willReturn(success(Map.of("uids", uidsCookie))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("uid"); @@ -842,7 +883,7 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) - .willReturn(UidsCookieUpdateResult.success(Map.of("uids", uidsCookie))); + .willReturn(success(Map.of("uids", uidsCookie))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("updatedUid"); From 74ac069772e4a5e7e08b18bb7d05c62973252de1 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Wed, 22 Jan 2025 10:29:55 +0100 Subject: [PATCH 04/10] Revert OptoutHandler changes --- .../org/prebid/server/cookie/UidsCookieService.java | 4 ++++ .../org/prebid/server/handler/OptoutHandler.java | 12 +++++------- .../org/prebid/server/handler/OptoutHandlerTest.java | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/prebid/server/cookie/UidsCookieService.java b/src/main/java/org/prebid/server/cookie/UidsCookieService.java index 3e2fe30d1ee..1fbbc2c51e6 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookieService.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookieService.java @@ -168,6 +168,10 @@ public Cookie makeCookie(String cookieName, UidsCookie uidsCookie) { .setDomain(hostCookieDomain); } + public Cookie makeCookie(UidsCookie uidsCookie) { + return makeCookie(COOKIE_NAME, uidsCookie); + } + public Cookie removeCookie(String cookieName) { return Cookie .cookie(cookieName, StringUtils.EMPTY) diff --git a/src/main/java/org/prebid/server/handler/OptoutHandler.java b/src/main/java/org/prebid/server/handler/OptoutHandler.java index 38c93d68174..bc052654aec 100644 --- a/src/main/java/org/prebid/server/handler/OptoutHandler.java +++ b/src/main/java/org/prebid/server/handler/OptoutHandler.java @@ -68,7 +68,7 @@ private void handleVerification(RoutingContext routingContext, AsyncResult cookies, String url) { + private void respondWithRedirectAndCookie(RoutingContext routingContext, Cookie cookie, String url) { HttpUtil.executeSafely(routingContext, Endpoint.optout, response -> response .setStatusCode(HttpResponseStatus.MOVED_PERMANENTLY.code()) .putHeader(HttpUtil.LOCATION_HEADER, url) - .putHeader(HttpUtil.SET_COOKIE_HEADER.toString(), cookies.stream().map(Cookie::encode).toList()) + .putHeader(HttpUtil.SET_COOKIE_HEADER, cookie.encode()) .end()); } @@ -102,14 +102,12 @@ private static boolean isOptout(RoutingContext routingContext) { return StringUtils.isNotEmpty(optoutValue); } - private List optCookies(boolean optout, RoutingContext routingContext) { + private Cookie optCookie(boolean optout, RoutingContext routingContext) { final UidsCookie uidsCookie = uidsCookieService .parseFromRequest(routingContext) .updateOptout(optout); - return uidsCookieService.splitUids(uidsCookie).entrySet().stream() - .map(entry -> uidsCookieService.makeCookie(entry.getKey(), entry.getValue())) - .toList(); + return uidsCookieService.makeCookie(uidsCookie); } private String optUrl(boolean optout) { diff --git a/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java b/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java index a11d24b0133..adcdface038 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.makeCookie(any(), any())).willReturn(Cookie.cookie("cookie", "value")); + given(uidsCookieService.makeCookie(any())).willReturn(Cookie.cookie("cookie", "value")); given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); From 361605aeefad2b1abe17b6f60146068853530ed0 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Fri, 24 Jan 2025 16:27:26 +0100 Subject: [PATCH 05/10] Fix comments --- .../org/prebid/server/cookie/UidsCookie.java | 8 +- .../server/cookie/UidsCookieService.java | 92 ++++--- .../cookie/model/UidsCookieUpdateResult.java | 22 -- .../prebid/server/handler/OptoutHandler.java | 2 +- .../prebid/server/handler/SetuidHandler.java | 19 +- .../server/cookie/UidsCookieServiceTest.java | 257 +++++++----------- .../server/handler/OptoutHandlerTest.java | 2 +- .../server/handler/SetuidHandlerTest.java | 139 +++------- 8 files changed, 205 insertions(+), 336 deletions(-) delete mode 100644 src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java diff --git a/src/main/java/org/prebid/server/cookie/UidsCookie.java b/src/main/java/org/prebid/server/cookie/UidsCookie.java index 274e1ad12c0..968474bb7da 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookie.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookie.java @@ -86,6 +86,12 @@ public UidsCookie updateUid(String familyName, String uid) { return new UidsCookie(uids.toBuilder().uids(uidsMap).build(), mapper); } + public UidsCookie updateUid(String familyName, UidWithExpiry uid) { + final Map uidsMap = new HashMap<>(uids.getUids()); + uidsMap.put(familyName, uid); + return new UidsCookie(uids.toBuilder().uids(uidsMap).build(), mapper); + } + /** * Performs updates of {@link UidsCookie}'s optout flag and returns newly constructed {@link UidsCookie} * to avoid mutation of the current {@link UidsCookie}. @@ -102,7 +108,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 1fbbc2c51e6..88ead9bdcc9 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,13 +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; @@ -137,17 +139,19 @@ private Uids parseUids(Map cookies) { for (Map.Entry cookie : cookies.entrySet()) { final String cookieKey = cookie.getKey(); - if (cookieKey.startsWith(COOKIE_NAME)) { - try { - 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, cookie.getValue()); + if (!cookieKey.startsWith(COOKIE_NAME)) { + continue; + } + + try { + 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, cookie.getValue()); } } @@ -158,7 +162,7 @@ private Uids parseUids(Map cookies) { * Creates a {@link Cookie} with 'uids' as a name and encoded JSON string representing supplied {@link UidsCookie} * as a value. */ - public Cookie makeCookie(String cookieName, UidsCookie uidsCookie) { + public Cookie aliveCookie(String cookieName, UidsCookie uidsCookie) { return Cookie .cookie(cookieName, Base64.getUrlEncoder().encodeToString(uidsCookie.toJson().getBytes())) .setPath("/") @@ -168,11 +172,11 @@ public Cookie makeCookie(String cookieName, UidsCookie uidsCookie) { .setDomain(hostCookieDomain); } - public Cookie makeCookie(UidsCookie uidsCookie) { - return makeCookie(COOKIE_NAME, uidsCookie); + public Cookie aliveCookie(UidsCookie uidsCookie) { + return aliveCookie(COOKIE_NAME, uidsCookie); } - public Cookie removeCookie(String cookieName) { + public Cookie expiredCookie(String cookieName) { return Cookie .cookie(cookieName, StringUtils.EMPTY) .setPath("/") @@ -245,17 +249,17 @@ private static boolean facebookSentinelOrEmpty(Map.Entry /*** * Removes expired {@link Uids}, updates {@link UidsCookie} with new uid for family name according to priority */ - public UidsCookieUpdateResult updateUidsCookie(UidsCookie uidsCookie, String familyName, String uid) { + 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 UidsCookieUpdateResult.failure(splitUids(initialCookie)); + return UpdateResult.unaltered(initialCookie); } final UidsCookie updatedCookie = initialCookie.updateUid(familyName, uid); - return UidsCookieUpdateResult.success(splitUids(updatedCookie)); + return UpdateResult.updated(updatedCookie); } private static UidsCookie removeExpiredUids(UidsCookie uidsCookie) { @@ -271,49 +275,49 @@ private static UidsCookie removeExpiredUids(UidsCookie uidsCookie) { return updatedCookie; } - public Map splitUids(UidsCookie uidsCookie) { + public List splitUidsIntoCookies(UidsCookie uidsCookie) { final Uids cookieUids = uidsCookie.getCookieUids(); final Map uids = cookieUids.getUids(); final boolean hasOptout = !uidsCookie.allowsSync(); - final Iterator cookieFamilyIterator = cookieFamilyNamesByDescPriorityAndExpiration(uidsCookie); - final Map splitCookies = new HashMap<>(); + final Iterator cookieFamilies = cookieFamilyNamesByDescPriorityAndExpiration(uidsCookie); + final List splitCookies = new ArrayList<>(); int uidsIndex = 0; String nextCookieFamily = null; while (uidsIndex < numberOfUidCookies) { final String uidsName = uidsIndex == 0 ? COOKIE_NAME : COOKIE_NAME_FORMAT.formatted(uidsIndex + 1); - final UidsCookie tempUidsCookie = splitCookies.computeIfAbsent( - uidsName, - key -> new UidsCookie(Uids.builder().uids(new HashMap<>()).optout(hasOptout).build(), mapper)); - final Map tempUids = tempUidsCookie.getCookieUids().getUids(); - - while (nextCookieFamily != null || cookieFamilyIterator.hasNext()) { - nextCookieFamily = nextCookieFamily == null ? cookieFamilyIterator.next() : nextCookieFamily; - tempUids.put(nextCookieFamily, uids.get(nextCookieFamily)); + UidsCookie tempUidsCookie = new UidsCookie( + Uids.builder().uids(new HashMap<>()).optout(hasOptout).build(), + mapper); + + while (nextCookieFamily != null || cookieFamilies.hasNext()) { + nextCookieFamily = nextCookieFamily == null ? cookieFamilies.next() : nextCookieFamily; + tempUidsCookie = tempUidsCookie.updateUid(nextCookieFamily, uids.get(nextCookieFamily)); if (cookieExceededMaxLength(uidsName, tempUidsCookie)) { - tempUids.remove(nextCookieFamily); + tempUidsCookie = tempUidsCookie.deleteUid(nextCookieFamily); break; } nextCookieFamily = null; } - uidsIndex++; - } - - while (nextCookieFamily != null || cookieFamilyIterator.hasNext()) { - nextCookieFamily = nextCookieFamily == null ? cookieFamilyIterator.next() : nextCookieFamily; - if (prioritizedCoopSyncProvider.isPrioritizedFamily(nextCookieFamily)) { - metrics.updateUserSyncSizedOutMetric(nextCookieFamily); + if (tempUidsCookie.getCookieUids().getUids().isEmpty()) { + splitCookies.add(expiredCookie(uidsName)); } else { - metrics.updateUserSyncSizeBlockedMetric(nextCookieFamily); + splitCookies.add(aliveCookie(uidsName, tempUidsCookie)); } - nextCookieFamily = null; + uidsIndex++; } + if (nextCookieFamily != null) { + updateSyncSizeMetrics(nextCookieFamily); + } + + cookieFamilies.forEachRemaining(this::updateSyncSizeMetrics); + return splitCookies; } @@ -340,12 +344,20 @@ private int compareCookieFamilyNames(Map.Entry left, } } + private void updateSyncSizeMetrics(String nextCookieFamily) { + if (prioritizedCoopSyncProvider.isPrioritizedFamily(nextCookieFamily)) { + metrics.updateUserSyncSizedOutMetric(nextCookieFamily); + } else { + metrics.updateUserSyncSizeBlockedMetric(nextCookieFamily); + } + } + private boolean cookieExceededMaxLength(String name, UidsCookie uidsCookie) { return maxCookieSizeBytes > 0 && cookieBytesLength(name, uidsCookie) > maxCookieSizeBytes; } private int cookieBytesLength(String cookieName, UidsCookie uidsCookie) { - return makeCookie(cookieName, uidsCookie).encode().getBytes().length; + return aliveCookie(cookieName, uidsCookie).encode().getBytes().length; } public String hostCookieUidToSync(RoutingContext routingContext, String cookieFamilyName) { 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 cd504e7b923..00000000000 --- a/src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.prebid.server.cookie.model; - -import lombok.Value; -import org.prebid.server.cookie.UidsCookie; - -import java.util.Map; - -@Value(staticConstructor = "of") -public class UidsCookieUpdateResult { - - boolean successfullyUpdated; - - Map uidsCookies; - - public static UidsCookieUpdateResult success(Map uidsCookies) { - return of(true, uidsCookies); - } - - public static UidsCookieUpdateResult failure(Map uidsCookies) { - return of(false, uidsCookies); - } -} diff --git a/src/main/java/org/prebid/server/handler/OptoutHandler.java b/src/main/java/org/prebid/server/handler/OptoutHandler.java index bc052654aec..5d9885c70ec 100644 --- a/src/main/java/org/prebid/server/handler/OptoutHandler.java +++ b/src/main/java/org/prebid/server/handler/OptoutHandler.java @@ -107,7 +107,7 @@ private Cookie optCookie(boolean optout, RoutingContext routingContext) { .parseFromRequest(routingContext) .updateOptout(optout); - return uidsCookieService.makeCookie(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 8ce32ea0db8..d907052a1df 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; @@ -328,14 +328,13 @@ private void respondWithCookie(SetuidContext setuidContext) { final String uid = routingContext.request().getParam(UID_PARAM); final String bidder = setuidContext.getCookieName(); - final UidsCookieUpdateResult uidsCookieUpdateResult = uidsCookieService.updateUidsCookie( + final UpdateResult uidsCookieUpdateResult = uidsCookieService.updateUidsCookie( setuidContext.getUidsCookie(), bidder, uid); - uidsCookieUpdateResult.getUidsCookies().entrySet().stream() - .map(entry -> toCookie(entry.getKey(), entry.getValue())) - .forEach(uidsCookie -> addCookie(routingContext, uidsCookie)); + uidsCookieService.splitUidsIntoCookies(uidsCookieUpdateResult.getValue()) + .forEach(cookie -> addCookie(routingContext, cookie)); - if (uidsCookieUpdateResult.isSuccessfullyUpdated()) { + if (uidsCookieUpdateResult.isUpdated()) { metrics.updateUserSyncSetsMetric(bidder); } final int statusCode = HttpResponseStatus.OK.code(); @@ -346,17 +345,11 @@ private void respondWithCookie(SetuidContext setuidContext) { .status(statusCode) .bidder(bidder) .uid(uid) - .success(uidsCookieUpdateResult.isSuccessfullyUpdated()) + .success(uidsCookieUpdateResult.isUpdated()) .build(); analyticsDelegator.processEvent(setuidEvent, tcfContext); } - private Cookie toCookie(String cookieName, UidsCookie uidsCookie) { - return uidsCookie.getCookieUids().getUids().isEmpty() - ? uidsCookieService.removeCookie(cookieName) - : uidsCookieService.makeCookie(cookieName, uidsCookie); - } - private Consumer buildCookieResponseConsumer(SetuidContext setuidContext, int responseStatusCode) { diff --git a/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java b/src/test/java/org/prebid/server/cookie/UidsCookieServiceTest.java index ec97ddf4598..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,7 +23,9 @@ 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; @@ -95,40 +98,15 @@ public void shouldReturnNonEmptyUidsCookieFromCookiesMap() { @Test public void shouldReturnNonEmptyUidsCookieFromCookiesMapWhenSeveralUidsCookiesArePresent() { // given - final String uidsCookieValue = """ - { - "tempUIDs": { - "bidderA": { - "uid": "bidder-A-uid", - "expires": "2023-12-05T19:00:05.103329-03:00" - }, - "bidderB": { - "uid": "bidder-B-uid", - "expires": "2023-12-05T19:00:05.103329-03:00" - } - } - } - """; - - final String uids2CookieValue = """ - { - "tempUIDs": { - "bidderC": { - "uid": "bidder-C-uid", - "expires": "2023-12-05T19:00:05.103329-03:00" - }, - "bidderD": { - "uid": "bidder-D-uid", - "expires": "2023-12-05T19:00:05.103329-03:00" - } - } - } - """; - - final String encodedUidsCookie = Base64.getEncoder().encodeToString(uidsCookieValue.getBytes()); - final String encodedUids2Cookie = Base64.getEncoder().encodeToString(uids2CookieValue.getBytes()); - - final Map cookies = Map.of("uids", encodedUidsCookie, "uids2", encodedUids2Cookie); + final Map cookies = Map.of( + "uids", "eyJ0ZW1wVUlEcyI6eyJiaWRkZXJBIjp7InVpZCI6ImJpZGRlci1BLXVp" + + "ZCIsImV4cGlyZXMiOiIyMDIzLTEyLTA1VDE5OjAwOjA1LjEwMzMyOS0wMzowMCJ9L" + + "CJiaWRkZXJCIjp7InVpZCI6ImJpZGRlci1CLXVpZCIsImV4cGlyZXMiOiIyMDIzLTE" + + "yLTA1VDE5OjAwOjA1LjEwMzMyOS0wMzowMCJ9fX0=", + "uids2", "eyJ0ZW1wVUlEcyI6eyJiaWRkZXJDIjp7InVpZCI6ImJpZGRlci1DLXVpZCIsIm" + + "V4cGlyZXMiOiIyMDIzLTEyLTA1VDE5OjAwOjA1LjEwMzMyOS0wMzowMCJ9LCJiaWRkZXJEIjp7I" + + "nVpZCI6ImJpZGRlci1ELXVpZCIsImV4cGlyZXMiOiIyMDIzLTEyLTA1VDE5OjAwOjA1LjEwMzMy" + + "OS0wMzowMCJ9fX0"); // when final UidsCookie uidsCookie = target.parseFromCookies(cookies); @@ -144,36 +122,13 @@ public void shouldReturnNonEmptyUidsCookieFromCookiesMapWhenSeveralUidsCookiesAr @Test public void shouldReturnMergedUidsFromCookiesWithOldestUidWhenDuplicatesArePresent() { // given - final String uidsCookieValue = """ - { - "tempUIDs": { - "bidderA": { - "uid": "bidder-A1-uid", - "expires": "2023-12-05T19:00:05.103329-03:00" - }, - "bidderB": { - "uid": "bidder-B-uid", - "expires": "2023-12-05T19:00:05.103329-03:00" - } - } - } - """; - - final String uids2CookieValue = """ - { - "tempUIDs": { - "bidderA": { - "uid": "bidder-A2-uid", - "expires": "2024-12-05T19:00:05.103329-03:00" - } - } - } - """; - - final String encodedUidsCookie = Base64.getEncoder().encodeToString(uidsCookieValue.getBytes()); - final String encodedUids2Cookie = Base64.getEncoder().encodeToString(uids2CookieValue.getBytes()); - - final Map cookies = Map.of("uids", encodedUidsCookie, "uids2", encodedUids2Cookie); + final Map cookies = Map.of( + "uids", "eyJ0ZW1wVUlEcyI6eyJiaWRkZXJBIjp7InVpZCI6ImJpZGRlci1BMS11aW" + + "QiLCJleHBpcmVzIjoiMjAyMy0xMi0wNVQxOTowMDowNS4xMDMzMjktMDM6MDAifSwiYml" + + "kZGVyQiI6eyJ1aWQiOiJiaWRkZXItQi11aWQiLCJleHBpcmVzIjoiMjAyMy0xMi0wNVQxOTo" + + "wMDowNS4xMDMzMjktMDM6MDAifX19", + "uids2", "eyJ0ZW1wVUlEcyI6eyJiaWRkZXJBIjp7InVpZCI6ImJpZGRlci1BMi11aWQiLCJleH" + + "BpcmVzIjoiMjAyNC0xMi0wNVQxOTowMDowNS4xMDMzMjktMDM6MDAifX19"); // when final UidsCookie uidsCookie = target.parseFromCookies(cookies); @@ -280,7 +235,7 @@ public void shouldReturnUidsCookieWithOptoutTrueIfUidsCookieIsPresentAndOptoutCo } @Test - public void makeCookieShouldSetSameSiteNone() { + public void aliveCookieShouldSetSameSiteNone() { // given final Uids uids = Uids.builder() .uids(Map.of(RUBICON, UidWithExpiry.live("test"))) @@ -289,14 +244,14 @@ public void makeCookieShouldSetSameSiteNone() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = target.makeCookie("uids", uidsCookie); + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then assertThat(cookie.getSameSite()).isEqualTo(CookieSameSite.NONE); } @Test - public void makeCookieShouldSetSecure() { + public void aliveCookieShouldSetSecure() { // given final Uids uids = Uids.builder() .uids(Map.of(RUBICON, UidWithExpiry.live("test"))) @@ -305,14 +260,14 @@ public void makeCookieShouldSetSecure() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = target.makeCookie("uids", uidsCookie); + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then assertThat(cookie.isSecure()).isTrue(); } @Test - public void makeCookieShouldSetPath() { + public void aliveCookieShouldSetPath() { // given final Uids uids = Uids.builder() .uids(Map.of(RUBICON, UidWithExpiry.live("test"))) @@ -321,7 +276,7 @@ public void makeCookieShouldSetPath() { final UidsCookie uidsCookie = new UidsCookie(uids, jacksonMapper); // when - final Cookie cookie = target.makeCookie("uids", uidsCookie); + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then assertThat(cookie.getPath()).isEqualTo("/"); @@ -484,7 +439,7 @@ public void shouldSkipFacebookSentinelFromUidsCookie() throws JsonProcessingExce } @Test - public void makeCookieShouldReturnCookieWithExpectedValue() throws IOException { + public void aliveCookieShouldReturnCookieWithExpectedValue() { // given final UidsCookie uidsCookie = new UidsCookie( Uids.builder().uids(new HashMap<>()).build(), jacksonMapper) @@ -492,7 +447,7 @@ public void makeCookieShouldReturnCookieWithExpectedValue() throws IOException { .updateUid(ADNXS, "adnxsUid"); // when - final Cookie cookie = target.makeCookie("uids", uidsCookie); + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then final Map uids = decodeUids(cookie.getValue()).getUids(); @@ -508,7 +463,7 @@ public void makeCookieShouldReturnCookieWithExpectedValue() throws IOException { } @Test - public void makeCookieShouldReturnCookieWithExpectedExpiration() { + public void aliveCookieShouldReturnCookieWithExpectedExpiration() { // given final UidsCookie uidsCookie = new UidsCookie( Uids.builder().uids(new HashMap<>()).build(), jacksonMapper) @@ -516,38 +471,38 @@ public void makeCookieShouldReturnCookieWithExpectedExpiration() { .updateUid(ADNXS, "adnxsUid"); // when - final Cookie cookie = target.makeCookie("uids", uidsCookie); + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then assertThat(cookie.encode()).containsSequence("Max-Age=7776000; Expires="); } @Test - public void removeCookieShouldReturnCookieWithZeroMaxAge() { + public void expiredCookieShouldReturnCookieWithZeroMaxAge() { // when - final Cookie cookie = target.removeCookie("uids"); + final Cookie cookie = target.expiredCookie("uids"); // then assertThat(cookie.encode()).containsSequence("Max-Age=0; Expires="); } @Test - public void removeCookieShouldReturnCookieWithEmptyValue() { + public void expiredCookieShouldReturnCookieWithEmptyValue() { // when - final Cookie cookie = target.removeCookie("uids"); + final Cookie cookie = target.expiredCookie("uids"); // then assertThat(cookie.encode()).containsSequence("uids=;"); } @Test - public void makeCookieShouldReturnCookieWithExpectedDomain() { + public void aliveCookieShouldReturnCookieWithExpectedDomain() { // given final UidsCookie uidsCookie = new UidsCookie( Uids.builder().uids(new HashMap<>()).build(), jacksonMapper); // when - final Cookie cookie = target.makeCookie("uids", uidsCookie); + final Cookie cookie = target.aliveCookie("uids", uidsCookie); // then assertThat(cookie.getDomain()).isEqualTo(HOST_COOKIE_DOMAIN); @@ -720,15 +675,13 @@ public void updateUidsCookieShouldRemoveAllExpiredUids() { "family3", UidWithExpiry.expired("uid3"))); // when - final UidsCookieUpdateResult result = target.updateUidsCookie(uidsCookie, "family4", "uid4"); + final UpdateResult result = target.updateUidsCookie(uidsCookie, "family4", "uid4"); // then - assertThat(result.isSuccessfullyUpdated()).isTrue(); + assertThat(result.isUpdated()).isTrue(); - final Map actualUidsCookies = result.getUidsCookies(); - assertThat(actualUidsCookies.keySet()).containsOnly("uids"); - assertThat(actualUidsCookies.get("uids")) - .extracting(UidsCookie::getCookieUids) + final UidsCookie actualUidsCookies = result.getValue(); + assertThat(actualUidsCookies.getCookieUids()) .extracting(Uids::getUids) .extracting(Map::values) .extracting(ArrayList::new) @@ -747,11 +700,11 @@ public void updateUidsCookieShouldNotAddIncomingCookieFamilyWhenItHasBlankUid() "family3", UidWithExpiry.expired("uid3"))); // when - final UidsCookieUpdateResult result = target.updateUidsCookie(uidsCookie, "family", null); + final UpdateResult result = target.updateUidsCookie(uidsCookie, "family", null); // then - assertThat(result.isSuccessfullyUpdated()).isFalse(); - assertThat(result.getUidsCookies().get("uids").getCookieUids().getUids().keySet()).containsOnly("family2"); + assertThat(result.isUpdated()).isFalse(); + assertThat(result.getValue().getCookieUids().getUids().keySet()).containsOnly("family2"); } @Test @@ -763,12 +716,12 @@ public void updateUidsCookieShouldNotAddIncomingCookieFamilyWhenItIsFacebookSent "family3", UidWithExpiry.expired("uid3"))); // when - final UidsCookieUpdateResult result = target.updateUidsCookie( + final UpdateResult result = target.updateUidsCookie( uidsCookie, "audienceNetwork", "0"); // then - assertThat(result.isSuccessfullyUpdated()).isFalse(); - assertThat(result.getUidsCookies().get("uids").getCookieUids().getUids().keySet()).containsOnly("family2"); + assertThat(result.isUpdated()).isFalse(); + assertThat(result.getValue().getCookieUids().getUids().keySet()).containsOnly("family2"); } @Test @@ -790,16 +743,12 @@ public void updateUidsCookieShouldUpdateCookieAndNotSplitCookieWhenLimitIsNotExc final UidsCookie uidsCookie = givenUidsCookie(Map.of("family", UidWithExpiry.live("uid"))); // when - final UidsCookieUpdateResult result = target.updateUidsCookie( + final UpdateResult result = target.updateUidsCookie( uidsCookie, "another-family", "uid"); // then - assertThat(result.isSuccessfullyUpdated()).isTrue(); - - final Map actualUidsCookies = result.getUidsCookies(); - assertThat(actualUidsCookies.keySet()).containsOnly("uids", "uids2"); - assertThat(actualUidsCookies.get("uids").getCookieUids().getUids().keySet()) - .containsExactly("another-family", "family"); + assertThat(result.isUpdated()).isTrue(); + assertThat(result.getValue().getCookieUids().getUids().keySet()).containsOnly("another-family", "family"); } @Test @@ -824,19 +773,17 @@ public void updateUidsCookieShouldNotFitNonPrioritizedFamilyWhenSizeExceedsLimit // 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 = target.updateUidsCookie( - uidsCookie, "family", "uid"); + final List result = target.splitUidsIntoCookies(uidsCookie); // then verify(metrics).updateUserSyncSizeBlockedMetric("family"); - assertThat(result.isSuccessfullyUpdated()).isTrue(); - final Map actualUidsCookies = result.getUidsCookies(); - assertThat(actualUidsCookies.keySet()).containsOnly("uids"); - assertThat(actualUidsCookies.get("uids").getCookieUids().getUids().keySet()) + 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"); } @@ -860,19 +807,17 @@ public void updateUidsCookieShouldNotFitPrioritizedFamilyWhenSizeExceedsLimitAnd // 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 = target.updateUidsCookie( - uidsCookie, "family", "uid"); + final List result = target.splitUidsIntoCookies(uidsCookie); // then verify(metrics).updateUserSyncSizedOutMetric("family"); - assertThat(result.isSuccessfullyUpdated()).isTrue(); - final Map actualUidsCookies = result.getUidsCookies(); - assertThat(actualUidsCookies.keySet()).containsOnly("uids"); - assertThat(actualUidsCookies.get("uids").getCookieUids().getUids().keySet()) + 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"); } @@ -896,34 +841,19 @@ public void updateUidsCookieShouldFitPrioritizedFamily() { // 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 = target.updateUidsCookie( - uidsCookie, "family", "uid"); + final List result = target.splitUidsIntoCookies(uidsCookie); // then verifyNoInteractions(metrics); - assertThat(result.isSuccessfullyUpdated()).isTrue(); - final Map actualUidsCookies = result.getUidsCookies(); - assertThat(actualUidsCookies.keySet()).containsOnly("uids", "uids2"); - assertThat(actualUidsCookies.get("uids")) - .extracting(UidsCookie::getCookieUids) - .extracting(Uids::getUids) - .extracting(Map::keySet) - .extracting(ArrayList::new) - .asList() - .flatExtracting(identity()) + 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(actualUidsCookies.get("uids2")) - .extracting(UidsCookie::getCookieUids) - .extracting(Uids::getUids) - .extracting(Map::keySet) - .extracting(ArrayList::new) - .asList() - .flatExtracting(identity()) + assertThat(decodeUids(result.getLast().getValue()).getUids().keySet()) .containsExactly("family"); } @@ -947,25 +877,35 @@ public void updateUidsCookieShouldFitNonPrioritizedFamily() { // 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 = target.updateUidsCookie( - uidsCookie, "family", "uid"); + final List result = target.splitUidsIntoCookies(uidsCookie); // then verifyNoInteractions(metrics); - assertThat(result.isSuccessfullyUpdated()).isTrue(); - final Map actualUidsCookies = result.getUidsCookies(); - assertThat(actualUidsCookies.keySet()).containsOnly("uids", "uids2", "uids3", "uids4", "uids5"); - assertThat(actualUidsCookies.get("uids").getCookieUids().getUids().keySet()) - .containsExactly("very-very-very-very-long-family", "another-very-very-very-long-family"); - assertThat(actualUidsCookies.get("uids2").getCookieUids().getUids().keySet()) + 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(actualUidsCookies.get("uids3").getCookieUids().getUids()).isEmpty(); - assertThat(actualUidsCookies.get("uids4").getCookieUids().getUids()).isEmpty(); - assertThat(actualUidsCookies.get("uids5").getCookieUids().getUids()).isEmpty(); + 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 @@ -988,23 +928,21 @@ public void updateUidsCookieShouldDisallowSyncForAllCookiesWhenOptoutSetTrue() { // 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")); + "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 UidsCookieUpdateResult result = target.updateUidsCookie( - uidsCookie, "family", "uid"); + final List result = target.splitUidsIntoCookies(uidsCookie); // then verifyNoInteractions(metrics); - assertThat(result.isSuccessfullyUpdated()).isTrue(); - final Map actualUidsCookies = result.getUidsCookies(); - assertThat(actualUidsCookies.keySet()).containsOnly("uids", "uids2"); - assertThat(actualUidsCookies.get("uids").allowsSync()).isFalse(); - assertThat(actualUidsCookies.get("uids2").allowsSync()).isFalse(); + 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) { @@ -1015,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 adcdface038..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.makeCookie(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 925fe0dc308..e83b75b83d2 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -7,7 +7,6 @@ import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; -import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -52,6 +51,7 @@ 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; @@ -59,6 +59,7 @@ 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; @@ -70,8 +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.cookie.model.UidsCookieUpdateResult.failure; -import static org.prebid.server.cookie.model.UidsCookieUpdateResult.success; +import static org.prebid.server.model.UpdateResult.unaltered; +import static org.prebid.server.model.UpdateResult.updated; @ExtendWith(MockitoExtension.class) public class SetuidHandlerTest extends VertxTest { @@ -142,9 +143,11 @@ public void setUp() { given(httpResponse.putHeader(any(CharSequence.class), any(CharSequence.class))).willReturn(httpResponse); given(httpResponse.closed()).willReturn(false); - given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie.cookie("test", "test")); - given(uidsCookieService.removeCookie(any())) - .willAnswer(invocation -> Cookie.cookie(invocation.getArgument(0), StringUtils.EMPTY)); + 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); @@ -307,7 +310,7 @@ public void shouldPassUnsuccessfulEventToAnalyticsReporterIfUidMissingInRequest( given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(failure(Map.of("uids", uidsCookie))); + .willReturn(unaltered(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -401,7 +404,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsFound() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(success(Map.of("uids", uidsCookie))); + .willReturn(updated(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("account")).willReturn("accId"); @@ -430,7 +433,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsNotFound() given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, null)) - .willReturn(success(Map.of("uids", uidsCookie))); + .willReturn(updated(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("account")).willReturn("accId"); @@ -453,11 +456,7 @@ public void shouldRespondWithCookieFromRequestParam() throws IOException { .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(success(Map.of("uids", uidsCookie.updateUid(RUBICON, "J5VLCWQP-26-CWFT")))); - - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.makeCookie(any(), 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"); @@ -480,11 +479,7 @@ public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreD given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, ADNXS, "J5VLCWQP-26-CWFT")) - .willReturn(success(Map.of("uids", uidsCookie.updateUid(ADNXS, "J5VLCWQP-26-CWFT")))); - - // {"tempUIDs":{"adnxs":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.makeCookie(any(), 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"); @@ -506,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.makeCookie(any(), any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(uidsCookieService.updateUidsCookie(any(), any(), any())) - .willReturn(success(Map.of("uids", emptyUidsCookie()))); + .willReturn(updated(emptyUidsCookie())); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("f")).willReturn("i"); @@ -533,11 +525,7 @@ public void shouldSendEmptyResponseWhenFParamIsEqualToBWhenTypeIsRedirect() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(success(Map.of("uids", uidsCookie))); - - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + .willReturn(updated(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("f")).willReturn("b"); @@ -578,11 +566,7 @@ public void shouldSendEmptyResponseWhenFParamNotDefinedAndTypeIsIframe() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(success(Map.of("uids", uidsCookie))); - - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + .willReturn(updated(uidsCookie)); given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn( Optional.of(Usersyncer.of(RUBICON, iframeMethod(), null))); @@ -622,11 +606,8 @@ public void shouldSendPixelWhenFParamNotDefinedAndTypeIsRedirect() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(success(Map.of("uids", uidsCookie))); + .willReturn(updated(uidsCookie)); - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.makeCookie(any(), any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(bidderCatalog.usersyncReadyBidders()).willReturn(singleton(RUBICON)); given(bidderCatalog.usersyncerByName(any())) @@ -665,17 +646,11 @@ public void shouldInCookieWithRequestValue() throws IOException { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) - .willReturn(success(Map.of("uids", 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.makeCookie(any(), any())) - .willReturn(Cookie.cookie("uids", - "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9LCAicnViaWNvbiI6eyJ1aWQiOiJ1cGRhdGVkVW" - + "lkIn19fQ==")); - // when setuidHandler.handle(routingContext); @@ -700,19 +675,20 @@ public void shouldReturnMultipleCookies() throws IOException { 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(success(Map.of( - "uids", uidsCookie.updateUid(RUBICON, "updatedUid"), - "uids2", uidsCookie.updateUid(ADNXS, "12345")))); + .willReturn(updated(givenUidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("updatedUid"); // {"tempUIDs":{"adnxs":{"uid":"12345"}, "rubicon":{"uid":"updatedUid"}}} - given(uidsCookieService.makeCookie(eq("uids"), any())) - .willReturn(Cookie.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6InVwZGF0ZWRVaWQifX19")); - given(uidsCookieService.makeCookie(eq("uids2"), any())) - .willReturn(Cookie.cookie("uids2", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9fX0")); + given(uidsCookieService.splitUidsIntoCookies(givenUidsCookie)).willReturn(List.of( + Cookie.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6InVwZGF0ZWRVaWQifX19"), + Cookie.cookie("uids2", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9fX0"))); // when setuidHandler.handle(routingContext); @@ -737,43 +713,6 @@ public void shouldReturnMultipleCookies() throws IOException { assertThat(decodedUids2.getUids().get(ADNXS).getUid()).isEqualTo("12345"); } - @Test - public void shouldCleanUpEmptyUidsCookies() { - // 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); - given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) - .willReturn(success(Map.of( - "uids", uidsCookie.updateUid(RUBICON, "updatedUid"), - "uids2", uidsCookie.updateUid(ADNXS, "12345"), - "uids3", emptyUidsCookie()))); - - given(httpRequest.getParam("bidder")).willReturn(RUBICON); - given(httpRequest.getParam("uid")).willReturn("updatedUid"); - - // {"tempUIDs":{"adnxs":{"uid":"12345"}, "rubicon":{"uid":"updatedUid"}}} - given(uidsCookieService.makeCookie(eq("uids"), any())) - .willReturn(Cookie.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6InVwZGF0ZWRVaWQifX19")); - given(uidsCookieService.makeCookie(eq("uids2"), any())) - .willReturn(Cookie.cookie("uids2", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9fX0")); - - // when - setuidHandler.handle(routingContext); - - // then - verify(httpResponse).sendFile(any()); - verify(routingContext, never()).addCookie(any(Cookie.class)); - - verify(uidsCookieService).makeCookie(eq("uids"), any()); - verify(uidsCookieService).makeCookie(eq("uids2"), any()); - verify(uidsCookieService).removeCookie("uids3"); - } - @Test public void shouldRespondWithCookieIfUserIsNotInGdprScope() throws IOException { // given @@ -784,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(success(Map.of("uids", uidsCookie.updateUid(RUBICON, "J5VLCWQP-26-CWFT")))); - - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.makeCookie(any(), 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"); @@ -829,11 +764,7 @@ public void shouldSkipTcfChecksAndRespondWithCookieIfHostVendorIdNotDefined() th given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "J5VLCWQP-26-CWFT")) - .willReturn(success(Map.of("uids", uidsCookie.updateUid(RUBICON, "J5VLCWQP-26-CWFT")))); - - // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} - given(uidsCookieService.makeCookie(any(), 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"); @@ -859,7 +790,7 @@ public void shouldNotSendResponseIfClientClosedConnection() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "uid")) - .willReturn(success(Map.of("uids", uidsCookie))); + .willReturn(updated(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("uid"); @@ -883,7 +814,7 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(uidsCookie); given(uidsCookieService.updateUidsCookie(uidsCookie, RUBICON, "updatedUid")) - .willReturn(success(Map.of("uids", uidsCookie))); + .willReturn(updated(uidsCookie)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("updatedUid"); @@ -937,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() { From db7550718492f16cfc42f4bca26c67b2d3ff9c41 Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:30:08 +0200 Subject: [PATCH 06/10] Tests: Multiple Uids Cookies Support (#3691) * Tests: Multiple Uids Cookies Support --- .../server/functional/model/UidsCookie.groovy | 4 +- .../model/request/setuid/SetuidRequest.groovy | 8 +- .../model/request/setuid/UidWithExpiry.groovy | 4 +- .../response/setuid/SetuidResponse.groovy | 2 +- .../service/PrebidServerService.groovy | 51 +++- .../server/functional/tests/SetUidSpec.groovy | 277 +++++++++++++++--- .../util/ObjectMapperWrapper.groovy | 4 + 7 files changed, 289 insertions(+), 61 deletions(-) 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/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/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/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) } From 2fd0b4001228ffa5b9fd2fbccf6a8a9d5a83b6d9 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Tue, 28 Jan 2025 16:38:33 +0100 Subject: [PATCH 07/10] Cookie Size Calculation Optimization --- .../server/cookie/UidsCookieService.java | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/prebid/server/cookie/UidsCookieService.java b/src/main/java/org/prebid/server/cookie/UidsCookieService.java index 88ead9bdcc9..12f1a7d631e 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookieService.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookieService.java @@ -42,6 +42,12 @@ public class UidsCookieService { private static final int MIN_NUMBER_OF_UID_COOKIES = 1; private static final int MAX_NUMBER_OF_UID_COOKIES = 30; + // {"tempUIDs":{},"optout":false} + private static final int TEMP_UIDS_BASE64_BYTES = "eyJ0ZW1wVUlEcyI6e30sIm9wdG91dCI6ZmFsc2V9".length(); + // "":{"uid":"","expires":"1970-01-01T00:00:00.000000000Z"}, + private static final int UID_BASE64_BYTES = ("IiI6eyJ1aWQiOiIiLCJleHBpcmVzI" + + "joiMTk3MC0wMS0wMVQwMDowMDowMC4wMDAwMDAwMDBaIn0s").length(); + private final String optOutCookieName; private final String optOutCookieValue; private final String hostCookieFamily; @@ -163,13 +169,8 @@ private Uids parseUids(Map cookies) { * as a value. */ public Cookie aliveCookie(String cookieName, UidsCookie uidsCookie) { - return Cookie - .cookie(cookieName, Base64.getUrlEncoder().encodeToString(uidsCookie.toJson().getBytes())) - .setPath("/") - .setSameSite(CookieSameSite.NONE) - .setSecure(true) - .setMaxAge(ttlSeconds) - .setDomain(hostCookieDomain); + final String value = Base64.getUrlEncoder().encodeToString(uidsCookie.toJson().getBytes()); + return makeCookie(cookieName, value, ttlSeconds); } public Cookie aliveCookie(UidsCookie uidsCookie) { @@ -177,12 +178,15 @@ public Cookie aliveCookie(UidsCookie uidsCookie) { } public Cookie expiredCookie(String cookieName) { - return Cookie - .cookie(cookieName, StringUtils.EMPTY) + return makeCookie(cookieName, StringUtils.EMPTY, 0); + } + + private Cookie makeCookie(String cookieName, String value, long maxAge) { + return Cookie.cookie(cookieName, value) .setPath("/") .setSameSite(CookieSameSite.NONE) .setSecure(true) - .setMaxAge(0) + .setMaxAge(maxAge) .setDomain(hostCookieDomain); } @@ -283,33 +287,38 @@ public List splitUidsIntoCookies(UidsCookie uidsCookie) { final Iterator cookieFamilies = cookieFamilyNamesByDescPriorityAndExpiration(uidsCookie); final List splitCookies = new ArrayList<>(); - int uidsIndex = 0; + final int staticCookieDataBytes = makeCookie(COOKIE_NAME, StringUtils.EMPTY, ttlSeconds).encode().length(); + String nextCookieFamily = null; - while (uidsIndex < numberOfUidCookies) { - final String uidsName = uidsIndex == 0 ? COOKIE_NAME : COOKIE_NAME_FORMAT.formatted(uidsIndex + 1); - UidsCookie tempUidsCookie = new UidsCookie( - Uids.builder().uids(new HashMap<>()).optout(hasOptout).build(), - mapper); + for (int uidsIndex = 0; uidsIndex < numberOfUidCookies; uidsIndex++) { + int actualCookieSize = staticCookieDataBytes + TEMP_UIDS_BASE64_BYTES; + final Map tempUids = new HashMap<>(); while (nextCookieFamily != null || cookieFamilies.hasNext()) { nextCookieFamily = nextCookieFamily == null ? cookieFamilies.next() : nextCookieFamily; - tempUidsCookie = tempUidsCookie.updateUid(nextCookieFamily, uids.get(nextCookieFamily)); - if (cookieExceededMaxLength(uidsName, tempUidsCookie)) { - tempUidsCookie = tempUidsCookie.deleteUid(nextCookieFamily); + + final UidWithExpiry uidWithExpiry = uids.get(nextCookieFamily); + actualCookieSize += UID_BASE64_BYTES + + calculateCookieSize(uidsIndex, nextCookieFamily, uidWithExpiry.getUid()); + + if (maxCookieSizeBytes > 0 && actualCookieSize > maxCookieSizeBytes) { break; } + tempUids.put(nextCookieFamily, uidWithExpiry); nextCookieFamily = null; } - if (tempUidsCookie.getCookieUids().getUids().isEmpty()) { + final String uidsName = uidsIndex == 0 ? COOKIE_NAME : COOKIE_NAME_FORMAT.formatted(uidsIndex + 1); + + if (tempUids.isEmpty()) { splitCookies.add(expiredCookie(uidsName)); } else { - splitCookies.add(aliveCookie(uidsName, tempUidsCookie)); + splitCookies.add(aliveCookie( + uidsName, + new UidsCookie(Uids.builder().uids(tempUids).optout(hasOptout).build(), mapper))); } - - uidsIndex++; } if (nextCookieFamily != null) { @@ -321,11 +330,18 @@ public List splitUidsIntoCookies(UidsCookie uidsCookie) { return splitCookies; } + private static int calculateCookieSize(int uidsIndex, String cookieFamily, String uid) { + final int approximateBase64CookieFamilySize = (int) Math.ceil(cookieFamily.length() * 1.33); + final int approximateBase64UidSize = (int) Math.ceil(uid.length() * 1.33); + final int uidsIndexSize = uidsIndex == 0 ? 0 : 2; + + return uidsIndexSize + approximateBase64CookieFamilySize + approximateBase64UidSize; + } + private Iterator cookieFamilyNamesByDescPriorityAndExpiration(UidsCookie uidsCookie) { return uidsCookie.getCookieUids().getUids().entrySet().stream() .sorted(this::compareCookieFamilyNames) .map(Map.Entry::getKey) - .toList() .iterator(); } @@ -352,14 +368,6 @@ private void updateSyncSizeMetrics(String nextCookieFamily) { } } - private boolean cookieExceededMaxLength(String name, UidsCookie uidsCookie) { - return maxCookieSizeBytes > 0 && cookieBytesLength(name, uidsCookie) > maxCookieSizeBytes; - } - - private int cookieBytesLength(String cookieName, UidsCookie uidsCookie) { - return aliveCookie(cookieName, uidsCookie).encode().getBytes().length; - } - public String hostCookieUidToSync(RoutingContext routingContext, String cookieFamilyName) { if (!StringUtils.equals(cookieFamilyName, hostCookieFamily)) { return null; From 7405892b4924431f40c8204b35966cfdf84b2a85 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Wed, 29 Jan 2025 00:11:40 +0200 Subject: [PATCH 08/10] update functional tests --- .../model/response/amp/RawAmpResponse.groovy | 2 +- .../response/auction/RawAuctionResponse.groovy | 2 +- .../cookiesync/RawCookieSyncResponse.groovy | 2 +- .../server/functional/tests/AmpSpec.groovy | 2 +- .../server/functional/tests/AuctionSpec.groovy | 2 +- .../functional/tests/CookieSyncSpec.groovy | 10 +++++----- .../functional/tests/HttpSettingsSpec.groovy | 12 ++++++++---- .../functional/tests/TopicsHeaderSpec.groovy | 18 +++++++++--------- 8 files changed, 27 insertions(+), 23 deletions(-) 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/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/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, ""] From 7ff5ff4a8436e9ab5f9b1d9519721e62913be951 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:00:46 +0100 Subject: [PATCH 09/10] Change size prediction logic (#3712) --- .../server/cookie/UidsCookieService.java | 31 ++------ .../prebid/server/cookie/UidsCookieSize.java | 73 +++++++++++++++++++ 2 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 src/main/java/org/prebid/server/cookie/UidsCookieSize.java diff --git a/src/main/java/org/prebid/server/cookie/UidsCookieService.java b/src/main/java/org/prebid/server/cookie/UidsCookieService.java index 12f1a7d631e..8bfe414884c 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookieService.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookieService.java @@ -42,12 +42,6 @@ public class UidsCookieService { private static final int MIN_NUMBER_OF_UID_COOKIES = 1; private static final int MAX_NUMBER_OF_UID_COOKIES = 30; - // {"tempUIDs":{},"optout":false} - private static final int TEMP_UIDS_BASE64_BYTES = "eyJ0ZW1wVUlEcyI6e30sIm9wdG91dCI6ZmFsc2V9".length(); - // "":{"uid":"","expires":"1970-01-01T00:00:00.000000000Z"}, - private static final int UID_BASE64_BYTES = ("IiI6eyJ1aWQiOiIiLCJleHBpcmVzI" - + "joiMTk3MC0wMS0wMVQwMDowMDowMC4wMDAwMDAwMDBaIn0s").length(); - private final String optOutCookieName; private final String optOutCookieValue; private final String hostCookieFamily; @@ -287,22 +281,19 @@ public List splitUidsIntoCookies(UidsCookie uidsCookie) { final Iterator cookieFamilies = cookieFamilyNamesByDescPriorityAndExpiration(uidsCookie); final List splitCookies = new ArrayList<>(); - final int staticCookieDataBytes = makeCookie(COOKIE_NAME, StringUtils.EMPTY, ttlSeconds).encode().length(); - + 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); - for (int uidsIndex = 0; uidsIndex < numberOfUidCookies; uidsIndex++) { - int actualCookieSize = staticCookieDataBytes + TEMP_UIDS_BASE64_BYTES; final Map tempUids = new HashMap<>(); - while (nextCookieFamily != null || cookieFamilies.hasNext()) { nextCookieFamily = nextCookieFamily == null ? cookieFamilies.next() : nextCookieFamily; - final UidWithExpiry uidWithExpiry = uids.get(nextCookieFamily); - actualCookieSize += UID_BASE64_BYTES - + calculateCookieSize(uidsIndex, nextCookieFamily, uidWithExpiry.getUid()); - if (maxCookieSizeBytes > 0 && actualCookieSize > maxCookieSizeBytes) { + uidsCookieSize.addUid(nextCookieFamily, uidWithExpiry.getUid()); + if (!uidsCookieSize.isValid()) { break; } @@ -310,7 +301,7 @@ public List splitUidsIntoCookies(UidsCookie uidsCookie) { nextCookieFamily = null; } - final String uidsName = uidsIndex == 0 ? COOKIE_NAME : COOKIE_NAME_FORMAT.formatted(uidsIndex + 1); + final String uidsName = i == 0 ? COOKIE_NAME : COOKIE_NAME_FORMAT.formatted(i + 1); if (tempUids.isEmpty()) { splitCookies.add(expiredCookie(uidsName)); @@ -330,14 +321,6 @@ public List splitUidsIntoCookies(UidsCookie uidsCookie) { return splitCookies; } - private static int calculateCookieSize(int uidsIndex, String cookieFamily, String uid) { - final int approximateBase64CookieFamilySize = (int) Math.ceil(cookieFamily.length() * 1.33); - final int approximateBase64UidSize = (int) Math.ceil(uid.length() * 1.33); - final int uidsIndexSize = uidsIndex == 0 ? 0 : 2; - - return uidsIndexSize + approximateBase64CookieFamilySize + approximateBase64UidSize; - } - private Iterator cookieFamilyNamesByDescPriorityAndExpiration(UidsCookie uidsCookie) { return uidsCookie.getCookieUids().getUids().entrySet().stream() .sorted(this::compareCookieFamilyNames) 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); + } + } +} From 9d75e1342a358c059b66d8de81186ec2fa4c9f4f Mon Sep 17 00:00:00 2001 From: antonbabak Date: Wed, 29 Jan 2025 13:45:25 +0100 Subject: [PATCH 10/10] Remove Unused Method --- src/main/java/org/prebid/server/cookie/UidsCookie.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/org/prebid/server/cookie/UidsCookie.java b/src/main/java/org/prebid/server/cookie/UidsCookie.java index 968474bb7da..ce7354c45f5 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookie.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookie.java @@ -86,12 +86,6 @@ public UidsCookie updateUid(String familyName, String uid) { return new UidsCookie(uids.toBuilder().uids(uidsMap).build(), mapper); } - public UidsCookie updateUid(String familyName, UidWithExpiry uid) { - final Map uidsMap = new HashMap<>(uids.getUids()); - uidsMap.put(familyName, uid); - return new UidsCookie(uids.toBuilder().uids(uidsMap).build(), mapper); - } - /** * Performs updates of {@link UidsCookie}'s optout flag and returns newly constructed {@link UidsCookie} * to avoid mutation of the current {@link UidsCookie}.