From 882b4ef0c4d6994fae55bb51bf27f40a4383797f Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Thu, 21 Jan 2021 16:27:30 +0200 Subject: [PATCH 1/3] Add per-account /cookie_sync endpoint configuration --- docs/application-settings.md | 31 +++++++++++++++++-- .../settings/JdbcApplicationSettings.java | 8 ++++- .../prebid/server/settings/model/Account.java | 7 ++++- .../model/AccountCookieSyncConfig.java | 17 ++++++++++ .../settings/FileApplicationSettingsTest.java | 7 +++-- .../settings/JdbcApplicationSettingsTest.java | 11 ++++--- 6 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/prebid/server/settings/model/AccountCookieSyncConfig.java diff --git a/docs/application-settings.md b/docs/application-settings.md index 0f2ed0076c3..5ef0b2d1958 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -25,6 +25,9 @@ There are two ways to configure application settings: database and file. This do - `analytics-config.auction-events.` - defines which channels are supported by analytics for this account - `bid-validations.banner-creative-max-size` - Overrides creative max size validation for banners. - `status` - allows to mark account as `active` or `inactive`. +- `cookie-sync.default-limit` - if the "limit" isn't specified in the `/cookie_sync` request, this is what to use +- `cookie-sync.max-limit` - if the "limit" is specified in the `/cookie_sync` request, it can't be greater than this value +- `cookie-sync.default-coop-sync` - if the "coopSync" value isn't specified in the `/cookie_sync` request, use this Here are the definitions of the "purposes" that can be defined in the GDPR setting configurations: ``` @@ -77,6 +80,10 @@ accounts: auction-events: amp: true status: active + cookie-sync: + default-limit: 5 + max-limit: 8 + default-coop-sync: true gdpr: enabled: true integration-enabled: @@ -187,7 +194,7 @@ Prebid Server returns expected data in the expected order. Here's an example con settings: database: type: mysql - account-query: SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, bid_validations, status FROM accounts_account where uuid = ? LIMIT 1 + account-query: SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, bid_validations, status, config FROM accounts_account where uuid = ? LIMIT 1 ``` The SQL query for account must: @@ -203,7 +210,9 @@ The SQL query for account must: * maximum targeting attribute size, integer * default integration value, string * analytics configuration, JSON string, see below + * bid validations configuration, JSON string, see below * status, string. Expected values: "active", "inactive", NULL. Only "inactive" has any effect and only when settings.enforce-valid-account is on. + * consolidated configuration, JSON string, see below * specify a special single `%ACCOUNT_ID%` placeholder in the `WHERE` clause that will be replaced with account ID in runtime @@ -215,7 +224,7 @@ If a host company doesn't support a given field, or they have a different table settings: database: type: mysql - account-query: SELECT uuid, 'med', banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, 0, null, default_integration, '{}', '{}' FROM myaccountstable where uuid = ? LIMIT 1 + account-query: SELECT uuid, 'med', banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, 0, null, default_integration, '{}', '{}', status, '{}' FROM myaccountstable where uuid = ? LIMIT 1 ``` ### Configuration Details @@ -351,7 +360,7 @@ Valid values are: #### Analytics Validations configuration JSON -The `analytics_config` configuration column format: +The `analytics_config` configuration column format: ```json { @@ -363,6 +372,21 @@ The `analytics_config` configuration column format: } ``` +#### Consolidated configuration JSON + +The `config` column is envisioned as a new home for all configuration values. All new configuration values are added here. +The schema of this JSON document has following format so far: + +```json +{ + "cookie-sync": { + "default-limit": 5, + "max-limit": 8, + "default-coop-sync": true + } +} +``` + #### Creating the accounts table Traditionally the table name used by Prebid Server is `accounts_account`. No one remembers why. But here's SQL @@ -386,6 +410,7 @@ you could use to create your table: `analytics_config` varchar(512) DEFAULT NULL, `bid_validations` json DEFAULT NULL, `status` enum('active','inactive') DEFAULT 'active', + `config` json DEFAULT NULL, `updated_by` int(11) DEFAULT NULL, `updated_by_user` varchar(64) DEFAULT NULL, `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, diff --git a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java index 952d11e24a7..b42fe10eaa5 100644 --- a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java @@ -112,7 +112,7 @@ public Future getAccountById(String accountId, Timeout timeout) { return jdbcClient.executeQuery( selectAccountQuery, Collections.singletonList(accountId), - result -> mapToModelOrError(result, row -> Account.builder() + result -> mapToModelOrError(result, row -> partialAccountBuilderFrom(row.getString(13)) .id(row.getString(0)) .priceGranularity(row.getString(1)) .bannerCacheTtl(row.getInteger(2)) @@ -166,6 +166,12 @@ private static Future failedIfNull(T value, String id, String errorPrefix : Future.failedFuture(new PreBidException(String.format("%s not found: %s", errorPrefix, id))); } + private Account.AccountBuilder partialAccountBuilderFrom(String config) { + final Account partialAccount = toModel(config, Account.class); + + return partialAccount != null ? partialAccount.toBuilder() : Account.builder(); + } + private T toModel(String source, Class targetClass) { try { return source != null ? mapper.decodeValue(source, targetClass) : null; diff --git a/src/main/java/org/prebid/server/settings/model/Account.java b/src/main/java/org/prebid/server/settings/model/Account.java index 479c2bc680a..3495b38b486 100644 --- a/src/main/java/org/prebid/server/settings/model/Account.java +++ b/src/main/java/org/prebid/server/settings/model/Account.java @@ -1,10 +1,11 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Value; import org.apache.commons.lang3.ObjectUtils; -@Builder +@Builder(toBuilder = true) @Value public class Account { @@ -34,6 +35,9 @@ public class Account { AccountStatus status; + @JsonProperty("cookie-sync") + AccountCookieSyncConfig cookieSync; + public Account merge(Account another) { return Account.builder() .id(ObjectUtils.defaultIfNull(id, another.id)) @@ -50,6 +54,7 @@ public Account merge(Account another) { .analyticsConfig(ObjectUtils.defaultIfNull(analyticsConfig, another.analyticsConfig)) .bidValidations(ObjectUtils.defaultIfNull(bidValidations, another.bidValidations)) .status(ObjectUtils.defaultIfNull(status, another.status)) + .cookieSync(ObjectUtils.defaultIfNull(cookieSync, another.cookieSync)) .build(); } diff --git a/src/main/java/org/prebid/server/settings/model/AccountCookieSyncConfig.java b/src/main/java/org/prebid/server/settings/model/AccountCookieSyncConfig.java new file mode 100644 index 00000000000..574f7798772 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountCookieSyncConfig.java @@ -0,0 +1,17 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountCookieSyncConfig { + + @JsonProperty("default-limit") + Integer defaultLimit; + + @JsonProperty("max-limit") + Integer maxLimit; + + @JsonProperty("default-coop-sync") + Boolean defaultCoopSync; +} diff --git a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java index 9581f3cbb5f..b1fc59f26a4 100644 --- a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java @@ -11,9 +11,10 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.settings.model.AccountBidValidationConfig; +import org.prebid.server.settings.model.AccountCookieSyncConfig; import org.prebid.server.settings.model.AccountGdprConfig; -import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.settings.model.AccountStatus; +import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.settings.model.EnabledForRequestType; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -111,7 +112,8 @@ public void getAccountByIdShouldReturnPresentAccount() { + "bidValidations: {" + "banner-creative-max-size: 'enforce'" + "}," - + "status: 'active'" + + "status: 'active'," + + "cookie-sync: {default-limit: 5,max-limit: 8,default-coop-sync: true}" + "}" + "]")); @@ -149,6 +151,7 @@ public void getAccountByIdShouldReturnPresentAccount() { .analyticsConfig(AccountAnalyticsConfig.of(singletonMap("amp", true))) .bidValidations(AccountBidValidationConfig.of(BidValidationEnforcement.enforce)) .status(AccountStatus.active) + .cookieSync(AccountCookieSyncConfig.of(5, 8, true)) .build()); } diff --git a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java index 804791b7a9a..268157ec20e 100644 --- a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java @@ -25,6 +25,7 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.settings.model.AccountBidValidationConfig; +import org.prebid.server.settings.model.AccountCookieSyncConfig; import org.prebid.server.settings.model.AccountGdprConfig; import org.prebid.server.settings.model.AccountStatus; import org.prebid.server.settings.model.BidValidationEnforcement; @@ -64,7 +65,7 @@ public class JdbcApplicationSettingsTest extends VertxTest { private static final String SELECT_ACCOUNT_QUERY = "SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, " + "events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, " - + "default_integration, analytics_config, bid_validations, status " + + "default_integration, analytics_config, bid_validations, status, config " + "FROM accounts_account where uuid = %ACCOUNT_ID% LIMIT 1"; private static final String SELECT_QUERY = @@ -116,7 +117,7 @@ public static void beforeClass() throws SQLException { + "banner_cache_ttl INT, video_cache_ttl INT, events_enabled BIT, enforce_ccpa BIT, " + "tcf_config varchar(512), analytics_sampling_factor INT, truncate_target_attr INT, " + "default_integration varchar(64), analytics_config varchar(512), bid_validations varchar(512), " - + "status varchar(25));"); + + "status varchar(25), config varchar(4096));"); connection.createStatement().execute("CREATE TABLE s2sconfig_config (id SERIAL PRIMARY KEY, uuid varchar(40) " + "NOT NULL, config varchar(512));"); connection.createStatement().execute("CREATE TABLE stored_requests (id SERIAL PRIMARY KEY, " @@ -135,11 +136,12 @@ public static void beforeClass() throws SQLException { connection.createStatement().execute("insert into accounts_account " + "(uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, " + "tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config, " - + "bid_validations, status) " + + "bid_validations, status, config) " + "values ('1001','med', 100, 100, TRUE, TRUE, '{\"enabled\": true, " + "\"integration-enabled\": {\"amp\": true, \"app\": true, \"video\": true, \"web\": true}}', 1, 0, " + "'web', '{\"auction-events\": {\"amp\": true}}', '{\"banner-creative-max-size\": \"enforce\"}', " - + "'active');"); + + "'active', " + + "'{\"cookie-sync\": {\"default-limit\": 5, \"max-limit\": 8, \"default-coop-sync\": true}}');"); connection.createStatement().execute( "insert into s2sconfig_config (uuid, config) values ('adUnitConfigId', 'config');"); connection.createStatement().execute( @@ -213,6 +215,7 @@ public void getAccountByIdShouldReturnAccountWithAllFieldsPopulated(TestContext .analyticsConfig(AccountAnalyticsConfig.of(singletonMap("amp", true))) .bidValidations(AccountBidValidationConfig.of(BidValidationEnforcement.enforce)) .status(AccountStatus.active) + .cookieSync(AccountCookieSyncConfig.of(5, 8, true)) .build()); async.complete(); })); From 616727b5d874088e6fd83569d144054fee9d4d3e Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Thu, 21 Jan 2021 16:38:56 +0200 Subject: [PATCH 2/3] Fix default account merging --- .../config/model/AccountConfigurationProperties.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/org/prebid/server/spring/config/model/AccountConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/model/AccountConfigurationProperties.java index 3eac6b136cb..220324d45b6 100644 --- a/src/main/java/org/prebid/server/spring/config/model/AccountConfigurationProperties.java +++ b/src/main/java/org/prebid/server/spring/config/model/AccountConfigurationProperties.java @@ -5,6 +5,8 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAnalyticsConfig; +import org.prebid.server.settings.model.AccountBidValidationConfig; +import org.prebid.server.settings.model.AccountCookieSyncConfig; import org.prebid.server.settings.model.AccountGdprConfig; import org.prebid.server.settings.model.AccountStatus; @@ -32,8 +34,12 @@ public class AccountConfigurationProperties { private String analyticsConfig; + private String bidValidations; + private AccountStatus status; + private String cookieSync; + public Account toAccount(JacksonMapper mapper) { return Account.builder() .priceGranularity(getPriceGranularity()) @@ -46,7 +52,9 @@ public Account toAccount(JacksonMapper mapper) { .truncateTargetAttr(getTruncateTargetAttr()) .defaultIntegration(getDefaultIntegration()) .analyticsConfig(toModel(mapper, getAnalyticsConfig(), AccountAnalyticsConfig.class)) + .bidValidations(toModel(mapper, getBidValidations(), AccountBidValidationConfig.class)) .status(getStatus()) + .cookieSync(toModel(mapper, getCookieSync(), AccountCookieSyncConfig.class)) .build(); } From fce64aee2c5176fcc6f950b6d56752e47dd790f2 Mon Sep 17 00:00:00 2001 From: Sergii Chernysh Date: Fri, 22 Jan 2021 12:29:49 +0200 Subject: [PATCH 3/3] Use account configuration for cookie_sync endpoint in CookieSyncHandler --- .../server/handler/CookieSyncHandler.java | 105 +++++++++++----- .../server/handler/CookieSyncHandlerTest.java | 115 ++++++++++++++++++ 2 files changed, 192 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java index 487f6e44d9a..264a117a608 100644 --- a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java +++ b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java @@ -14,7 +14,7 @@ import org.prebid.server.analytics.AnalyticsReporter; import org.prebid.server.analytics.model.CookieSyncEvent; import org.prebid.server.auction.PrivacyEnforcementService; -import org.prebid.server.auction.model.Tuple2; +import org.prebid.server.auction.model.Tuple3; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.UsersyncInfoAssembler; import org.prebid.server.bidder.Usersyncer; @@ -39,6 +39,7 @@ import org.prebid.server.proto.response.UsersyncInfo; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountCookieSyncConfig; import org.prebid.server.util.HttpUtil; import java.util.ArrayList; @@ -67,7 +68,7 @@ public class CookieSyncHandler implements Handler { private final TcfDefinerService tcfDefinerService; private final PrivacyEnforcementService privacyEnforcementService; private final Integer gdprHostVendorId; - private final boolean defaultCoopSync; + private final Boolean defaultCoopSync; private final List> listOfCoopSyncBidders; private final AnalyticsReporter analyticsReporter; private final Metrics metrics; @@ -162,33 +163,39 @@ public void handle(RoutingContext context) { } final Integer limit = cookieSyncRequest.getLimit(); - final Boolean coopSync = cookieSyncRequest.getCoopSync(); - final Set biddersToSync = biddersToSync(cookieSyncRequest.getBidders(), coopSync, limit); - final String requestAccount = cookieSyncRequest.getAccount(); final Set vendorIds = Collections.singleton(gdprHostVendorId); final Timeout timeout = timeoutFactory.create(defaultTimeout); accountById(requestAccount, timeout) .compose(account -> privacyEnforcementService.contextFromCookieSyncRequest( - cookieSyncRequest, context.request(), account, timeout) - .map(privacyContext -> Tuple2.of(account, privacyContext))) - .map((Tuple2 accountAndPrivacy) -> allowedForVendorId(vendorIds, - accountAndPrivacy.getRight().getTcfContext()) + cookieSyncRequest, + context.request(), + account, + timeout) + .map(privacyContext -> Tuple3.of( + account, privacyContext, biddersToSync(cookieSyncRequest, account)))) + .map((Tuple3> accountAndPrivacyAndBidders) -> allowedForVendorId( + vendorIds, accountAndPrivacyAndBidders.getMiddle().getTcfContext()) .compose(ignored -> tcfDefinerService.resultForBidderNames( - biddersToSync, - accountAndPrivacy.getRight().getTcfContext(), - accountAndPrivacy.getLeft().getGdpr())) + accountAndPrivacyAndBidders.getRight(), + accountAndPrivacyAndBidders.getMiddle().getTcfContext(), + accountAndPrivacyAndBidders.getLeft().getGdpr())) .map(tcfResponse -> handleBidderNamesResult( tcfResponse, context, - accountAndPrivacy.getLeft(), + accountAndPrivacyAndBidders.getLeft(), uidsCookie, - biddersToSync, - accountAndPrivacy.getRight().getPrivacy(), + accountAndPrivacyAndBidders.getRight(), + accountAndPrivacyAndBidders.getMiddle().getPrivacy(), limit)) .otherwise(ignored -> handleTcfError( - context, uidsCookie, biddersToSync, accountAndPrivacy.getRight().getPrivacy(), limit))); + context, + uidsCookie, + accountAndPrivacyAndBidders.getRight(), + accountAndPrivacyAndBidders.getLeft(), + accountAndPrivacyAndBidders.getMiddle().getPrivacy(), + limit))); } private static boolean gdprParamsNotConsistent(CookieSyncRequest request) { @@ -200,13 +207,16 @@ private static boolean gdprParamsNotConsistent(CookieSyncRequest request) { *

