From 310da3fec0eb2d2a04a5615c5727370bd8fa102f Mon Sep 17 00:00:00 2001 From: Viktor Kryshtal Date: Tue, 7 Apr 2026 14:31:11 +0300 Subject: [PATCH 1/3] Update setuid endpoint to accept both bidder names and cookie family names --- .../server/auction/model/SetuidContext.java | 6 +- .../org/prebid/server/bidder/Usersyncer.java | 6 +- .../prebid/server/handler/SetuidHandler.java | 148 +++++++++--------- .../org/prebid/server/metric/Metrics.java | 12 +- .../bidder/util/BidderDepsAssembler.java | 17 +- .../config/bidder/util/UsersyncerCreator.java | 32 ++-- .../server/auction/ExchangeServiceTest.java | 2 +- .../server/bidder/BidderCatalogTest.java | 18 ++- .../bidder/UsersyncMethodChooserTest.java | 58 +++++-- .../server/cookie/CookieSyncServiceTest.java | 2 +- .../server/cookie/CoopSyncProviderTest.java | 1 + .../PrioritizedCoopSyncProviderTest.java | 1 + .../server/handler/SetuidHandlerTest.java | 47 ++++-- .../bidder/util/UsersyncerCreatorTest.java | 11 +- 14 files changed, 216 insertions(+), 145 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/model/SetuidContext.java b/src/main/java/org/prebid/server/auction/model/SetuidContext.java index 05c451bf863..d543325ed1f 100644 --- a/src/main/java/org/prebid/server/auction/model/SetuidContext.java +++ b/src/main/java/org/prebid/server/auction/model/SetuidContext.java @@ -6,7 +6,7 @@ import lombok.Value; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.auction.gpp.model.GppContext; -import org.prebid.server.bidder.UsersyncMethodType; +import org.prebid.server.bidder.Usersyncer; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.privacy.model.PrivacyContext; @@ -27,10 +27,10 @@ public class SetuidContext { Account account; - String cookieName; + String bidderQueryParam; @JsonIgnore - UsersyncMethodType syncType; + Usersyncer usersyncer; PrivacyContext privacyContext; diff --git a/src/main/java/org/prebid/server/bidder/Usersyncer.java b/src/main/java/org/prebid/server/bidder/Usersyncer.java index ebe776d2e3e..2d8e8b03ac2 100644 --- a/src/main/java/org/prebid/server/bidder/Usersyncer.java +++ b/src/main/java/org/prebid/server/bidder/Usersyncer.java @@ -10,6 +10,8 @@ public class Usersyncer { boolean enabled; + String bidder; + String cookieFamilyName; CookieFamilySource cookieFamilySource; @@ -22,7 +24,8 @@ public class Usersyncer { List gppSidToSkip; - public static Usersyncer of(String cookieFamilyName, + public static Usersyncer of(String bidder, + String cookieFamilyName, UsersyncMethod iframe, UsersyncMethod redirect, boolean skipWhenInGdprScope, @@ -30,6 +33,7 @@ public static Usersyncer of(String cookieFamilyName, return of( true, + bidder, cookieFamilyName, CookieFamilySource.ROOT, iframe, diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index bce568db2d7..0ad9e2b8fe4 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -13,7 +13,6 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; @@ -89,7 +88,8 @@ public class SetuidHandler implements ApplicationResource { private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; private final TimeoutFactory timeoutFactory; - private final Map> cookieNameToBidderAndSyncType; + private final Map bidderToUsersyncer; + private final Map cookieNameToUsersyncer; public SetuidHandler(long defaultTimeout, UidsCookieService uidsCookieService, @@ -113,21 +113,9 @@ public SetuidHandler(long defaultTimeout, this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); - this.cookieNameToBidderAndSyncType = collectUsersyncers(bidderCatalog); - } - - private static Map> collectUsersyncers(BidderCatalog bidderCatalog) { validateUsersyncersDuplicates(bidderCatalog); - - return bidderCatalog.usersyncReadyBidders().stream() - .sorted(Comparator.comparing(bidderName -> BooleanUtils.toInteger(bidderCatalog.isAlias(bidderName)))) - .filter(StreamUtil.distinctBy(bidderCatalog::cookieFamilyName)) - .map(bidderName -> bidderCatalog.usersyncerByName(bidderName) - .map(usersyncer -> Pair.of(bidderName, usersyncer))) - .flatMap(Optional::stream) - .collect(Collectors.toMap( - pair -> pair.getRight().getCookieFamilyName(), - pair -> Pair.of(pair.getLeft(), preferredUserSyncType(pair.getRight())))); + this.bidderToUsersyncer = collectUsersyncersByBidderName(bidderCatalog); + this.cookieNameToUsersyncer = collectUsersyncersByCookieName(bidderCatalog); } private static void validateUsersyncersDuplicates(BidderCatalog bidderCatalog) { @@ -158,8 +146,18 @@ private static boolean isAliasWithRootCookieFamilyName(BidderCatalog bidderCatal && parentCookieFamilyName.equals(bidderCookieFamilyName); } - private static UsersyncMethodType preferredUserSyncType(Usersyncer usersyncer) { - return ObjectUtils.firstNonNull(usersyncer.getIframe(), usersyncer.getRedirect()).getType(); + private static Map collectUsersyncersByBidderName(BidderCatalog bidderCatalog) { + return bidderCatalog.usersyncReadyBidders().stream() + .map(bidder -> bidderCatalog.usersyncerByName(bidder).orElseThrow()) + .collect(Collectors.toMap(Usersyncer::getBidder, Function.identity())); + } + + private static Map collectUsersyncersByCookieName(BidderCatalog bidderCatalog) { + return bidderCatalog.usersyncReadyBidders().stream() + .sorted(Comparator.comparing(bidderName -> BooleanUtils.toInteger(bidderCatalog.isAlias(bidderName)))) + .filter(StreamUtil.distinctBy(bidderCatalog::cookieFamilyName)) + .map(bidderName -> bidderCatalog.usersyncerByName(bidderName).orElseThrow()) + .collect(Collectors.toMap(Usersyncer::getCookieFamilyName, Function.identity())); } @Override @@ -170,20 +168,20 @@ public List endpoints() { @Override public void handle(RoutingContext routingContext) { toSetuidContext(routingContext) - .onComplete(setuidContextResult -> handleSetuidContextResult(setuidContextResult, routingContext)); + .onFailure(error -> handleErrors(error, routingContext, null)) + .onSuccess(setuidContext -> processSetuidContext(setuidContext, routingContext)); } private Future toSetuidContext(RoutingContext routingContext) { final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); final HttpServerRequest httpRequest = routingContext.request(); - final String cookieName = httpRequest.getParam(BIDDER_PARAM); + final String bidderQueryParam = httpRequest.getParam(BIDDER_PARAM); final String requestAccount = httpRequest.getParam(ACCOUNT_PARAM); final Timeout timeout = timeoutFactory.create(defaultTimeout); - final UsersyncMethodType syncType = Optional.ofNullable(cookieName) - .map(cookieNameToBidderAndSyncType::get) - .map(Pair::getRight) - .orElse(null); + final Usersyncer usersyncer = ObjectUtils.firstNonNull( + bidderToUsersyncer.get(bidderQueryParam), + cookieNameToUsersyncer.get(bidderQueryParam)); return accountById(requestAccount, timeout) .compose(account -> setuidPrivacyContextFactory.contextFrom(httpRequest, account, timeout) @@ -192,8 +190,8 @@ private Future toSetuidContext(RoutingContext routingContext) { .uidsCookie(uidsCookie) .timeout(timeout) .account(account) - .cookieName(cookieName) - .syncType(syncType) + .bidderQueryParam(bidderQueryParam) + .usersyncer(usersyncer) .privacyContext(privacyContext) .build())) @@ -218,51 +216,41 @@ private SetuidContext fillWithActivityInfrastructure(SetuidContext setuidContext .build(); } - private void handleSetuidContextResult(AsyncResult setuidContextResult, - RoutingContext routingContext) { + private void processSetuidContext(SetuidContext setuidContext, RoutingContext routingContext) { + final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); - if (setuidContextResult.succeeded()) { - final SetuidContext setuidContext = setuidContextResult.result(); - final String bidderCookieFamily = setuidContext.getCookieName(); - final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); + try { + validateSetuidContext(setuidContext); + } catch (InvalidRequestException | UnauthorizedUidsException | UnavailableForLegalReasonsException e) { + handleErrors(e, routingContext, tcfContext); + return; + } - try { - validateSetuidContext(setuidContext, bidderCookieFamily); - } catch (InvalidRequestException | UnauthorizedUidsException | UnavailableForLegalReasonsException e) { - handleErrors(e, routingContext, tcfContext); - return; - } + final String bidderName = setuidContext.getUsersyncer().getBidder(); - final AccountPrivacyConfig privacyConfig = setuidContext.getAccount().getPrivacy(); - final AccountGdprConfig accountGdprConfig = privacyConfig != null ? privacyConfig.getGdpr() : null; + final AccountPrivacyConfig privacyConfig = setuidContext.getAccount().getPrivacy(); + final AccountGdprConfig accountGdprConfig = privacyConfig != null ? privacyConfig.getGdpr() : null; - final String bidderName = cookieNameToBidderAndSyncType.get(bidderCookieFamily).getLeft(); + Future.all( + tcfDefinerService.isAllowedForHostVendorId(tcfContext), + tcfDefinerService.resultForBidderNames( + Collections.singleton(bidderName), tcfContext, accountGdprConfig)) + .onComplete(hostTcfResponseResult -> respondByTcfResponse(hostTcfResponseResult, setuidContext)); + } - Future.all( - tcfDefinerService.isAllowedForHostVendorId(tcfContext), - tcfDefinerService.resultForBidderNames( - Collections.singleton(bidderName), tcfContext, accountGdprConfig)) - .onComplete(hostTcfResponseResult -> respondByTcfResponse( - hostTcfResponseResult, - bidderName, - setuidContext)); - } else { - final Throwable error = setuidContextResult.cause(); - handleErrors(error, routingContext, null); + private void validateSetuidContext(SetuidContext setuidContext) { + if (StringUtils.isBlank(setuidContext.getBidderQueryParam())) { + throw new InvalidRequestException("\"bidder\" query param is required"); } - } - private void validateSetuidContext(SetuidContext setuidContext, String bidderCookieFamily) { - final String cookieName = setuidContext.getCookieName(); - final boolean isCookieNameBlank = StringUtils.isBlank(cookieName); - if (isCookieNameBlank || !cookieNameToBidderAndSyncType.containsKey(cookieName)) { - final String cookieNameError = isCookieNameBlank ? "required" : "invalid"; - throw new InvalidRequestException("\"bidder\" query param is " + cookieNameError); + final Usersyncer usersyncer = setuidContext.getUsersyncer(); + if (usersyncer == null) { + throw new InvalidRequestException("\"bidder\" query param is invalid"); } final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); if (tcfContext.isInGdprScope() && !tcfContext.isConsentValid()) { - metrics.updateUserSyncTcfInvalidMetric(bidderCookieFamily); + metrics.updateUserSyncTcfInvalidMetric(usersyncer.getCookieFamilyName()); throw new InvalidRequestException("Consent string is invalid"); } @@ -273,7 +261,7 @@ private void validateSetuidContext(SetuidContext setuidContext, String bidderCoo final ActivityInfrastructure activityInfrastructure = setuidContext.getActivityInfrastructure(); final ActivityInvocationPayload activityInvocationPayload = TcfContextActivityInvocationPayload.of( - ActivityInvocationPayloadImpl.of(ComponentType.BIDDER, bidderCookieFamily), + ActivityInvocationPayloadImpl.of(ComponentType.BIDDER, usersyncer.getCookieFamilyName()), tcfContext); if (!activityInfrastructure.isAllowed(Activity.SYNC_USER, activityInvocationPayload)) { @@ -282,11 +270,11 @@ private void validateSetuidContext(SetuidContext setuidContext, String bidderCoo } private void respondByTcfResponse(AsyncResult hostTcfResponseResult, - String bidderName, SetuidContext setuidContext) { final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); final RoutingContext routingContext = setuidContext.getRoutingContext(); + final String cookieFamilyName = setuidContext.getUsersyncer().getCookieFamilyName(); if (hostTcfResponseResult.succeeded()) { final CompositeFuture compositeFuture = hostTcfResponseResult.result(); @@ -295,7 +283,7 @@ private void respondByTcfResponse(AsyncResult hostTcfResponseRe final Map vendorIdToAction = bidderTcfResponse.getActions(); final PrivacyEnforcementAction action = vendorIdToAction != null - ? vendorIdToAction.get(bidderName) + ? vendorIdToAction.get(setuidContext.getUsersyncer().getBidder()) : null; final boolean notInGdprScope = BooleanUtils.isFalse(bidderTcfResponse.getUserInGdprScope()); @@ -304,7 +292,7 @@ private void respondByTcfResponse(AsyncResult hostTcfResponseRe if (hostVendorTcfResponse.isVendorAllowed() && isBidderVendorAllowed) { respondWithCookie(setuidContext); } else { - metrics.updateUserSyncTcfBlockedMetric(setuidContext.getCookieName()); + metrics.updateUserSyncTcfBlockedMetric(cookieFamilyName); final HttpResponseStatus status = new HttpResponseStatus(UNAVAILABLE_FOR_LEGAL_REASONS, "Unavailable for legal reasons"); @@ -319,7 +307,7 @@ private void respondByTcfResponse(AsyncResult hostTcfResponseRe } } else { final Throwable error = hostTcfResponseResult.cause(); - metrics.updateUserSyncTcfBlockedMetric(setuidContext.getCookieName()); + metrics.updateUserSyncTcfBlockedMetric(cookieFamilyName); handleErrors(error, routingContext, tcfContext); } } @@ -327,16 +315,16 @@ private void respondByTcfResponse(AsyncResult hostTcfResponseRe private void respondWithCookie(SetuidContext setuidContext) { final RoutingContext routingContext = setuidContext.getRoutingContext(); final String uid = routingContext.request().getParam(UID_PARAM); - final String bidder = setuidContext.getCookieName(); + final String cookieFamilyName = setuidContext.getUsersyncer().getCookieFamilyName(); final UpdateResult uidsCookieUpdateResult = uidsCookieService.updateUidsCookie( - setuidContext.getUidsCookie(), bidder, uid); + setuidContext.getUidsCookie(), cookieFamilyName, uid); uidsCookieService.splitUidsIntoCookies(uidsCookieUpdateResult.getValue()) .forEach(cookie -> addCookie(routingContext, cookie)); if (uidsCookieUpdateResult.isUpdated()) { - metrics.updateUserSyncSetsMetric(bidder); + metrics.updateUserSyncSetsMetric(cookieFamilyName); } final int statusCode = HttpResponseStatus.OK.code(); HttpUtil.executeSafely(routingContext, Endpoint.setuid, buildCookieResponseConsumer(setuidContext, statusCode)); @@ -344,7 +332,7 @@ private void respondWithCookie(SetuidContext setuidContext) { final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); final SetuidEvent setuidEvent = SetuidEvent.builder() .status(statusCode) - .bidder(bidder) + .bidder(cookieFamilyName) .uid(uid) .success(uidsCookieUpdateResult.isUpdated()) .build(); @@ -355,13 +343,17 @@ private Consumer buildCookieResponseConsumer(SetuidContext s int responseStatusCode) { final String format = setuidContext.getRoutingContext().request().getParam(UsersyncUtil.FORMAT_PARAMETER); - return shouldRespondWithPixel(format, setuidContext.getSyncType()) + return shouldRespondWithPixel(format, preferredUserSyncType(setuidContext.getUsersyncer())) ? response -> response.sendFile(PIXEL_FILE_PATH) : response -> response - .setStatusCode(responseStatusCode) - .putHeader(HttpHeaders.CONTENT_LENGTH, "0") - .putHeader(HttpHeaders.CONTENT_TYPE, HttpHeaders.TEXT_HTML) - .end(); + .setStatusCode(responseStatusCode) + .putHeader(HttpHeaders.CONTENT_LENGTH, "0") + .putHeader(HttpHeaders.CONTENT_TYPE, HttpHeaders.TEXT_HTML) + .end(); + } + + private static UsersyncMethodType preferredUserSyncType(Usersyncer usersyncer) { + return ObjectUtils.firstNonNull(usersyncer.getIframe(), usersyncer.getRedirect()).getType(); } private boolean shouldRespondWithPixel(String format, UsersyncMethodType syncType) { @@ -374,21 +366,21 @@ private void handleErrors(Throwable error, RoutingContext routingContext, TcfCon final HttpResponseStatus status; final String body; switch (error) { - case InvalidRequestException invalidRequestException -> { + case InvalidRequestException ignored -> { metrics.updateUserSyncBadRequestMetric(); status = HttpResponseStatus.BAD_REQUEST; body = "Invalid request format: " + message; } - case UnauthorizedUidsException unauthorizedUidsException -> { + case UnauthorizedUidsException ignored -> { metrics.updateUserSyncOptoutMetric(); status = HttpResponseStatus.UNAUTHORIZED; body = "Unauthorized: " + message; } - case UnavailableForLegalReasonsException unavailableForLegalReasonsException -> { + case UnavailableForLegalReasonsException ignored -> { status = HttpResponseStatus.valueOf(451); body = "Unavailable For Legal Reasons."; } - case InvalidAccountConfigException invalidAccountConfigException -> { + case InvalidAccountConfigException ignored -> { metrics.updateUserSyncBadRequestMetric(); status = HttpResponseStatus.BAD_REQUEST; body = "Invalid account configuration: " + message; diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index e7b290bc14a..88775fcb927 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -400,12 +400,12 @@ public void updateUserSyncBadRequestMetric() { userSync().incCounter(MetricName.bad_requests); } - public void updateUserSyncSetsMetric(String bidder) { - userSync().forBidder(bidder).incCounter(MetricName.sets); + public void updateUserSyncSetsMetric(String cookieFamilyName) { + userSync().forBidder(cookieFamilyName).incCounter(MetricName.sets); } - public void updateUserSyncTcfBlockedMetric(String bidder) { - userSync().forBidder(bidder).tcf().incCounter(MetricName.blocked); + public void updateUserSyncTcfBlockedMetric(String cookieFamilyName) { + userSync().forBidder(cookieFamilyName).tcf().incCounter(MetricName.blocked); } public void updateUserSyncSizeBlockedMetric(String cookieFamilyName) { @@ -416,8 +416,8 @@ public void updateUserSyncSizedOutMetric(String cookieFamilyName) { userSync().forBidder(cookieFamilyName).incCounter(MetricName.sizedout); } - public void updateUserSyncTcfInvalidMetric(String bidder) { - userSync().forBidder(bidder).tcf().incCounter(MetricName.invalid); + public void updateUserSyncTcfInvalidMetric(String cookieFamilyName) { + userSync().forBidder(cookieFamilyName).tcf().incCounter(MetricName.invalid); } public void updateUserSyncTcfInvalidMetric() { diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java index 1c0b27d1188..8e1b5abe08f 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java @@ -36,7 +36,6 @@ import java.util.Optional; import java.util.Properties; import java.util.Set; -import java.util.function.BiFunction; import java.util.function.Function; public class BidderDepsAssembler { @@ -52,7 +51,7 @@ public class BidderDepsAssembler { private String bidderName; private CFG configProperties; - private BiFunction usersyncerCreator; + private UsersyncerCreator usersyncerCreator; private Function> bidderCreator; private BidderDepsAssembler() { @@ -64,9 +63,7 @@ public static BidderDepsAssembler usersyncerCreator( - BiFunction usersyncerCreator) { - + public BidderDepsAssembler usersyncerCreator(UsersyncerCreator usersyncerCreator) { this.usersyncerCreator = usersyncerCreator; return this; } @@ -98,7 +95,7 @@ private BidderInstanceDeps coreDeps() { validateCoreCapabilities(bidderName, configProperties); return deps( bidderName, - usersyncer(configProperties, CookieFamilySource.ROOT), + usersyncer(bidderName, CookieFamilySource.ROOT, configProperties), BidderInfoCreator.create(configProperties), configProperties); } @@ -122,8 +119,8 @@ private BidderInstanceDeps aliasDeps(Map.Entry entry) { final Usersyncer usersyncer = Optional.ofNullable(aliasConfigProperties.getUsersync()) .map(UsersyncConfigurationProperties::getCookieFamilyName) - .map(familyName -> usersyncer(aliasMergedProperties, CookieFamilySource.ALIAS)) - .orElseGet(() -> usersyncer(aliasMergedProperties, CookieFamilySource.ROOT)); + .map(familyName -> usersyncer(alias, CookieFamilySource.ALIAS, aliasMergedProperties)) + .orElseGet(() -> usersyncer(alias, CookieFamilySource.ROOT, aliasMergedProperties)); return deps( alias, @@ -146,11 +143,11 @@ private BidderInstanceDeps deps(String bidderName, .build(); } - private Usersyncer usersyncer(CFG configProperties, CookieFamilySource cookieFamilySource) { + private Usersyncer usersyncer(String bidder, CookieFamilySource cookieFamilySource, CFG configProperties) { final UsersyncConfigurationProperties usersync = configProperties.getUsersync(); final boolean usersyncPresent = usersync != null && ObjectUtils.anyNotNull(usersync.getRedirect(), usersync.getIframe()); - return usersyncPresent ? usersyncerCreator.apply(usersync, cookieFamilySource) : null; + return usersyncPresent ? usersyncerCreator.createAndValidate(bidder, usersync, cookieFamilySource) : null; } private Bidder bidder(CFG configProperties) { diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java b/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java index b5606c719b9..0ca786fb927 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java @@ -1,5 +1,7 @@ package org.prebid.server.spring.config.bidder.util; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.UsersyncMethod; import org.prebid.server.bidder.UsersyncMethodType; @@ -12,41 +14,37 @@ import org.prebid.server.util.HttpUtil; import java.util.Objects; -import java.util.function.BiFunction; +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class UsersyncerCreator { - private UsersyncerCreator() { - } - - public static BiFunction create( - String externalUrl) { + private final String externalUrl; - return (usersyncConfig, cookieFamilySource) -> - createAndValidate(usersyncConfig, cookieFamilySource, externalUrl); + public static UsersyncerCreator create(String externalUrl) { + return new UsersyncerCreator(externalUrl); } - private static Usersyncer createAndValidate(UsersyncConfigurationProperties usersync, - CookieFamilySource cookieFamilySource, - String externalUrl) { + public Usersyncer createAndValidate(String bidder, + UsersyncConfigurationProperties usersync, + CookieFamilySource cookieFamilySource) { final String cookieFamilyName = usersync.getCookieFamilyName(); final UsersyncBidderRegulationScopeProperties skipwhenConfig = usersync.getSkipwhen(); return Usersyncer.of( usersync.getEnabled(), + bidder, cookieFamilyName, cookieFamilySource, - toMethod(UsersyncMethodType.IFRAME, usersync.getIframe(), cookieFamilyName, externalUrl), - toMethod(UsersyncMethodType.REDIRECT, usersync.getRedirect(), cookieFamilyName, externalUrl), + toMethod(UsersyncMethodType.IFRAME, usersync.getIframe(), cookieFamilyName), + toMethod(UsersyncMethodType.REDIRECT, usersync.getRedirect(), cookieFamilyName), skipwhenConfig != null && skipwhenConfig.isGdpr(), skipwhenConfig == null ? null : skipwhenConfig.getGppSid()); } - private static UsersyncMethod toMethod(UsersyncMethodType type, - UsersyncMethodConfigurationProperties properties, - String cookieFamilyName, - String externalUrl) { + private UsersyncMethod toMethod(UsersyncMethodType type, + UsersyncMethodConfigurationProperties properties, + String cookieFamilyName) { if (properties == null) { return null; diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 84ec69ecddb..c16f3874890 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -305,7 +305,7 @@ public void setUp() { given(bidderCatalog.isValidName(anyString())).willReturn(true); given(bidderCatalog.isActive(anyString())).willReturn(true); given(bidderCatalog.usersyncerByName(anyString())) - .willReturn(Optional.of(Usersyncer.of("cookieFamily", null, null, false, null))); + .willReturn(Optional.of(Usersyncer.of("bidder", "cookieFamily", null, null, false, null))); given(bidderCatalog.bidderInfoByName(anyString())).willReturn(BidderInfo.create( true, null, diff --git a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java index 96861372cd3..ae2cbc14697 100644 --- a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java +++ b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java @@ -218,7 +218,7 @@ public void metaInfoByNameShouldReturnNullForUnknownBidder() { @Test public void usersyncerByNameShouldReturnUsersyncerForKnownBidderIgnoringCase() { // given - final Usersyncer usersyncer = Usersyncer.of("name", null, null, false, null); + final Usersyncer usersyncer = Usersyncer.of("BIDder", "name", null, null, false, null); final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder() .name("BIDder") .deprecatedNames(emptyList()) @@ -233,7 +233,7 @@ public void usersyncerByNameShouldReturnUsersyncerForKnownBidderIgnoringCase() { @Test public void cookieFamilyNameShouldReturnCookieFamilyForKnownBidderIgnoringCase() { // given - final Usersyncer usersyncer = Usersyncer.of("name", null, null, false, null); + final Usersyncer usersyncer = Usersyncer.of("BIDder", "name", null, null, false, null); final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder() .name("BIDder") .deprecatedNames(emptyList()) @@ -310,7 +310,12 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { .name("bidder-with-usersync") .deprecatedNames(emptyList()) .bidderInfo(infoOfBidderWithUsersyncConfig) - .usersyncer(Usersyncer.of("bidder-with-usersync-family", null, null, false, null)) + .usersyncer(Usersyncer.of("bidder-with-usersync", + "bidder-with-usersync-family", + null, + null, + false, + null)) .build())), BidderDeps.of(singletonList(BidderInstanceDeps.builder() .name("bidder-without-usersync") @@ -320,7 +325,12 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { BidderDeps.of(singletonList(BidderInstanceDeps.builder() .name("disabled-bidder-with-usersync") .bidderInfo(infoOfDisabledBidderWithUsersyncConfig) - .usersyncer(Usersyncer.of("isabled-bidder-with-usersync-family", null, null, false, null)) + .usersyncer(Usersyncer.of("disabled-bidder-with-usersync", + "disabled-bidder-with-usersync-family", + null, + null, + false, + null)) .deprecatedNames(emptyList()) .build()))); diff --git a/src/test/java/org/prebid/server/bidder/UsersyncMethodChooserTest.java b/src/test/java/org/prebid/server/bidder/UsersyncMethodChooserTest.java index b129a075dce..a2f930f506b 100644 --- a/src/test/java/org/prebid/server/bidder/UsersyncMethodChooserTest.java +++ b/src/test/java/org/prebid/server/bidder/UsersyncMethodChooserTest.java @@ -58,7 +58,12 @@ public void shouldReturnRedirectMethodWhenIframeMethodFilterExcludeAndNullBidder null, CookieSyncRequest.FilterType.exclude), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); + final Usersyncer usersyncer = Usersyncer.of(null, + null, + iframeMethod("url"), + redirectMethod("url"), + false, + null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -93,7 +98,12 @@ public void shouldReturnSecondaryMethodWhenInMethodFilterExcludeList() { mapper.createArrayNode().add(BIDDER), CookieSyncRequest.FilterType.exclude), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); + final Usersyncer usersyncer = Usersyncer.of(null, + null, + iframeMethod("url"), + redirectMethod("url"), + false, + null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -111,7 +121,12 @@ public void shouldReturnSecondaryMethodWhenMethodFilterExcludesAll() { new TextNode("*"), CookieSyncRequest.FilterType.exclude), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); + final Usersyncer usersyncer = Usersyncer.of(null, + null, + iframeMethod("url"), + redirectMethod("url"), + false, + null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -180,7 +195,12 @@ public void shouldReturnSecondaryMethodWhenNotInMethodFilterIncludeList() { mapper.createArrayNode().add("anotherbidder"), CookieSyncRequest.FilterType.include), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); + final Usersyncer usersyncer = Usersyncer.of(null, + null, + iframeMethod("url"), + redirectMethod("url"), + false, + null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -231,7 +251,12 @@ public void shouldReturnSecondaryMethodWhenMethodFilterIncludeListIsNotArray() { new IntNode(1), CookieSyncRequest.FilterType.include), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); + final Usersyncer usersyncer = Usersyncer.of(null, + null, + iframeMethod("url"), + redirectMethod("url"), + false, + null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER); @@ -248,7 +273,12 @@ public void shouldReturnSecondaryMethodWhenMethodFilterIncludeListIsNotStringArr mapper.createArrayNode().add(1), CookieSyncRequest.FilterType.include), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); + final Usersyncer usersyncer = Usersyncer.of(null, + null, + iframeMethod("url"), + redirectMethod("url"), + false, + null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -268,7 +298,12 @@ public void shouldReturnSecondaryMethodWhenPrimaryIsFilteredOutAndSecondIsNot() CookieSyncRequest.MethodFilter.of( new TextNode("*"), CookieSyncRequest.FilterType.include)); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); + final Usersyncer usersyncer = Usersyncer.of(null, + null, + iframeMethod("url"), + redirectMethod("url"), + false, + null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -288,7 +323,12 @@ public void shouldReturnNullWhenPrimaryAndSecondaryAreFilteredOut() { CookieSyncRequest.MethodFilter.of( new TextNode("*"), CookieSyncRequest.FilterType.exclude)); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); + final Usersyncer usersyncer = Usersyncer.of(null, + null, + iframeMethod("url"), + redirectMethod("url"), + false, + null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -329,7 +369,7 @@ public void shouldReturnNullWhenPrimaryIsFilteredOutAndNoSecondary() { } private Usersyncer iframeUsersyncer(String url) { - return Usersyncer.of(null, iframeMethod(url), null, false, null); + return Usersyncer.of(null, null, iframeMethod(url), null, false, null); } private UsersyncMethod iframeMethod(String url) { diff --git a/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java b/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java index bafc6a11d16..3d7201b97eb 100644 --- a/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java +++ b/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java @@ -1261,7 +1261,7 @@ private void givenUsersyncerForBidder(boolean enabled, final UsersyncMethod usersyncMethod = givenUsersyncMethod(bidder); final Usersyncer usersyncer = Usersyncer.of( - enabled, cookieFamilyName, cookieFamilySource, usersyncMethod, null, gdpr, gppSid); + enabled, bidder, cookieFamilyName, cookieFamilySource, usersyncMethod, null, gdpr, gppSid); given(bidderCatalog.usersyncerByName(eq(bidder))).willReturn(Optional.of(usersyncer)); given(bidderCatalog.cookieFamilyName(eq(bidder))).willReturn(Optional.of(cookieFamilyName)); diff --git a/src/test/java/org/prebid/server/cookie/CoopSyncProviderTest.java b/src/test/java/org/prebid/server/cookie/CoopSyncProviderTest.java index 49ee89ff6d8..715724a5583 100644 --- a/src/test/java/org/prebid/server/cookie/CoopSyncProviderTest.java +++ b/src/test/java/org/prebid/server/cookie/CoopSyncProviderTest.java @@ -174,6 +174,7 @@ private void givenValidBidderWithCookieSync(String bidder) { given(bidderCatalog.isActive(bidder)).willReturn(true); given(bidderCatalog.usersyncerByName(bidder)).willReturn( Optional.of(Usersyncer.of( + bidder, "cookie-family-name", UsersyncMethod.builder().build(), null, diff --git a/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java b/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java index a6e24bed906..b60cbbce848 100644 --- a/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java +++ b/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java @@ -104,6 +104,7 @@ private void givenValidBidderWithCookieSync(String bidder) { given(bidderCatalog.cookieFamilyName(bidder)).willReturn(Optional.of(bidder + "-cookie-family")); given(bidderCatalog.usersyncerByName(bidder)).willReturn( Optional.of(Usersyncer.of( + bidder, "cookie-family-name", UsersyncMethod.builder().build(), null, diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index ed88f7f6fdd..73fa913b325 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -153,15 +153,15 @@ public void setUp() { given(bidderCatalog.isAlias(any())).willReturn(false); given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn( - Optional.of(Usersyncer.of(RUBICON, null, redirectMethod(), false, null))); + Optional.of(Usersyncer.of(RUBICON, RUBICON, null, redirectMethod(), false, null))); given(bidderCatalog.cookieFamilyName(eq(RUBICON))).willReturn(Optional.of(RUBICON)); given(bidderCatalog.usersyncerByName(eq(FACEBOOK))).willReturn( - Optional.of(Usersyncer.of(FACEBOOK, null, redirectMethod(), false, null))); + Optional.of(Usersyncer.of(FACEBOOK, FACEBOOK, null, redirectMethod(), false, null))); given(bidderCatalog.cookieFamilyName(eq(FACEBOOK))).willReturn(Optional.of(FACEBOOK)); given(bidderCatalog.usersyncerByName(eq(APPNEXUS))).willReturn( - Optional.of(Usersyncer.of(ADNXS, null, redirectMethod(), false, null))); + Optional.of(Usersyncer.of(APPNEXUS, ADNXS, null, redirectMethod(), false, null))); given(bidderCatalog.cookieFamilyName(eq(APPNEXUS))).willReturn(Optional.of(ADNXS)); given(activityInfrastructure.isAllowed(any(), any())) @@ -473,7 +473,9 @@ public void shouldRespondWithCookieFromRequestParam() throws IOException { } @Test - public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreDifferent() throws IOException { + public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreDifferentWhenCookieFamilyNameIsUsed() + throws IOException { + // given final UidsCookie uidsCookie = emptyUidsCookie(); given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) @@ -495,6 +497,31 @@ public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreD assertThat(decodedUids.getUids().get(ADNXS).getUid()).isEqualTo("J5VLCWQP-26-CWFT"); } + @Test + public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreDifferentWhenBidderNameIsUsed() + throws IOException { + + // given + final UidsCookie uidsCookie = emptyUidsCookie(); + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) + .willReturn(uidsCookie); + given(uidsCookieService.updateUidsCookie(uidsCookie, ADNXS, "J5VLCWQP-26-CWFT")) + .willReturn(updated(uidsCookie.updateUid(ADNXS, "J5VLCWQP-26-CWFT"))); + + given(httpRequest.getParam("bidder")).willReturn(APPNEXUS); + given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); + + // when + setuidHandler.handle(routingContext); + + // then + verify(routingContext, never()).addCookie(any(Cookie.class)); + final String encodedUidsCookie = getUidsCookie(); + final Uids decodedUids = decodeUids(encodedUidsCookie); + assertThat(decodedUids.getUids()).hasSize(1); + assertThat(decodedUids.getUids().get(ADNXS).getUid()).isEqualTo("J5VLCWQP-26-CWFT"); + } + @Test public void shouldSendPixelWhenFParamIsEqualToIWhenTypeIsIframe() { // given @@ -532,7 +559,7 @@ public void shouldSendEmptyResponseWhenFParamIsEqualToBWhenTypeIsRedirect() { given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); given(bidderCatalog.usersyncReadyBidders()).willReturn(singleton(RUBICON)); given(bidderCatalog.usersyncerByName(any())) - .willReturn(Optional.of(Usersyncer.of(RUBICON, null, redirectMethod(), false, null))); + .willReturn(Optional.of(Usersyncer.of(RUBICON, RUBICON, null, redirectMethod(), false, null))); setuidHandler = new SetuidHandler( 2000, @@ -569,7 +596,7 @@ public void shouldSendEmptyResponseWhenFParamNotDefinedAndTypeIsIframe() { .willReturn(updated(uidsCookie)); given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn( - Optional.of(Usersyncer.of(RUBICON, iframeMethod(), null, false, null))); + Optional.of(Usersyncer.of(RUBICON, RUBICON, iframeMethod(), null, false, null))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); @@ -611,7 +638,7 @@ public void shouldSendPixelWhenFParamNotDefinedAndTypeIsRedirect() { given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(bidderCatalog.usersyncReadyBidders()).willReturn(singleton(RUBICON)); given(bidderCatalog.usersyncerByName(any())) - .willReturn(Optional.of(Usersyncer.of(RUBICON, null, redirectMethod(), false, null))); + .willReturn(Optional.of(Usersyncer.of(RUBICON, RUBICON, null, redirectMethod(), false, null))); given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); setuidHandler = new SetuidHandler( @@ -844,11 +871,11 @@ public void shouldThrowExceptionInCaseOfBaseBidderCookieFamilyNameDuplicates() { .willReturn(Set.of(RUBICON, FACEBOOK, firstDuplicateName, secondDuplicateName, thirdDuplicateName)); given(bidderCatalog.isAlias(thirdDuplicateName)).willReturn(true); given(bidderCatalog.usersyncerByName(eq(firstDuplicateName))).willReturn( - Optional.of(Usersyncer.of(RUBICON, iframeMethod(), redirectMethod(), false, null))); + Optional.of(Usersyncer.of(RUBICON, RUBICON, iframeMethod(), redirectMethod(), false, null))); given(bidderCatalog.usersyncerByName(eq(secondDuplicateName))).willReturn( - Optional.of(Usersyncer.of(FACEBOOK, iframeMethod(), redirectMethod(), false, null))); + Optional.of(Usersyncer.of(FACEBOOK, FACEBOOK, iframeMethod(), redirectMethod(), false, null))); given(bidderCatalog.usersyncerByName(eq(thirdDuplicateName))).willReturn( - Optional.of(Usersyncer.of(FACEBOOK, iframeMethod(), redirectMethod(), false, null))); + Optional.of(Usersyncer.of(FACEBOOK, FACEBOOK, iframeMethod(), redirectMethod(), false, null))); final Executable exceptionSource = () -> new SetuidHandler( 2000, diff --git a/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java b/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java index 7337d5d8b1c..c57d13948e4 100644 --- a/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java +++ b/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java @@ -29,7 +29,7 @@ public void createShouldReturnUsersyncerWithConcatenatedExternalAndRedirectUrl() config.setRedirect(methodConfig); // when and then - assertThat(UsersyncerCreator.create("http://localhost:8000").apply(config, CookieFamilySource.ROOT)) + assertThat(UsersyncerCreator.create("http://localhost:8000").createAndValidate("rubicon", config, CookieFamilySource.ROOT)) .extracting(usersyncer -> usersyncer.getRedirect().getRedirectUrl()) .isEqualTo(""" http://localhost:8000/setuid\ @@ -56,7 +56,8 @@ public void createShouldValidateExternalUrl() { config.setRedirect(methodConfig); // given, when and then - assertThatThrownBy(() -> UsersyncerCreator.create(null).apply(config, CookieFamilySource.ROOT)) + assertThatThrownBy(() -> UsersyncerCreator.create(null) + .createAndValidate("bidder", config, CookieFamilySource.ROOT)) .hasCauseExactlyInstanceOf(MalformedURLException.class) .hasMessage("URL supplied is not valid: null"); } @@ -82,7 +83,7 @@ public void createShouldReturnUsersyncerWithPrimaryAndSecondaryMethods() { config.setRedirect(redirectConfig); // when - final Usersyncer result = UsersyncerCreator.create("http://localhost:8000").apply(config, CookieFamilySource.ROOT); + final Usersyncer result = UsersyncerCreator.create("http://localhost:8000").createAndValidate("rubicon", config, CookieFamilySource.ROOT); // then final UsersyncMethod expectedIframeMethod = UsersyncMethod.builder() @@ -117,7 +118,7 @@ public void createShouldReturnUsersyncerWithPrimaryAndSecondaryMethods() { .build(); assertThat(result).isEqualTo( - Usersyncer.of("rubicon", expectedIframeMethod, expectedRedirectMethod, false, null)); + Usersyncer.of("rubicon", "rubicon", expectedIframeMethod, expectedRedirectMethod, false, null)); } @Test @@ -133,7 +134,7 @@ public void createShouldTolerateMissingUid() { config.setRedirect(methodConfig); // when and then - assertThat(UsersyncerCreator.create("http://localhost:8000").apply(config, CookieFamilySource.ROOT)) + assertThat(UsersyncerCreator.create("http://localhost:8000").createAndValidate("rubicon", config, CookieFamilySource.ROOT)) .extracting(usersyncer -> usersyncer.getRedirect().getRedirectUrl()) .isEqualTo(""" http://localhost:8000/setuid\ From 7ac551f5f90c7cae1e47c725b4c1347274624669 Mon Sep 17 00:00:00 2001 From: Viktor Kryshtal Date: Tue, 14 Apr 2026 16:34:23 +0300 Subject: [PATCH 2/3] Update cookie sync endpoint to return bidder name instead of cookie family name in redirect url --- .../org/prebid/server/bidder/Usersyncer.java | 4 - .../server/cookie/CookieSyncService.java | 140 +++----- .../server/cookie/model/BiddersContext.java | 22 +- .../server/cookie/model/RejectionReason.java | 4 +- .../model/usersync/CookieFamilySource.java | 6 - .../bidder/util/BidderDepsAssembler.java | 14 +- .../config/bidder/util/UsersyncerCreator.java | 20 +- .../server/cookie/CookieSyncServiceTest.java | 317 ++++++++++-------- .../org/prebid/server/it/ApplicationTest.java | 4 +- .../bidder/util/UsersyncerCreatorTest.java | 10 +- 10 files changed, 241 insertions(+), 300 deletions(-) delete mode 100644 src/main/java/org/prebid/server/spring/config/bidder/model/usersync/CookieFamilySource.java diff --git a/src/main/java/org/prebid/server/bidder/Usersyncer.java b/src/main/java/org/prebid/server/bidder/Usersyncer.java index 2d8e8b03ac2..2aedc0b36fe 100644 --- a/src/main/java/org/prebid/server/bidder/Usersyncer.java +++ b/src/main/java/org/prebid/server/bidder/Usersyncer.java @@ -1,7 +1,6 @@ package org.prebid.server.bidder; import lombok.Value; -import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource; import java.util.List; @@ -14,8 +13,6 @@ public class Usersyncer { String cookieFamilyName; - CookieFamilySource cookieFamilySource; - UsersyncMethod iframe; UsersyncMethod redirect; @@ -35,7 +32,6 @@ public static Usersyncer of(String bidder, true, bidder, cookieFamilyName, - CookieFamilySource.ROOT, iframe, redirect, skipWhenInGdprScope, diff --git a/src/main/java/org/prebid/server/cookie/CookieSyncService.java b/src/main/java/org/prebid/server/cookie/CookieSyncService.java index 4ebd38c45ed..8d221d25ae7 100644 --- a/src/main/java/org/prebid/server/cookie/CookieSyncService.java +++ b/src/main/java/org/prebid/server/cookie/CookieSyncService.java @@ -3,6 +3,7 @@ import io.vertx.core.Future; import io.vertx.ext.web.RoutingContext; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.IteratorUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.SetUtils; import org.apache.commons.lang3.ObjectUtils; @@ -40,10 +41,8 @@ import org.prebid.server.settings.model.AccountCookieSyncConfig; import org.prebid.server.settings.model.AccountGdprConfig; import org.prebid.server.settings.model.AccountPrivacyConfig; -import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; -import org.prebid.server.util.StreamUtil; import java.util.ArrayList; import java.util.Collection; @@ -115,7 +114,8 @@ public Future processContext(CookieSyncContext cookieSyncCont .map(this::filterBiddersByGppSid) .map(this::applyRequestFilterSettings) .compose(this::applyPrivacyFilteringRules) - .map(this::filterInSyncBidders); + .map(this::filterInSyncBidders) + .map(this::applyLimitAndDeduplicate); } private CookieSyncContext validateCookieSyncContext(CookieSyncContext cookieSyncContext) { @@ -161,8 +161,6 @@ private CookieSyncContext resolveLimit(CookieSyncContext cookieSyncContext) { } private CookieSyncContext resolveBiddersToSync(CookieSyncContext cookieSyncContext) { - // TODO: Add multisync bidders from 1.a) - // TODO: filter that are done with multisync final List requestedBiddersAsList = new ArrayList<>( SetUtils.emptyIfNull(cookieSyncContext.getCookieSyncRequest().getBidders())); Collections.shuffle(requestedBiddersAsList); @@ -170,7 +168,6 @@ private CookieSyncContext resolveBiddersToSync(CookieSyncContext cookieSyncConte final BiddersContext updatedContext = cookieSyncContext.getBiddersContext().toBuilder() .requestedBidders(new LinkedHashSet<>(requestedBiddersAsList)) .coopSyncBidders(coopSyncProvider.coopSyncBidders(cookieSyncContext)) - .multiSyncBidders(Set.of()) .build(); return cookieSyncContext.with(updatedContext); @@ -285,6 +282,30 @@ private Future applyPrivacyFilteringRules(CookieSyncContext c .map(context -> filterDisallowedActivities(context, tcfContext)); } + private CookieSyncContext applyLimitAndDeduplicate(CookieSyncContext cookieSyncContext) { + final Set allowedBiddersByPriority = cookieSyncContext.getBiddersContext().allowedBiddersByPriority(); + + final Set cookieFamiliesSeen = new HashSet<>(); + final Set biddersWithDuplicateCookieFamilyName = new HashSet<>(); + final Iterator biddersIterator = allowedBiddersByPriority.iterator(); + + while (cookieFamiliesSeen.size() < cookieSyncContext.getLimit() && biddersIterator.hasNext()) { + final String bidder = biddersIterator.next(); + final String cookieFamilyName = bidderCatalog.cookieFamilyName(bidder).orElseThrow(); + + if (!cookieFamiliesSeen.add(cookieFamilyName)) { + biddersWithDuplicateCookieFamilyName.add(bidder); + } + } + final Set biddersOverLimit = new HashSet<>(IteratorUtils.toList(biddersIterator)); + + final BiddersContext updatedBiddersContext = cookieSyncContext.getBiddersContext() + .withRejectedBidders(biddersOverLimit, RejectionReason.OVER_LIMIT) + .withRejectedBidders(biddersWithDuplicateCookieFamilyName, + RejectionReason.DUPLICATE_COOKIE_FAMILY_NAME); + return cookieSyncContext.with(updatedBiddersContext); + } + private Future filterWithTcfResponse(HostVendorTcfResponse hostVendorTcfResponse, CookieSyncContext cookieSyncContext) { @@ -363,11 +384,9 @@ public CookieSyncResponse prepareResponse(CookieSyncContext cookieSyncContext) { ? CookieSyncStatus.OK : CookieSyncStatus.NO_COOKIE; - final Set biddersToSync = biddersToSync(cookieSyncContext); - final List statuses = ListUtils.union( - validStatuses(biddersToSync, cookieSyncContext), - debugStatuses(biddersToSync, cookieSyncContext)); + validStatuses(cookieSyncContext), + cookieSyncContext.isDebug() ? rejectionStatuses(cookieSyncContext) : Collections.emptyList()); final List warnings = cookieSyncContext.getWarnings(); final List resolvedWarnings = CollectionUtils.isNotEmpty(warnings) @@ -377,37 +396,8 @@ public CookieSyncResponse prepareResponse(CookieSyncContext cookieSyncContext) { return CookieSyncResponse.of(cookieSyncStatus, Collections.unmodifiableList(statuses), resolvedWarnings); } - private Set biddersToSync(CookieSyncContext cookieSyncContext) { - final Set allowedBiddersByPriority = allowedBiddersByPriority(cookieSyncContext); - - final Set cookieFamiliesToSync = new HashSet<>(); // multiple bidders may have same cookie families - final Set biddersToSync = new LinkedHashSet<>(); - final Iterator biddersIterator = allowedBiddersByPriority.iterator(); - - while (cookieFamiliesToSync.size() < cookieSyncContext.getLimit() && biddersIterator.hasNext()) { - final String bidder = biddersIterator.next(); - final String cookieFamilyName = bidderCatalog.cookieFamilyName(bidder).orElseThrow(); - - cookieFamiliesToSync.add(cookieFamilyName); - biddersToSync.add(bidder); - } - - return biddersToSync; - } - - private static Set allowedBiddersByPriority(CookieSyncContext cookieSyncContext) { - final BiddersContext biddersContext = cookieSyncContext.getBiddersContext(); - - final Set allowedBiddersByPriority = new LinkedHashSet<>(); - allowedBiddersByPriority.addAll(biddersContext.allowedRequestedBidders()); - allowedBiddersByPriority.addAll(biddersContext.allowedCoopSyncBidders()); - - return allowedBiddersByPriority; - } - - private List validStatuses(Set biddersToSync, CookieSyncContext cookieSyncContext) { - return biddersToSync.stream() - .filter(StreamUtil.distinctBy(bidder -> bidderCatalog.cookieFamilyName(bidder).orElseThrow())) + private List validStatuses(CookieSyncContext cookieSyncContext) { + return cookieSyncContext.getBiddersContext().allowedBidders().stream() .map(bidder -> validStatus(bidder, cookieSyncContext)) .toList(); } @@ -421,17 +411,17 @@ private BidderUsersyncStatus validStatus(String bidder, CookieSyncContext cookie final Privacy privacy = cookieSyncContext.getPrivacyContext().getPrivacy(); final String hostCookieUid = uidsCookieService.hostCookieUidToSync(routingContext, cookieFamilyName); - final UsersyncInfo usersyncInfo = toUsersyncInfo(usersyncMethod, cookieFamilyName, hostCookieUid, privacy); + final UsersyncInfo usersyncInfo = toUsersyncInfo(usersyncMethod, bidder, hostCookieUid, privacy); return BidderUsersyncStatus.builder() - .bidder(cookieFamilyName) // we are syncing cookie-family-names instead of bidder codes + .bidder(bidder) .noCookie(true) .usersync(usersyncInfo) .build(); } private UsersyncInfo toUsersyncInfo(UsersyncMethod usersyncMethod, - String cookieFamilyName, + String bidder, String hostCookieUid, Privacy privacy) { @@ -439,7 +429,7 @@ private UsersyncInfo toUsersyncInfo(UsersyncMethod usersyncMethod, if (hostCookieUid != null) { final String url = UsersyncUtil.CALLBACK_URL_TEMPLATE.formatted( - externalUrl, HttpUtil.encodeUrl(cookieFamilyName), HttpUtil.encodeUrl(hostCookieUid)); + externalUrl, HttpUtil.encodeUrl(bidder), HttpUtil.encodeUrl(hostCookieUid)); usersyncInfoBuilder .usersyncUrl(UsersyncUtil.enrichUrlWithFormat(url, UsersyncUtil.resolveFormat(usersyncMethod))) @@ -451,19 +441,6 @@ private UsersyncInfo toUsersyncInfo(UsersyncMethod usersyncMethod, .build(); } - private List debugStatuses(Set biddersToSync, CookieSyncContext cookieSyncContext) { - if (!cookieSyncContext.isDebug()) { - return Collections.emptyList(); - } - - final List debugStatuses = new ArrayList<>(); - debugStatuses.addAll(rejectionStatuses(cookieSyncContext)); - debugStatuses.addAll(limitStatuses(biddersToSync, cookieSyncContext)); - debugStatuses.addAll(aliasSyncedAsRootStatuses(biddersToSync, cookieSyncContext)); - - return debugStatuses; - } - private List rejectionStatuses(CookieSyncContext cookieSyncContext) { final BiddersContext biddersContext = cookieSyncContext.getBiddersContext(); return biddersContext.rejectedBidders().entrySet().stream() @@ -474,9 +451,7 @@ private List rejectionStatuses(CookieSyncContext cookieSyn } private BidderUsersyncStatus rejectionStatus(String bidder, RejectionReason reason, BiddersContext biddersContext) { - final String cookieFamilyName = bidderCatalog.cookieFamilyName(bidder).orElse(bidder); - BidderUsersyncStatus.BidderUsersyncStatusBuilder builder = BidderUsersyncStatus.builder() - .bidder(cookieFamilyName); + BidderUsersyncStatus.BidderUsersyncStatusBuilder builder = BidderUsersyncStatus.builder().bidder(bidder); final boolean requested = biddersContext.isRequested(bidder); final boolean coopSync = biddersContext.isCoopSync(bidder); @@ -493,51 +468,14 @@ private BidderUsersyncStatus rejectionStatus(String bidder, RejectionReason reas case ALREADY_IN_SYNC -> builder.conditionalError(requested, "Already in sync"); case REJECTED_BY_REGULATION_SCOPE -> builder.conditionalError( requested || coopSync, "Rejected by regulation scope"); + case OVER_LIMIT -> builder.conditionalError(requested, "Limit reached"); + case DUPLICATE_COOKIE_FAMILY_NAME -> builder.conditionalError( + requested, "Duplicate bidder synced as " + bidderCatalog.cookieFamilyName(bidder).orElseThrow()); }; return builder.build(); } - private List limitStatuses(Set biddersToSync, CookieSyncContext cookieSyncContext) { - final Set droppedDueToLimitBidders = SetUtils.difference( - cookieSyncContext.getBiddersContext().allowedRequestedBidders(), biddersToSync); - - return droppedDueToLimitBidders.stream() - .map(bidder -> BidderUsersyncStatus.builder() - .bidder(bidderCatalog.cookieFamilyName(bidder).orElseThrow()) - .error("limit reached") - .build()) - .toList(); - } - - private List aliasSyncedAsRootStatuses(Set biddersToSync, - CookieSyncContext cookieSyncContext) { - - final Set allowedRequestedBidders = cookieSyncContext.getBiddersContext().allowedRequestedBidders(); - - return biddersToSync.stream() - .filter(allowedRequestedBidders::contains) - .filter(this::isAliasSyncedAsRootFamily) - .map(this::warningForAliasSyncedAsRootFamily) - .toList(); - } - - private boolean isAliasSyncedAsRootFamily(String bidder) { - return bidderCatalog.isAlias(bidder) - && bidderCatalog.usersyncerByName(bidder) - .map(Usersyncer::getCookieFamilySource) - .filter(source -> source == CookieFamilySource.ROOT) - .isPresent(); - } - - private BidderUsersyncStatus warningForAliasSyncedAsRootFamily(String bidder) { - final String cookieFamilyName = bidderCatalog.cookieFamilyName(bidder).orElseThrow(); - return BidderUsersyncStatus.builder() - .bidder(bidder) - .error("synced as " + cookieFamilyName) - .build(); - } - private void updateCookieSyncTcfMetrics(BiddersContext biddersContext) { biddersContext.rejectedBidders().entrySet().stream() .filter(entry -> entry.getValue() == RejectionReason.REJECTED_BY_TCF) diff --git a/src/main/java/org/prebid/server/cookie/model/BiddersContext.java b/src/main/java/org/prebid/server/cookie/model/BiddersContext.java index 98a90604adc..45d0f784de1 100644 --- a/src/main/java/org/prebid/server/cookie/model/BiddersContext.java +++ b/src/main/java/org/prebid/server/cookie/model/BiddersContext.java @@ -10,6 +10,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -24,9 +25,6 @@ public class BiddersContext { @Builder.Default Set coopSyncBidders = new HashSet<>(); - @Builder.Default - Set multiSyncBidders = new HashSet<>(); - @Builder.Default Map rejectedBidders = new HashMap<>(); @@ -42,25 +40,21 @@ public boolean isCoopSync(String bidder) { } private Set involvedBidders() { - return SetUtils.union( - multiSyncBidders, - SetUtils.union(requestedBidders, coopSyncBidders)); + return SetUtils.union(requestedBidders, coopSyncBidders); } public Set allowedBidders() { return SetUtils.difference(involvedBidders(), rejectedBidders.keySet()); } - public Set allowedRequestedBidders() { - return SetUtils.difference(requestedBidders, rejectedBidders.keySet()); - } + public Set allowedBiddersByPriority() { + final Set allowedBiddersByPriority = new LinkedHashSet<>(); - public Set allowedCoopSyncBidders() { - return SetUtils.difference(coopSyncBidders, rejectedBidders.keySet()); - } + allowedBiddersByPriority.addAll(requestedBidders); + allowedBiddersByPriority.addAll(coopSyncBidders); + allowedBiddersByPriority.removeAll(rejectedBidders.keySet()); - public Set allowedMultisyncBidders() { - return SetUtils.difference(multiSyncBidders, rejectedBidders.keySet()); + return allowedBiddersByPriority; } public BiddersContext withRejectedBidder(String bidder, RejectionReason reason) { diff --git a/src/main/java/org/prebid/server/cookie/model/RejectionReason.java b/src/main/java/org/prebid/server/cookie/model/RejectionReason.java index 5130a3abb93..fbfd7a0aad3 100644 --- a/src/main/java/org/prebid/server/cookie/model/RejectionReason.java +++ b/src/main/java/org/prebid/server/cookie/model/RejectionReason.java @@ -11,5 +11,7 @@ public enum RejectionReason { DISABLED_USERSYNC, REJECTED_BY_FILTER, ALREADY_IN_SYNC, - REJECTED_BY_REGULATION_SCOPE + REJECTED_BY_REGULATION_SCOPE, + OVER_LIMIT, + DUPLICATE_COOKIE_FAMILY_NAME } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/CookieFamilySource.java b/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/CookieFamilySource.java deleted file mode 100644 index 42cae92a9ae..00000000000 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/CookieFamilySource.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.prebid.server.spring.config.bidder.model.usersync; - -public enum CookieFamilySource { - - ROOT, ALIAS -} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java index 8e1b5abe08f..f05f586c551 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java @@ -19,7 +19,6 @@ import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.model.MediaType; import org.prebid.server.spring.config.bidder.model.MetaInfo; -import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource; import org.prebid.server.spring.config.bidder.model.usersync.UsersyncConfigurationProperties; import org.prebid.server.spring.env.YamlPropertySourceFactory; import org.springframework.boot.context.properties.bind.Binder; @@ -95,7 +94,7 @@ private BidderInstanceDeps coreDeps() { validateCoreCapabilities(bidderName, configProperties); return deps( bidderName, - usersyncer(bidderName, CookieFamilySource.ROOT, configProperties), + usersyncer(bidderName, configProperties), BidderInfoCreator.create(configProperties), configProperties); } @@ -117,14 +116,9 @@ private BidderInstanceDeps aliasDeps(Map.Entry entry) { validateCapabilities(alias, aliasMergedProperties, bidderName, configProperties); - final Usersyncer usersyncer = Optional.ofNullable(aliasConfigProperties.getUsersync()) - .map(UsersyncConfigurationProperties::getCookieFamilyName) - .map(familyName -> usersyncer(alias, CookieFamilySource.ALIAS, aliasMergedProperties)) - .orElseGet(() -> usersyncer(alias, CookieFamilySource.ROOT, aliasMergedProperties)); - return deps( alias, - usersyncer, + usersyncer(alias, aliasMergedProperties), BidderInfoCreator.create(aliasMergedProperties, bidderName), aliasMergedProperties); } @@ -143,11 +137,11 @@ private BidderInstanceDeps deps(String bidderName, .build(); } - private Usersyncer usersyncer(String bidder, CookieFamilySource cookieFamilySource, CFG configProperties) { + private Usersyncer usersyncer(String bidder, CFG configProperties) { final UsersyncConfigurationProperties usersync = configProperties.getUsersync(); final boolean usersyncPresent = usersync != null && ObjectUtils.anyNotNull(usersync.getRedirect(), usersync.getIframe()); - return usersyncPresent ? usersyncerCreator.createAndValidate(bidder, usersync, cookieFamilySource) : null; + return usersyncPresent ? usersyncerCreator.createAndValidate(bidder, usersync) : null; } private Bidder bidder(CFG configProperties) { diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java b/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java index 0ca786fb927..785e4a39bfd 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java @@ -7,7 +7,6 @@ import org.prebid.server.bidder.UsersyncMethodType; import org.prebid.server.bidder.UsersyncUtil; import org.prebid.server.bidder.Usersyncer; -import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource; import org.prebid.server.spring.config.bidder.model.usersync.UsersyncBidderRegulationScopeProperties; import org.prebid.server.spring.config.bidder.model.usersync.UsersyncConfigurationProperties; import org.prebid.server.spring.config.bidder.model.usersync.UsersyncMethodConfigurationProperties; @@ -24,9 +23,7 @@ public static UsersyncerCreator create(String externalUrl) { return new UsersyncerCreator(externalUrl); } - public Usersyncer createAndValidate(String bidder, - UsersyncConfigurationProperties usersync, - CookieFamilySource cookieFamilySource) { + public Usersyncer createAndValidate(String bidder, UsersyncConfigurationProperties usersync) { final String cookieFamilyName = usersync.getCookieFamilyName(); final UsersyncBidderRegulationScopeProperties skipwhenConfig = usersync.getSkipwhen(); @@ -35,16 +32,15 @@ public Usersyncer createAndValidate(String bidder, usersync.getEnabled(), bidder, cookieFamilyName, - cookieFamilySource, - toMethod(UsersyncMethodType.IFRAME, usersync.getIframe(), cookieFamilyName), - toMethod(UsersyncMethodType.REDIRECT, usersync.getRedirect(), cookieFamilyName), + toMethod(UsersyncMethodType.IFRAME, usersync.getIframe(), bidder), + toMethod(UsersyncMethodType.REDIRECT, usersync.getRedirect(), bidder), skipwhenConfig != null && skipwhenConfig.isGdpr(), skipwhenConfig == null ? null : skipwhenConfig.getGppSid()); } private UsersyncMethod toMethod(UsersyncMethodType type, - UsersyncMethodConfigurationProperties properties, - String cookieFamilyName) { + UsersyncMethodConfigurationProperties properties, + String bidder) { if (properties == null) { return null; @@ -53,14 +49,14 @@ private UsersyncMethod toMethod(UsersyncMethodType type, return UsersyncMethod.builder() .type(type) .usersyncUrl(Objects.requireNonNull(properties.getUrl())) - .redirectUrl(toRedirectUrl(cookieFamilyName, externalUrl, properties.getUidMacro())) + .redirectUrl(toRedirectUrl(bidder, externalUrl, properties.getUidMacro())) .supportCORS(properties.getSupportCors()) .formatOverride(properties.getFormatOverride()) .build(); } - private static String toRedirectUrl(String cookieFamilyName, String externalUri, String uidMacro) { + private static String toRedirectUrl(String bidder, String externalUri, String uidMacro) { return UsersyncUtil.CALLBACK_URL_TEMPLATE.formatted( - HttpUtil.validateUrl(externalUri), cookieFamilyName, StringUtils.defaultString(uidMacro)); + HttpUtil.validateUrl(externalUri), bidder, StringUtils.defaultString(uidMacro)); } } diff --git a/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java b/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java index 3d7201b97eb..7587ea2611c 100644 --- a/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java +++ b/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java @@ -2,6 +2,7 @@ import io.vertx.core.Future; import io.vertx.ext.web.RoutingContext; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -39,11 +40,9 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountCookieSyncConfig; import org.prebid.server.settings.model.AccountCoopSyncConfig; -import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -97,7 +96,7 @@ public void setUp() { given(hostVendorTcfDefinerService.isAllowedForHostVendorId(any())) .willReturn(Future.succeededFuture(HostVendorTcfResponse.allowedVendor())); given(hostVendorTcfDefinerService.resultForBidderNames(anySet(), any(), any())) - .willReturn(Future.succeededFuture(TcfResponse.of(false, emptyMap(), "country"))); + .willReturn(Future.succeededFuture(TcfResponse.of(false, emptyMap(), "country"))); given(activityInfrastructure.isAllowed(any(), any())) .willReturn(true); @@ -317,7 +316,6 @@ public void processContextShouldResolveBiddersToSync() { .satisfies(context -> { assertThat(context.requestedBidders()).containsExactly("requested-bidder"); assertThat(context.coopSyncBidders()).containsExactly("coop-sync-bidder"); - assertThat(context.multiSyncBidders()).isEmpty(); // TODO: add multisync }); } @@ -405,7 +403,6 @@ public void processContextShouldRejectBiddersWithDisabledUsersync() { false, "bidder-with-disabled-usersync", "bidder-with-disabled-usersync-cookie-family", - CookieFamilySource.ROOT, false, null); @@ -436,7 +433,6 @@ public void processContextShouldRejectBiddersWhenUserIsGdprScope() { true, "bidder-skipwhen-in-gdpr-scope", "bidder-skipwhen-in-gdpr-scope-cookie-family", - CookieFamilySource.ROOT, true, null); @@ -481,7 +477,6 @@ public void processContextShouldRejectBiddersWhenBidderShouldBeSkippedForSid() { true, "bidder-skipwhen-sid-1", "bidder-skipwhen-sid-1-cookie-family", - CookieFamilySource.ROOT, false, List.of(1)); @@ -489,7 +484,6 @@ public void processContextShouldRejectBiddersWhenBidderShouldBeSkippedForSid() { true, "bidder-skipwhen-sid-2", "bidder-skipwhen-sid-2-cookie-family", - CookieFamilySource.ROOT, false, List.of(2)); @@ -497,7 +491,6 @@ public void processContextShouldRejectBiddersWhenBidderShouldBeSkippedForSid() { true, "bidder-skipwhen-sid-1-3", "bidder-skipwhen-sid-1-3-cookie-family", - CookieFamilySource.ROOT, false, List.of(1, 3)); @@ -505,7 +498,6 @@ public void processContextShouldRejectBiddersWhenBidderShouldBeSkippedForSid() { true, "bidder-skipwhen-sid-absent", "bidder-skipwhen-sid-absent-cookie-family", - CookieFamilySource.ROOT, false, null); @@ -626,7 +618,7 @@ public void processContextShouldFilterInSyncBidders() { } @Test - public void processContextShouldFilterDisallowedByActivityInfrastructureBidders() { + public void processContextShouldRejectBiddersOverLimit() { // given givenCoopSyncBidders("coop-sync-bidder"); @@ -635,14 +627,43 @@ public void processContextShouldFilterDisallowedByActivityInfrastructureBidders( givenAllAllowedTcfResultForBidders("requested-bidder", "coop-sync-bidder"); - given(activityInfrastructure.isAllowed( - eq(Activity.SYNC_USER), - argThat(argument -> argument.componentType().equals(ComponentType.BIDDER) - && "requested-bidder".equals(argument.componentName())))) - .willReturn(false); + final CookieSyncRequest cookieSyncRequest = CookieSyncRequest.builder() + .limit(1) + .bidders(Set.of("requested-bidder")) + .build(); - final CookieSyncContext cookieSyncContext = givenCookieSyncContext(builder -> - builder.cookieSyncRequest(givenCookieSyncRequest("requested-bidder"))); + final CookieSyncContext cookieSyncContext = givenCookieSyncContext(cookieSyncContextBuilder -> + cookieSyncContextBuilder.cookieSyncRequest(cookieSyncRequest)); + + // when + final Future result = target.processContext(cookieSyncContext); + + // then + assertThat(result).isSucceeded() + .unwrap() + .extracting(CookieSyncContext::getBiddersContext) + .extracting(BiddersContext::allowedBidders) + .asInstanceOf(InstanceOfAssertFactories.COLLECTION) + .hasSize(1); + } + + @Test + public void processContextShouldFavorRequestBidders() { + // given + givenCoopSyncBidders("coop-sync-bidder"); + + givenValidActiveBidders("requested-bidder", "coop-sync-bidder"); + givenUsersyncersForBidders("requested-bidder", "coop-sync-bidder"); + + givenAllAllowedTcfResultForBidders("requested-bidder", "coop-sync-bidder"); + + final CookieSyncRequest cookieSyncRequest = CookieSyncRequest.builder() + .limit(1) + .bidders(Set.of("requested-bidder")) + .build(); + + final CookieSyncContext cookieSyncContext = givenCookieSyncContext(cookieSyncContextBuilder -> + cookieSyncContextBuilder.cookieSyncRequest(cookieSyncRequest)); // when final Future result = target.processContext(cookieSyncContext); @@ -652,99 +673,127 @@ public void processContextShouldFilterDisallowedByActivityInfrastructureBidders( .unwrap() .extracting(CookieSyncContext::getBiddersContext) .extracting(BiddersContext::rejectedBidders) - .isEqualTo(Map.of("requested-bidder", RejectionReason.DISALLOWED_ACTIVITY)); + .isEqualTo(Map.of("coop-sync-bidder", RejectionReason.OVER_LIMIT)); } @Test - public void prepareResponseShouldReturnOkStatusWhenUidsCookieHasLiveUids() { + public void processContextShouldUseCoopSyncBiddersAfterRequestBidders() { // given - given(uidsCookie.hasLiveUids()).willReturn(true); - final CookieSyncContext cookieSyncContext = givenCookieSyncContext(UnaryOperator.identity()); + givenCoopSyncBidders("coop-sync-bidder"); + + givenValidActiveBidders("requested-bidder", "coop-sync-bidder"); + givenUsersyncersForBidders("requested-bidder", "coop-sync-bidder"); + + givenAllAllowedTcfResultForBidders("coop-sync-bidder"); + + final CookieSyncContext cookieSyncContext = givenCookieSyncContext(cookieSyncContextBuilder -> + cookieSyncContextBuilder.cookieSyncRequest(givenCookieSyncRequest("requested-bidder"))); // when - final CookieSyncResponse result = target.prepareResponse(cookieSyncContext); + final Future result = target.processContext(cookieSyncContext); // then - assertThat(result.getStatus()).isEqualTo(CookieSyncStatus.OK); + assertThat(result).isSucceeded() + .unwrap() + .extracting(CookieSyncContext::getBiddersContext) + .extracting(BiddersContext::allowedBidders) + .asInstanceOf(InstanceOfAssertFactories.set(String.class)) + .containsExactly("coop-sync-bidder"); } @Test - public void prepareResponseShouldReturnNoCookieStatusWhenUidsCookieHasNoLiveUids() { + public void processContextShouldRejectBiddersWithDuplicateCookieFamilyName() { // given - given(uidsCookie.hasLiveUids()).willReturn(false); - final CookieSyncContext cookieSyncContext = givenCookieSyncContext(UnaryOperator.identity()); + givenCoopSyncBidders("coop-sync-bidder"); + + givenValidActiveBidders("requested-bidder", "coop-sync-bidder"); + givenUsersyncerForBidder("requested-bidder", "cookie-family-name"); + givenUsersyncerForBidder("coop-sync-bidder", "cookie-family-name"); + + givenAllAllowedTcfResultForBidders("requested-bidder", "coop-sync-bidder"); + + final CookieSyncContext cookieSyncContext = givenCookieSyncContext(cookieSyncContextBuilder -> + cookieSyncContextBuilder.cookieSyncRequest(givenCookieSyncRequest("requested-bidder"))); // when - final CookieSyncResponse result = target.prepareResponse(cookieSyncContext); + final Future result = target.processContext(cookieSyncContext); // then - assertThat(result.getStatus()).isEqualTo(CookieSyncStatus.NO_COOKIE); + assertThat(result).isSucceeded() + .unwrap() + .extracting(CookieSyncContext::getBiddersContext) + .extracting(BiddersContext::rejectedBidders) + .isEqualTo(Map.of("coop-sync-bidder", RejectionReason.DUPLICATE_COOKIE_FAMILY_NAME)); } @Test - public void prepareResponseShouldLimitResponseStatuses() { + public void processContextShouldFilterDisallowedByActivityInfrastructureBidders() { // given + givenCoopSyncBidders("coop-sync-bidder"); + + givenValidActiveBidders("requested-bidder", "coop-sync-bidder"); givenUsersyncersForBidders("requested-bidder", "coop-sync-bidder"); - final Map bidderUsersyncMethods = Map.of( - "requested-bidder", givenUsersyncMethod("requested-bidder"), - "coop-sync-bidder", givenUsersyncMethod("coop-sync-bidder")); + givenAllAllowedTcfResultForBidders("requested-bidder", "coop-sync-bidder"); - final CookieSyncContext cookieSyncContext = givenCookieSyncContext( - cookieSyncContextBuilder -> cookieSyncContextBuilder.limit(1), - biddersContextBuilder -> biddersContextBuilder - .requestedBidders(singleton("requested-bidder")) - .coopSyncBidders(singleton("coop-sync-bidder")) - .bidderUsersyncMethod(bidderUsersyncMethods)); + given(activityInfrastructure.isAllowed( + eq(Activity.SYNC_USER), + argThat(argument -> argument.componentType().equals(ComponentType.BIDDER) + && "requested-bidder".equals(argument.componentName())))) + .willReturn(false); + + final CookieSyncContext cookieSyncContext = givenCookieSyncContext(builder -> + builder.cookieSyncRequest(givenCookieSyncRequest("requested-bidder"))); // when - final CookieSyncResponse cookieSyncResponse = target.prepareResponse(cookieSyncContext); + final Future result = target.processContext(cookieSyncContext); // then - assertThat(cookieSyncResponse.getBidderStatus()).hasSize(1); + assertThat(result).isSucceeded() + .unwrap() + .extracting(CookieSyncContext::getBiddersContext) + .extracting(BiddersContext::rejectedBidders) + .isEqualTo(Map.of("requested-bidder", RejectionReason.DISALLOWED_ACTIVITY)); } @Test - public void prepareResponseShouldFavourRequest() { + public void prepareResponseShouldReturnOkStatusWhenUidsCookieHasLiveUids() { // given - givenUsersyncersForBidders("requested-bidder", "coop-sync-bidder"); + given(uidsCookie.hasLiveUids()).willReturn(true); + final CookieSyncContext cookieSyncContext = givenCookieSyncContext(UnaryOperator.identity()); - final Map bidderUsersyncMethods = Map.of( - "requested-bidder", givenUsersyncMethod("requested-bidder"), - "coop-sync-bidder", givenUsersyncMethod("coop-sync-bidder")); + // when + final CookieSyncResponse result = target.prepareResponse(cookieSyncContext); - final CookieSyncContext cookieSyncContext = givenCookieSyncContext( - cookieSyncContextBuilder -> cookieSyncContextBuilder.limit(1), - biddersContextBuilder -> biddersContextBuilder - .requestedBidders(singleton("requested-bidder")) - .coopSyncBidders(singleton("coop-sync-bidder")) - .bidderUsersyncMethod(bidderUsersyncMethods)); + // then + assertThat(result.getStatus()).isEqualTo(CookieSyncStatus.OK); + } + + @Test + public void prepareResponseShouldReturnNoCookieStatusWhenUidsCookieHasNoLiveUids() { + // given + given(uidsCookie.hasLiveUids()).willReturn(false); + final CookieSyncContext cookieSyncContext = givenCookieSyncContext(UnaryOperator.identity()); // when - final CookieSyncResponse cookieSyncResponse = target.prepareResponse(cookieSyncContext); + final CookieSyncResponse result = target.prepareResponse(cookieSyncContext); // then - assertThat(cookieSyncResponse.getBidderStatus()) - .extracting(BidderUsersyncStatus::getBidder) - .containsExactly("requested-bidder-cookie-family"); + assertThat(result.getStatus()).isEqualTo(CookieSyncStatus.NO_COOKIE); } @Test - public void prepareResponseShouldFavourCoopSyncAfterRequest() { + public void prepareResponseShouldReturnBidderNameAndNotTheCookieFamilyName() { // given - givenUsersyncersForBidders("requested-bidder", "coop-sync-bidder"); + givenUsersyncerForBidder("requested-bidder", "requested-bidder-cookie-family"); final Map bidderUsersyncMethods = Map.of( - "requested-bidder", givenUsersyncMethod("requested-bidder"), - "coop-sync-bidder", givenUsersyncMethod("coop-sync-bidder")); + "requested-bidder", givenUsersyncMethod("requested-bidder")); - final CookieSyncContext cookieSyncContext = givenCookieSyncContext( - cookieSyncContextBuilder -> cookieSyncContextBuilder.limit(1), - biddersContextBuilder -> biddersContextBuilder - .requestedBidders(singleton("requested-bidder")) - .coopSyncBidders(singleton("coop-sync-bidder")) - .rejectedBidders(Map.of("requested-bidder", RejectionReason.REJECTED_BY_TCF)) - .bidderUsersyncMethod(bidderUsersyncMethods)); + final CookieSyncContext cookieSyncContext = givenCookieSyncContext(cookieSyncContextBuilder -> + cookieSyncContextBuilder.biddersContext(BiddersContext.builder() + .requestedBidders(Set.of("requested-bidder")) + .bidderUsersyncMethod(bidderUsersyncMethods).build())); // when final CookieSyncResponse cookieSyncResponse = target.prepareResponse(cookieSyncContext); @@ -752,7 +801,7 @@ public void prepareResponseShouldFavourCoopSyncAfterRequest() { // then assertThat(cookieSyncResponse.getBidderStatus()) .extracting(BidderUsersyncStatus::getBidder) - .containsExactly("coop-sync-bidder-cookie-family"); + .containsExactly("requested-bidder"); } @Test @@ -778,7 +827,7 @@ public void prepareResponseShouldNotReturnErrorsWhenDebugFalse() { // then assertThat(cookieSyncResponse.getBidderStatus()) .extracting(BidderUsersyncStatus::getBidder) - .containsExactly("coop-sync-bidder-cookie-family"); + .containsExactly("coop-sync-bidder"); } @Test @@ -833,7 +882,7 @@ public void prepareResponseShouldReturnInvalidBidderErrorOnlyForRequestedBidders // then assertThat(cookieSyncResponse.getBidderStatus()) - .containsExactly(errorStatus("requested-bidder-cookie-family", "Unsupported bidder")); + .containsExactly(errorStatus("requested-bidder", "Unsupported bidder")); } @Test @@ -862,7 +911,7 @@ public void prepareResponseShouldReturnDisabledBidderErrorOnlyForRequestedBidder // then assertThat(cookieSyncResponse.getBidderStatus()) - .containsExactly(errorStatus("requested-bidder-cookie-family", "Disabled bidder")); + .containsExactly(errorStatus("requested-bidder", "Disabled bidder")); } @Test @@ -891,8 +940,8 @@ public void prepareResponseShouldReturnTcfRejectedErrorForCoopSyncAndRequestedBi // then assertThat(cookieSyncResponse.getBidderStatus()).containsExactlyInAnyOrder( - errorStatus("requested-bidder-cookie-family", "Rejected by TCF"), - errorStatus("coop-sync-bidder-cookie-family", "Rejected by TCF")); + errorStatus("requested-bidder", "Rejected by TCF"), + errorStatus("coop-sync-bidder", "Rejected by TCF")); } @Test @@ -921,8 +970,8 @@ public void prepareResponseShouldReturnCcpaRejectedErrorForCoopSyncAndRequestedB // then assertThat(cookieSyncResponse.getBidderStatus()).containsExactlyInAnyOrder( - errorStatus("requested-bidder-cookie-family", "Rejected by CCPA"), - errorStatus("coop-sync-bidder-cookie-family", "Rejected by CCPA")); + errorStatus("requested-bidder", "Rejected by CCPA"), + errorStatus("coop-sync-bidder", "Rejected by CCPA")); } @Test @@ -974,8 +1023,8 @@ public void prepareResponseShouldReturnFilterRejectedErrorForCoopSyncAndRequeste // then assertThat(cookieSyncResponse.getBidderStatus()).containsExactlyInAnyOrder( - errorStatus("requested-bidder-cookie-family", "Rejected by request filter"), - errorStatus("coop-sync-bidder-cookie-family", "Rejected by request filter")); + errorStatus("requested-bidder", "Rejected by request filter"), + errorStatus("coop-sync-bidder", "Rejected by request filter")); } @Test @@ -1004,90 +1053,65 @@ public void prepareResponseShouldReturnAlreadyInSyncErrorOnlyForRequestedBidders // then assertThat(cookieSyncResponse.getBidderStatus()) - .containsExactly(errorStatus("requested-bidder-cookie-family", "Already in sync")); + .containsExactly(errorStatus("requested-bidder", "Already in sync")); } @Test - public void prepareResponseShouldReturnWarningForAliasesSyncedAsRootCookieFamilyWhenDebugTrue() { + public void prepareResponseShouldReturnOverLimitErrorOnlyForRequestedBiddersWhenDebugTrue() { // given - given(bidderCatalog.isValidName("alias")).willReturn(true); - given(bidderCatalog.isActive("alias")).willReturn(true); - given(bidderCatalog.isAlias("alias")).willReturn(true); - givenUsersyncerForBidder(true, "alias", "root-cookie-family", CookieFamilySource.ROOT, false, null); - - final CookieSyncContext cookieSyncContext = givenCookieSyncContext( - cookieSyncContextBuilder -> cookieSyncContextBuilder.debug(true), - biddersContextBuilder -> biddersContextBuilder - .requestedBidders(singleton("alias")) - .bidderUsersyncMethod(Map.of("alias", givenUsersyncMethod("alias")))); - - // when - final CookieSyncResponse result = target.prepareResponse(cookieSyncContext); - - // then - final BidderUsersyncStatus warningStatus = errorStatus("alias", "synced as root-cookie-family"); - final BidderUsersyncStatus validStatus = BidderUsersyncStatus.builder() - .bidder("root-cookie-family") - .noCookie(true) - .usersync(UsersyncInfo.of("https://alias-usersync-url.com", UsersyncMethodType.IFRAME, false)) - .build(); + givenUsersyncersForBidders("requested-bidder", "coop-sync-bidder"); - assertThat(result.getBidderStatus()).containsExactlyInAnyOrder(validStatus, warningStatus); - } + final Map bidderUsersyncMethods = Map.of( + "requested-bidder", givenUsersyncMethod("requested-bidder"), + "coop-sync-bidder", givenUsersyncMethod("coop-sync-bidder")); - @Test - public void prepareResponseShouldNotReturnWarningForAliasesSyncedAsAliasCookieFamilyWhenDebugFalse() { - // given - given(bidderCatalog.isValidName("alias")).willReturn(true); - given(bidderCatalog.isActive("alias")).willReturn(true); - given(bidderCatalog.isAlias("alias")).willReturn(true); - givenUsersyncerForBidder(true, "alias", "alias-cookie-family", CookieFamilySource.ALIAS, false, null); + final Map biddersRejectionReasons = Map.of( + "requested-bidder", RejectionReason.OVER_LIMIT, + "coop-sync-bidder", RejectionReason.OVER_LIMIT); final CookieSyncContext cookieSyncContext = givenCookieSyncContext( cookieSyncContextBuilder -> cookieSyncContextBuilder.debug(true), biddersContextBuilder -> biddersContextBuilder - .requestedBidders(singleton("alias")) - .bidderUsersyncMethod(Map.of("alias", givenUsersyncMethod("alias")))); + .requestedBidders(singleton("requested-bidder")) + .coopSyncBidders(singleton("coop-sync-bidder")) + .rejectedBidders(biddersRejectionReasons) + .bidderUsersyncMethod(bidderUsersyncMethods)); // when - final CookieSyncResponse result = target.prepareResponse(cookieSyncContext); + final CookieSyncResponse cookieSyncResponse = target.prepareResponse(cookieSyncContext); // then - final BidderUsersyncStatus status = BidderUsersyncStatus.builder() - .bidder("alias-cookie-family") - .noCookie(true) - .usersync(UsersyncInfo.of("https://alias-usersync-url.com", UsersyncMethodType.IFRAME, false)) - .build(); - - assertThat(result.getBidderStatus()).containsExactly(status); + assertThat(cookieSyncResponse.getBidderStatus()) + .containsExactly(errorStatus("requested-bidder", "Limit reached")); } @Test - public void prepareResponseShouldReturnErrorForBiddersThatWereNotIncludedInResponseDueToLimitWhenDebugTrue() { + public void prepareResponseShouldReturnDuplicateBidderErrorOnlyForRequestedBiddersWhenDebugTrue() { // given - givenValidActiveBidders("bidder1", "bidder2"); - givenUsersyncersForBidders("bidder1", "bidder2"); + givenUsersyncersForBidders("requested-bidder", "coop-sync-bidder"); + + final Map bidderUsersyncMethods = Map.of( + "requested-bidder", givenUsersyncMethod("requested-bidder"), + "coop-sync-bidder", givenUsersyncMethod("coop-sync-bidder")); + + final Map biddersRejectionReasons = Map.of( + "requested-bidder", RejectionReason.DUPLICATE_COOKIE_FAMILY_NAME, + "coop-sync-bidder", RejectionReason.DUPLICATE_COOKIE_FAMILY_NAME); final CookieSyncContext cookieSyncContext = givenCookieSyncContext( - cookieSyncContextBuilder -> cookieSyncContextBuilder.debug(true).limit(1), + cookieSyncContextBuilder -> cookieSyncContextBuilder.debug(true), biddersContextBuilder -> biddersContextBuilder - .requestedBidders(new LinkedHashSet<>(List.of("bidder1", "bidder2"))) // to preserve order - .bidderUsersyncMethod( - Map.of("bidder1", givenUsersyncMethod("bidder1"), - "bidder2", givenUsersyncMethod("bidder2")))); + .requestedBidders(singleton("requested-bidder")) + .coopSyncBidders(singleton("coop-sync-bidder")) + .rejectedBidders(biddersRejectionReasons) + .bidderUsersyncMethod(bidderUsersyncMethods)); // when - final CookieSyncResponse result = target.prepareResponse(cookieSyncContext); + final CookieSyncResponse cookieSyncResponse = target.prepareResponse(cookieSyncContext); // then - final BidderUsersyncStatus warningStatus = errorStatus("bidder2-cookie-family", "limit reached"); - final BidderUsersyncStatus validStatus = BidderUsersyncStatus.builder() - .bidder("bidder1-cookie-family") - .noCookie(true) - .usersync(UsersyncInfo.of("https://bidder1-usersync-url.com", UsersyncMethodType.IFRAME, false)) - .build(); - - assertThat(result.getBidderStatus()).containsExactlyInAnyOrder(validStatus, warningStatus); + assertThat(cookieSyncResponse.getBidderStatus()).containsExactly( + errorStatus("requested-bidder", "Duplicate bidder synced as requested-bidder-cookie-family")); } @Test @@ -1111,7 +1135,7 @@ public void prepareResponseShouldReturnCustomUsersyncUrlForHostCookieSync() { // then final String expectedUrl = """ https://external-url.com/setuid\ - ?bidder=host-bidder-cookie-family\ + ?bidder=host-bidder\ &gdpr=gdpr\ &gdpr_consent=consent-string\ &us_privacy=\ @@ -1121,7 +1145,7 @@ public void prepareResponseShouldReturnCustomUsersyncUrlForHostCookieSync() { &uid=bogus"""; final BidderUsersyncStatus status = BidderUsersyncStatus.builder() .noCookie(true) - .bidder("host-bidder-cookie-family") + .bidder("host-bidder") .usersync(UsersyncInfo.of(expectedUrl, UsersyncMethodType.IFRAME, false)) .build(); @@ -1149,7 +1173,7 @@ public void prepareResponseShouldReturnResponseWithWarningsIfNotEmpty() { // then final String expectedUrl = """ https://external-url.com/setuid\ - ?bidder=host-bidder-cookie-family\ + ?bidder=host-bidder\ &gdpr=gdpr\ &gdpr_consent=consent-string\ &us_privacy=\ @@ -1159,7 +1183,7 @@ public void prepareResponseShouldReturnResponseWithWarningsIfNotEmpty() { &uid=bogus"""; final BidderUsersyncStatus status = BidderUsersyncStatus.builder() .noCookie(true) - .bidder("host-bidder-cookie-family") + .bidder("host-bidder") .usersync(UsersyncInfo.of(expectedUrl, UsersyncMethodType.IFRAME, false)) .build(); @@ -1188,7 +1212,7 @@ public void prepareResponseShouldReturnResponseWithoutWarningsIfEmpty() { // then final String expectedUrl = """ https://external-url.com/setuid\ - ?bidder=host-bidder-cookie-family\ + ?bidder=host-bidder\ &gdpr=gdpr\ &gdpr_consent=consent-string\ &us_privacy=\ @@ -1198,7 +1222,7 @@ public void prepareResponseShouldReturnResponseWithoutWarningsIfEmpty() { &uid=bogus"""; final BidderUsersyncStatus status = BidderUsersyncStatus.builder() .noCookie(true) - .bidder("host-bidder-cookie-family") + .bidder("host-bidder") .usersync(UsersyncInfo.of(expectedUrl, UsersyncMethodType.IFRAME, false)) .build(); @@ -1249,19 +1273,22 @@ private void givenUsersyncersForBidders(String... bidders) { } private void givenUsersyncerForBidder(String bidder) { - givenUsersyncerForBidder(true, bidder, bidder + "-cookie-family", CookieFamilySource.ROOT, false, null); + givenUsersyncerForBidder(true, bidder, bidder + "-cookie-family", false, null); + } + + private void givenUsersyncerForBidder(String bidder, String cookieFamilyName) { + givenUsersyncerForBidder(true, bidder, cookieFamilyName, false, null); } private void givenUsersyncerForBidder(boolean enabled, String bidder, String cookieFamilyName, - CookieFamilySource cookieFamilySource, boolean gdpr, List gppSid) { final UsersyncMethod usersyncMethod = givenUsersyncMethod(bidder); final Usersyncer usersyncer = Usersyncer.of( - enabled, bidder, cookieFamilyName, cookieFamilySource, usersyncMethod, null, gdpr, gppSid); + enabled, bidder, cookieFamilyName, usersyncMethod, null, gdpr, gppSid); given(bidderCatalog.usersyncerByName(eq(bidder))).willReturn(Optional.of(usersyncer)); given(bidderCatalog.cookieFamilyName(eq(bidder))).willReturn(Optional.of(cookieFamilyName)); diff --git a/src/test/java/org/prebid/server/it/ApplicationTest.java b/src/test/java/org/prebid/server/it/ApplicationTest.java index 3a3710c6e6e..b659d577771 100644 --- a/src/test/java/org/prebid/server/it/ApplicationTest.java +++ b/src/test/java/org/prebid/server/it/ApplicationTest.java @@ -329,11 +329,11 @@ public void cookieSyncShouldReturnBidderStatusWithExpectedUsersyncInfo() { UsersyncMethodType.REDIRECT, false)) .build(), BidderUsersyncStatus.builder() - .bidder(APPNEXUS_COOKIE_FAMILY) + .bidder(APPNEXUS) .noCookie(true) .usersync(UsersyncInfo.of( "//usersync-url/getuid?http%3A%2F%2Flocalhost%3A8080%2Fsetuid%3Fbidder" - + "%3Dadnxs%26gdpr%3D1%26gdpr_consent%3D" + gdprConsent + + "%3Dappnexus%26gdpr%3D1%26gdpr_consent%3D" + gdprConsent + "%26us_privacy%3D1YNN" + "%26gpp%3D" + "%26gpp_sid%3D" diff --git a/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java b/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java index c57d13948e4..a83c2cfb2c0 100644 --- a/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java +++ b/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java @@ -4,7 +4,6 @@ import org.prebid.server.bidder.UsersyncMethod; import org.prebid.server.bidder.UsersyncMethodType; import org.prebid.server.bidder.Usersyncer; -import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource; import org.prebid.server.spring.config.bidder.model.usersync.UsersyncConfigurationProperties; import org.prebid.server.spring.config.bidder.model.usersync.UsersyncMethodConfigurationProperties; @@ -29,7 +28,7 @@ public void createShouldReturnUsersyncerWithConcatenatedExternalAndRedirectUrl() config.setRedirect(methodConfig); // when and then - assertThat(UsersyncerCreator.create("http://localhost:8000").createAndValidate("rubicon", config, CookieFamilySource.ROOT)) + assertThat(UsersyncerCreator.create("http://localhost:8000").createAndValidate("rubicon", config)) .extracting(usersyncer -> usersyncer.getRedirect().getRedirectUrl()) .isEqualTo(""" http://localhost:8000/setuid\ @@ -57,7 +56,7 @@ public void createShouldValidateExternalUrl() { // given, when and then assertThatThrownBy(() -> UsersyncerCreator.create(null) - .createAndValidate("bidder", config, CookieFamilySource.ROOT)) + .createAndValidate("bidder", config)) .hasCauseExactlyInstanceOf(MalformedURLException.class) .hasMessage("URL supplied is not valid: null"); } @@ -83,7 +82,8 @@ public void createShouldReturnUsersyncerWithPrimaryAndSecondaryMethods() { config.setRedirect(redirectConfig); // when - final Usersyncer result = UsersyncerCreator.create("http://localhost:8000").createAndValidate("rubicon", config, CookieFamilySource.ROOT); + final Usersyncer result = UsersyncerCreator.create("http://localhost:8000") + .createAndValidate("rubicon", config); // then final UsersyncMethod expectedIframeMethod = UsersyncMethod.builder() @@ -134,7 +134,7 @@ public void createShouldTolerateMissingUid() { config.setRedirect(methodConfig); // when and then - assertThat(UsersyncerCreator.create("http://localhost:8000").createAndValidate("rubicon", config, CookieFamilySource.ROOT)) + assertThat(UsersyncerCreator.create("http://localhost:8000").createAndValidate("rubicon", config)) .extracting(usersyncer -> usersyncer.getRedirect().getRedirectUrl()) .isEqualTo(""" http://localhost:8000/setuid\ From ab591336a1097366abd9d66730a213ecf4506cfa Mon Sep 17 00:00:00 2001 From: Viktor Kryshtal Date: Thu, 16 Apr 2026 13:26:18 +0300 Subject: [PATCH 3/3] Update setuid endpoint to accept only bidder name --- .../server/auction/model/SetuidContext.java | 2 +- .../prebid/server/handler/SetuidHandler.java | 36 ++++++------------- .../server/handler/SetuidHandlerTest.java | 29 ++------------- 3 files changed, 14 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/model/SetuidContext.java b/src/main/java/org/prebid/server/auction/model/SetuidContext.java index d543325ed1f..7b11bbe8a7e 100644 --- a/src/main/java/org/prebid/server/auction/model/SetuidContext.java +++ b/src/main/java/org/prebid/server/auction/model/SetuidContext.java @@ -27,7 +27,7 @@ public class SetuidContext { Account account; - String bidderQueryParam; + String bidder; @JsonIgnore Usersyncer usersyncer; diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index 0ad9e2b8fe4..7be5eb8c739 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -58,7 +58,6 @@ import org.prebid.server.vertx.verticles.server.application.ApplicationResource; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -89,7 +88,6 @@ public class SetuidHandler implements ApplicationResource { private final Metrics metrics; private final TimeoutFactory timeoutFactory; private final Map bidderToUsersyncer; - private final Map cookieNameToUsersyncer; public SetuidHandler(long defaultTimeout, UidsCookieService uidsCookieService, @@ -113,9 +111,14 @@ public SetuidHandler(long defaultTimeout, this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); + this.bidderToUsersyncer = collectUsersyncers(bidderCatalog); + } + + private static Map collectUsersyncers(BidderCatalog bidderCatalog) { validateUsersyncersDuplicates(bidderCatalog); - this.bidderToUsersyncer = collectUsersyncersByBidderName(bidderCatalog); - this.cookieNameToUsersyncer = collectUsersyncersByCookieName(bidderCatalog); + return bidderCatalog.usersyncReadyBidders().stream() + .map(bidder -> bidderCatalog.usersyncerByName(bidder).orElseThrow()) + .collect(Collectors.toMap(Usersyncer::getBidder, Function.identity())); } private static void validateUsersyncersDuplicates(BidderCatalog bidderCatalog) { @@ -146,20 +149,6 @@ private static boolean isAliasWithRootCookieFamilyName(BidderCatalog bidderCatal && parentCookieFamilyName.equals(bidderCookieFamilyName); } - private static Map collectUsersyncersByBidderName(BidderCatalog bidderCatalog) { - return bidderCatalog.usersyncReadyBidders().stream() - .map(bidder -> bidderCatalog.usersyncerByName(bidder).orElseThrow()) - .collect(Collectors.toMap(Usersyncer::getBidder, Function.identity())); - } - - private static Map collectUsersyncersByCookieName(BidderCatalog bidderCatalog) { - return bidderCatalog.usersyncReadyBidders().stream() - .sorted(Comparator.comparing(bidderName -> BooleanUtils.toInteger(bidderCatalog.isAlias(bidderName)))) - .filter(StreamUtil.distinctBy(bidderCatalog::cookieFamilyName)) - .map(bidderName -> bidderCatalog.usersyncerByName(bidderName).orElseThrow()) - .collect(Collectors.toMap(Usersyncer::getCookieFamilyName, Function.identity())); - } - @Override public List endpoints() { return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.setuid.value())); @@ -175,13 +164,10 @@ public void handle(RoutingContext routingContext) { private Future toSetuidContext(RoutingContext routingContext) { final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); final HttpServerRequest httpRequest = routingContext.request(); - final String bidderQueryParam = httpRequest.getParam(BIDDER_PARAM); + final String bidder = httpRequest.getParam(BIDDER_PARAM); final String requestAccount = httpRequest.getParam(ACCOUNT_PARAM); final Timeout timeout = timeoutFactory.create(defaultTimeout); - - final Usersyncer usersyncer = ObjectUtils.firstNonNull( - bidderToUsersyncer.get(bidderQueryParam), - cookieNameToUsersyncer.get(bidderQueryParam)); + final Usersyncer usersyncer = bidderToUsersyncer.get(bidder); return accountById(requestAccount, timeout) .compose(account -> setuidPrivacyContextFactory.contextFrom(httpRequest, account, timeout) @@ -190,7 +176,7 @@ private Future toSetuidContext(RoutingContext routingContext) { .uidsCookie(uidsCookie) .timeout(timeout) .account(account) - .bidderQueryParam(bidderQueryParam) + .bidder(bidder) .usersyncer(usersyncer) .privacyContext(privacyContext) .build())) @@ -239,7 +225,7 @@ private void processSetuidContext(SetuidContext setuidContext, RoutingContext ro } private void validateSetuidContext(SetuidContext setuidContext) { - if (StringUtils.isBlank(setuidContext.getBidderQueryParam())) { + if (StringUtils.isBlank(setuidContext.getBidder())) { throw new InvalidRequestException("\"bidder\" query param is required"); } diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index 73fa913b325..5ed265fab24 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -473,32 +473,7 @@ public void shouldRespondWithCookieFromRequestParam() throws IOException { } @Test - public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreDifferentWhenCookieFamilyNameIsUsed() - throws IOException { - - // given - final UidsCookie uidsCookie = emptyUidsCookie(); - given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) - .willReturn(uidsCookie); - given(uidsCookieService.updateUidsCookie(uidsCookie, ADNXS, "J5VLCWQP-26-CWFT")) - .willReturn(updated(uidsCookie.updateUid(ADNXS, "J5VLCWQP-26-CWFT"))); - - given(httpRequest.getParam("bidder")).willReturn(ADNXS); - given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); - - // when - setuidHandler.handle(routingContext); - - // then - verify(routingContext, never()).addCookie(any(Cookie.class)); - final String encodedUidsCookie = getUidsCookie(); - final Uids decodedUids = decodeUids(encodedUidsCookie); - assertThat(decodedUids.getUids()).hasSize(1); - assertThat(decodedUids.getUids().get(ADNXS).getUid()).isEqualTo("J5VLCWQP-26-CWFT"); - } - - @Test - public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreDifferentWhenBidderNameIsUsed() + public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreDifferent() throws IOException { // given @@ -744,7 +719,7 @@ public void shouldReturnMultipleCookies() throws IOException { public void shouldRespondWithCookieIfUserIsNotInGdprScope() throws IOException { // given given(tcfDefinerService.resultForVendorIds(anySet(), any())) - .willReturn(Future.succeededFuture(TcfResponse.of(false, emptyMap(), null))); + .willReturn(Future.succeededFuture(TcfResponse.of(false, emptyMap(), null))); final UidsCookie uidsCookie = emptyUidsCookie(); given(uidsCookieService.parseFromRequest(any(RoutingContext.class)))