* If bidder list was omitted in request, that means sync should be done for all bidders. */ - private Set biddersToSync(List requestBidders, Boolean requestCoop, Integer requestLimit) { + private Set biddersToSync(CookieSyncRequest cookieSyncRequest, Account account) { + final List requestBidders = cookieSyncRequest.getBidders(); + if (CollectionUtils.isEmpty(requestBidders)) { return activeBidders; } - final boolean coop = requestCoop != null ? requestCoop : defaultCoopSync; - if (coop) { + if (coopSyncAllowed(cookieSyncRequest, account)) { + final Integer requestLimit = resolveLimit(cookieSyncRequest.getLimit(), account); + return requestLimit == null ? addAllCoopSyncBidders(requestBidders) : addCoopSyncBidders(requestBidders, requestLimit); @@ -215,6 +225,20 @@ private Set biddersToSync(List requestBidders, Boolean requestCo return new HashSet<>(requestBidders); } + private Boolean coopSyncAllowed(CookieSyncRequest cookieSyncRequest, Account account) { + final Boolean requestCoopSync = cookieSyncRequest.getCoopSync(); + if (requestCoopSync != null) { + return requestCoopSync; + } + + final AccountCookieSyncConfig accountCookieSyncConfig = account.getCookieSync(); + final Boolean accountCoopSync = accountCookieSyncConfig != null + ? accountCookieSyncConfig.getDefaultCoopSync() + : null; + + return ObjectUtils.firstNonNull(accountCoopSync, defaultCoopSync); + } + /** * Returns failed future if vendor is not allowed for cookie sync. * If host vendor id is null, host allowed to sync cookies. @@ -307,7 +331,8 @@ private Void handleBidderNamesResult(TcfResponse tcfResponse, || bidderNameToAction.get(bidder).isBlockPixelSync()) .collect(Collectors.toSet()); - respondWith(context, uidsCookie, privacy, biddersToSync, biddersRejectedByTcf, ccpaEnforcedBidders, limit); + respondWith( + context, uidsCookie, account, privacy, biddersToSync, biddersRejectedByTcf, ccpaEnforcedBidders, limit); return null; } @@ -315,10 +340,11 @@ private Void handleBidderNamesResult(TcfResponse tcfResponse, private Void handleTcfError(RoutingContext context, UidsCookie uidsCookie, Set biddersToSync, + Account account, Privacy privacy, Integer limit) { - respondWith(context, uidsCookie, privacy, biddersToSync, biddersToSync, Collections.emptySet(), limit); + respondWith(context, uidsCookie, account, privacy, biddersToSync, biddersToSync, Collections.emptySet(), limit); return null; } @@ -328,6 +354,7 @@ private Void handleTcfError(RoutingContext context, */ private void respondWith(RoutingContext context, UidsCookie uidsCookie, + Account account, Privacy privacy, Collection bidders, Set biddersRejectedByTcf, @@ -343,13 +370,8 @@ private void respondWith(RoutingContext context, .collect(Collectors.toList()); updateCookieSyncMatchMetrics(bidders, bidderStatuses); - final List updatedBidderStatuses; - if (limit != null && limit > 0 && limit < bidderStatuses.size()) { - Collections.shuffle(bidderStatuses); - updatedBidderStatuses = bidderStatuses.subList(0, limit); - } else { - updatedBidderStatuses = bidderStatuses; - } + final List updatedBidderStatuses = + truncateBidderStatuses(bidderStatuses, resolveLimit(limit, account)); final CookieSyncResponse response = CookieSyncResponse.of(uidsCookie.hasLiveUids() ? "ok" : "no_cookie", updatedBidderStatuses); @@ -370,6 +392,33 @@ private void respondWith(RoutingContext context, .build()); } + private static Integer resolveLimit(Integer limit, Account account) { + final AccountCookieSyncConfig cookieSyncConfig = account.getCookieSync(); + if (cookieSyncConfig == null) { + return limit; + } + + final Integer resolvedLimit = ObjectUtils.defaultIfNull(limit, cookieSyncConfig.getDefaultLimit()); + if (resolvedLimit == null) { + return null; + } + + final Integer maxLimit = cookieSyncConfig.getMaxLimit(); + + return maxLimit == null ? resolvedLimit : Math.min(resolvedLimit, maxLimit); + } + + private static List truncateBidderStatuses(List bidderStatuses, + Integer limit) { + + if (limit != null && limit > 0 && limit < bidderStatuses.size()) { + Collections.shuffle(bidderStatuses); + return bidderStatuses.subList(0, limit); + } + + return bidderStatuses; + } + private Set extractCcpaEnforcedBidders(Account account, Collection biddersToSync, Privacy privacy) { if (privacyEnforcementService.isCcpaEnforced(privacy.getCcpa(), account)) { return biddersToSync.stream() diff --git a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java index 86a771f508c..16e52f2e2b5 100644 --- a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java @@ -39,6 +39,7 @@ import org.prebid.server.proto.response.UsersyncInfo; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountCookieSyncConfig; import org.prebid.server.settings.model.AccountGdprConfig; import org.prebid.server.settings.model.EnabledForRequestType; @@ -397,6 +398,47 @@ public void shouldRespondWithCoopBiddersWhenRequestCoopSyncTrue() throws IOExcep UsersyncInfo.of("http://rubiconexample.com", "redirect", false)).build()))); } + @Test + public void shouldRespondWithCoopBiddersWhenAccountCoopSyncTrue() throws IOException { + // given + given(bidderCatalog.isActive(anyString())).willReturn(true); + + final List> coopBidders = singletonList(singletonList(RUBICON)); + + cookieSyncHandler = new CookieSyncHandler("http://external-url", 2000, uidsCookieService, applicationSettings, + bidderCatalog, tcfDefinerService, privacyEnforcementService, 1, false, coopBidders, + analyticsReporter, metrics, timeoutFactory, jacksonMapper); + + given(routingContext.getBody()).willReturn(givenRequestBody( + CookieSyncRequest.builder() + .bidders(singletonList(APPNEXUS)) + .account("account") + .build())); + + given(applicationSettings.getAccountById(anyString(), any())).willReturn(Future.succeededFuture( + Account.builder() + .cookieSync(AccountCookieSyncConfig.of(null, null, true)) + .build())); + + appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", null, null, "redirect", false); + rubiconUsersyncer = new Usersyncer(RUBICON, "http://rubiconexample.com", null, null, "redirect", false); + givenUsersyncersReturningFamilyName(); + + givenTcfServiceReturningVendorIdResult(singleton(1)); + givenTcfServiceReturningBidderNamesResult(set(RUBICON, APPNEXUS)); + + // when + cookieSyncHandler.handle(routingContext); + + // then + final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); + assertThat(cookieSyncResponse).isEqualTo(CookieSyncResponse.of("no_cookie", asList( + BidderUsersyncStatus.builder().bidder(APPNEXUS).noCookie(true).usersync( + UsersyncInfo.of("http://adnxsexample.com", "redirect", false)).build(), + BidderUsersyncStatus.builder().bidder(RUBICON).noCookie(true).usersync( + UsersyncInfo.of("http://rubiconexample.com", "redirect", false)).build()))); + } + @Test public void shouldRespondWithPrioritisedCoopBidderWhenRequestCoopDefaultTrueAndLimitIsLessThanCoopSize() throws IOException { @@ -1022,6 +1064,79 @@ public void shouldLimitBidderStatuses() throws IOException { given(bidderCatalog.isActive(anyString())).willReturn(true); + given(applicationSettings.getAccountById(anyString(), any())).willReturn(Future.succeededFuture( + Account.builder() + .cookieSync(AccountCookieSyncConfig.of(5, 5, null)) + .build())); + + rubiconUsersyncer = new Usersyncer(RUBICON, + "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", + null, null, "redirect", false); + appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://rubiconexample.com", null, null, "redirect", + false); + givenUsersyncersReturningFamilyName(); + + givenTcfServiceReturningVendorIdResult(singleton(1)); + givenTcfServiceReturningBidderNamesResult(singleton(APPNEXUS)); + + // when + cookieSyncHandler.handle(routingContext); + + // then + final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); + assertThat(cookieSyncResponse.getBidderStatus()).hasSize(1); + } + + @Test + public void shouldLimitBidderStatusesWithAccountDefaultLimit() throws IOException { + // given + given(routingContext.getBody()).willReturn(givenRequestBody(CookieSyncRequest.builder() + .bidders(asList(RUBICON, APPNEXUS)) + .gdpr(0) + .account("account") + .build())); + + given(bidderCatalog.isActive(anyString())).willReturn(true); + + given(applicationSettings.getAccountById(anyString(), any())).willReturn(Future.succeededFuture( + Account.builder() + .cookieSync(AccountCookieSyncConfig.of(1, null, null)) + .build())); + + rubiconUsersyncer = new Usersyncer(RUBICON, + "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", + null, null, "redirect", false); + appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://rubiconexample.com", null, null, "redirect", + false); + givenUsersyncersReturningFamilyName(); + + givenTcfServiceReturningVendorIdResult(singleton(1)); + givenTcfServiceReturningBidderNamesResult(singleton(APPNEXUS)); + + // when + cookieSyncHandler.handle(routingContext); + + // then + final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); + assertThat(cookieSyncResponse.getBidderStatus()).hasSize(1); + } + + @Test + public void shouldLimitBidderStatusesWithAccountMaxLimit() throws IOException { + // given + given(routingContext.getBody()).willReturn(givenRequestBody(CookieSyncRequest.builder() + .bidders(asList(RUBICON, APPNEXUS)) + .gdpr(0) + .account("account") + .build())); + + given(bidderCatalog.isActive(anyString())).willReturn(true); + + given(applicationSettings.getAccountById(anyString(), any())).willReturn(Future.succeededFuture( + Account.builder() + .cookieSync(AccountCookieSyncConfig.of(5, 1, null)) + .build())); + rubiconUsersyncer = new Usersyncer(RUBICON, "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", null, null, "redirect", false);