From db7550718492f16cfc42f4bca26c67b2d3ff9c41 Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:30:08 +0200 Subject: [PATCH] Tests: Multiple Uids Cookies Support (#3691) * Tests: Multiple Uids Cookies Support --- .../server/functional/model/UidsCookie.groovy | 4 +- .../model/request/setuid/SetuidRequest.groovy | 8 +- .../model/request/setuid/UidWithExpiry.groovy | 4 +- .../response/setuid/SetuidResponse.groovy | 2 +- .../service/PrebidServerService.groovy | 51 +++- .../server/functional/tests/SetUidSpec.groovy | 277 +++++++++++++++--- .../util/ObjectMapperWrapper.groovy | 4 + 7 files changed, 289 insertions(+), 61 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy b/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy index 8bbda5dd297..d721255741d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy @@ -16,11 +16,11 @@ class UidsCookie { Map tempUIDs Boolean optout - static UidsCookie getDefaultUidsCookie(BidderName bidder = GENERIC) { + static UidsCookie getDefaultUidsCookie(BidderName bidder = GENERIC, Integer daysUntilExpiry = 2) { new UidsCookie().tap { uids = [(bidder): UUID.randomUUID().toString()] tempUIDs = [(bidder): new UidWithExpiry(uid: UUID.randomUUID().toString(), - expires: ZonedDateTime.now(Clock.systemUTC()).plusDays(2))] + expires: ZonedDateTime.now(Clock.systemUTC()).plusDays(daysUntilExpiry))] } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy index a40623d1f27..49553ded1ff 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy @@ -23,9 +23,9 @@ class SetuidRequest { String account static SetuidRequest getDefaultSetuidRequest() { - def request = new SetuidRequest() - request.bidder = GENERIC - request.gdpr = "0" - request + new SetuidRequest().tap { + bidder = GENERIC + gdpr = "0" + } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy b/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy index 2d9d7ee7d36..146f2724325 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy @@ -11,10 +11,10 @@ class UidWithExpiry { String uid ZonedDateTime expires - static UidWithExpiry getDefaultUidWithExpiry() { + static UidWithExpiry getDefaultUidWithExpiry(Integer daysUntilExpiry = 2) { new UidWithExpiry().tap { uid = UUID.randomUUID().toString() - expires = ZonedDateTime.now(Clock.systemUTC()).plusDays(2) + expires = ZonedDateTime.now(Clock.systemUTC()).plusDays(daysUntilExpiry) } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy index bc35cd07d82..08a9adbb8fb 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy @@ -6,7 +6,7 @@ import org.prebid.server.functional.model.UidsCookie @ToString(includeNames = true, ignoreNulls = true) class SetuidResponse { - Map headers + Map> headers UidsCookie uidsCookie Byte[] responseBody } diff --git a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy index bac2badd46a..31df1efc8d5 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -167,12 +167,21 @@ class PrebidServerService implements ObjectMapperWrapper { } SetuidResponse sendSetUidRequest(SetuidRequest request, UidsCookie uidsCookie, Map header = [:]) { - def uidsCookieAsJson = encode(uidsCookie) - def uidsCookieAsEncodedJson = Base64.urlEncoder.encodeToString(uidsCookieAsJson.bytes) - def response = given(requestSpecification).cookie(UIDS_COOKIE_NAME, uidsCookieAsEncodedJson) - .queryParams(toMap(request)) - .headers(header) - .get(SET_UID_ENDPOINT) + sendSetUidRequest(request, [uidsCookie], header) + } + + SetuidResponse sendSetUidRequest(SetuidRequest request, List uidsCookies, Map header = [:]) { + def cookies = uidsCookies.withIndex().collectEntries { group, index -> + def uidsCookieAsJson = encode(group) + def uidsCookieAsEncodedJson = Base64.urlEncoder.encodeToString(uidsCookieAsJson.bytes) + ["${UIDS_COOKIE_NAME}${index > 0 ? index + 1 : ''}": uidsCookieAsEncodedJson] + } + + def response = given(requestSpecification) + .cookies(cookies) + .queryParams(toMap(request)) + .headers(header) + .get(SET_UID_ENDPOINT) checkResponseStatusCode(response) @@ -344,16 +353,32 @@ class PrebidServerService implements ObjectMapperWrapper { } } - private static Map getHeaders(Response response) { - response.headers().collectEntries { [it.name, it.value] } + private static Map> getHeaders(Response response) { + response.headers().groupBy { it.name }.collectEntries { [(it.key): it.value*.value] } } private static UidsCookie getDecodedUidsCookie(Response response) { - def uids = response.detailedCookie(UIDS_COOKIE_NAME)?.value - if (uids) { - return decode(new String(Base64.urlDecoder.decode(uids)), UidsCookie) - } else { - throw new IllegalStateException("uids cookie is missing in response") + def sortedCookies = response.detailedCookies() + .findAll { cookie -> !(cookie =~ /\buids\d*=\s*;/) } + .sort { a, b -> + def aMatch = (a.name =~ /uids(\d*)/)[0] + def bMatch = (b.name =~ /uids(\d*)/)[0] + + def aNumber = (aMatch?.getAt(1) ? aMatch[1].toInteger() : 0) + def bNumber = (bMatch?.getAt(1) ? bMatch[1].toInteger() : 0) + + aNumber <=> bNumber + } + + def decodedCookiesList = sortedCookies.collect { cookie -> + def uid = (cookie =~ /uids\d*=(\S+?);/)[0][1] + decodeWithBase64(uid as String, UidsCookie) + } + + decodedCookiesList.inject(new UidsCookie()) { uidsCookie, decodedCookie -> + uidsCookie.uids = (uidsCookie.uids ?: new LinkedHashMap()) + (decodedCookie.uids ?: new LinkedHashMap()) + uidsCookie.tempUIDs = (uidsCookie.tempUIDs ?: new LinkedHashMap()) + (decodedCookie.tempUIDs ?: new LinkedHashMap()) + uidsCookie } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy index 1e33542e564..7e9eff9ebd3 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy @@ -3,19 +3,21 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.UidsCookie import org.prebid.server.functional.model.request.setuid.SetuidRequest import org.prebid.server.functional.model.response.cookiesync.UserSyncInfo +import org.prebid.server.functional.model.response.setuid.SetuidResponse import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.TcfConsent import org.prebid.server.util.ResourceUtil import spock.lang.Shared import java.time.Clock import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.ALIAS_CAMEL_CASE import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS -import static org.prebid.server.functional.model.bidder.BidderName.EMPTY import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.BidderName.GENERIC_CAMEL_CASE import static org.prebid.server.functional.model.bidder.BidderName.OPENX @@ -30,8 +32,11 @@ import static org.prebid.server.functional.util.privacy.TcfConsent.RUBICON_VENDO class SetUidSpec extends BaseSpec { private static final Integer MAX_COOKIE_SIZE = 500 + private static final Integer MAX_NUMBER_OF_UID_COOKIES = 30 + private static final Integer UPDATED_EXPIRE_DAYS = 14 private static final UserSyncInfo.Type USER_SYNC_TYPE = REDIRECT private static final boolean CORS_SUPPORT = false + private static final Integer RANDOM_EXPIRE_DAY = PBSUtils.getRandomNumber(1, 10) private static final String USER_SYNC_URL = "$networkServiceContainer.rootUri/generic-usersync" private static final Map PBS_CONFIG = ["host-cookie.max-cookie-size-bytes" : MAX_COOKIE_SIZE as String, @@ -43,13 +48,16 @@ class SetUidSpec extends BaseSpec { "adapters.${APPNEXUS.value}.usersync.cookie-family-name" : APPNEXUS.value, "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] + private static final Map UID_COOKIES_CONFIG = ['setuid.number-of-uid-cookies': MAX_NUMBER_OF_UID_COOKIES.toString()] private static final Map GENERIC_ALIAS_CONFIG = ["adapters.generic.aliases.alias.enabled" : "true", - "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] private static final String TCF_ERROR_MESSAGE = "The gdpr_consent param prevents cookies from being saved" private static final int UNAVAILABLE_FOR_LEGAL_REASONS_CODE = 451 @Shared - PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GENERIC_ALIAS_CONFIG) + PrebidServerService singleCookiesPbsService = pbsServiceFactory.getService(PBS_CONFIG + GENERIC_ALIAS_CONFIG) + @Shared + PrebidServerService multipleCookiesPbsService = pbsServiceFactory.getService(PBS_CONFIG + UID_COOKIES_CONFIG + GENERIC_ALIAS_CONFIG) def "PBS should set uids cookie"() { given: "Default SetuidRequest" @@ -57,30 +65,49 @@ class SetUidSpec extends BaseSpec { def uidsCookie = UidsCookie.defaultUidsCookie when: "PBS processes setuid request" - def response = prebidServerService.sendSetUidRequest(request, uidsCookie) + def response = singleCookiesPbsService.sendSetUidRequest(request, uidsCookie) + + then: "Response should contain uid cookie" + assert response.uidsCookie.tempUIDs[GENERIC].uid + assert response.responseBody == + ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + } + + def "PBS should updated uids cookie when request parameters contain uid"() { + given: "Default SetuidRequest" + def requestUid = UUID.randomUUID().toString() + def request = SetuidRequest.defaultSetuidRequest.tap { + uid = requestUid + } + def uidsCookie = UidsCookie.defaultUidsCookie + + and: "Flush metrics" + flushMetrics(singleCookiesPbsService) + + when: "PBS processes setuid request" + def response = singleCookiesPbsService.sendSetUidRequest(request, uidsCookie) then: "Response should contain uids cookie" - assert !response.uidsCookie.tempUIDs - assert !response.uidsCookie.uids + assert daysDifference(response.uidsCookie.tempUIDs[GENERIC].expires) == UPDATED_EXPIRE_DAYS + assert response.uidsCookie.tempUIDs[GENERIC].uid == requestUid assert response.responseBody == ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + + and: "usersync.FAMILY.sets metric should be updated" + def metrics = singleCookiesPbsService.sendCollectedMetricsRequest() + assert metrics["usersync.${GENERIC.value}.sets"] == 1 } def "PBS setuid should remove expired uids cookie"() { given: "Default SetuidRequest" def request = SetuidRequest.defaultSetuidRequest - def uidsCookie = UidsCookie.defaultUidsCookie.tap { - def uidWithExpiry = defaultUidWithExpiry.tap { - expires = ZonedDateTime.now(Clock.systemUTC()).minusDays(2) - } - tempUIDs = [(RUBICON): uidWithExpiry] - } + def uidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON, -RANDOM_EXPIRE_DAY) when: "PBS processes setuid request" - def response = prebidServerService.sendSetUidRequest(request, uidsCookie) + def response = singleCookiesPbsService.sendSetUidRequest(request, uidsCookie) then: "Response shouldn't contain uids cookie" - assert !response.uidsCookie.tempUIDs[RUBICON] + assert !response.uidsCookie.tempUIDs } def "PBS setuid should return requested uids cookie when priority bidder not present in config"() { @@ -99,7 +126,7 @@ class SetUidSpec extends BaseSpec { when: "PBS processes setuid request" def response = prebidServerService.sendSetUidRequest(request, uidsCookie) - then: "Response should contain requested uids" + then: "Response should contain requested tempUIDs" assert response.uidsCookie.tempUIDs[GENERIC] assert response.uidsCookie.tempUIDs[RUBICON] @@ -120,8 +147,8 @@ class SetUidSpec extends BaseSpec { } def rubiconBidder = RUBICON def uidsCookie = UidsCookie.defaultUidsCookie.tap { - tempUIDs = [(APPNEXUS) : defaultUidWithExpiry, - (rubiconBidder): defaultUidWithExpiry] + tempUIDs = [(APPNEXUS) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1), + (rubiconBidder): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY)] } when: "PBS processes setuid request" @@ -135,7 +162,7 @@ class SetUidSpec extends BaseSpec { pbsServiceFactory.removeContainer(pbsConfig) } - def "PBS setuid should remove earliest expiration bidder when size is full"() { + def "PBS setuid should remove most distant expiration bidder when size is full"() { given: "PBS config" def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": GENERIC.value] def prebidServerService = pbsServiceFactory.getService(pbsConfig) @@ -159,7 +186,7 @@ class SetUidSpec extends BaseSpec { def response = prebidServerService.sendSetUidRequest(request, uidsCookie) then: "Response should contain uids cookies" - assert response.uidsCookie.tempUIDs[APPNEXUS] + assert response.uidsCookie.tempUIDs[RUBICON] assert response.uidsCookie.tempUIDs[GENERIC] cleanup: "Stop and remove pbs container" @@ -200,13 +227,12 @@ class SetUidSpec extends BaseSpec { def "PBS setuid should reject bidder when cookie's filled and requested bidder in pri and rejected by tcf"() { given: "Setuid request" - def bidderName = RUBICON def pbsConfig = PBS_CONFIG + ["gdpr.host-vendor-id": RUBICON_VENDOR_ID.toString(), - "cookie-sync.pri" : bidderName.value] + "cookie-sync.pri" : RUBICON.value] def prebidServerService = pbsServiceFactory.getService(pbsConfig) def request = SetuidRequest.defaultSetuidRequest.tap { - it.bidder = bidderName + it.bidder = RUBICON gdpr = "1" gdprConsent = new TcfConsent.Builder().build() } @@ -226,13 +252,13 @@ class SetUidSpec extends BaseSpec { and: "usersync.FAMILY.tcf.blocked metric should be updated" def metric = prebidServerService.sendCollectedMetricsRequest() - assert metric["usersync.${bidderName.value}.tcf.blocked"] == 1 + assert metric["usersync.${RUBICON.value}.tcf.blocked"] == 1 cleanup: "Stop and remove pbs container" pbsServiceFactory.removeContainer(pbsConfig) } - def "PBS setuid should remove oldest uid and log metric when cookie's filled and oldest uid's not on the pri"() { + def "PBS setuid should remove most distant expiration uid and log metric when cookie's filled and this uid's not on the pri"() { given: "PBS config" def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": GENERIC.value] def prebidServerService = pbsServiceFactory.getService(pbsConfig) @@ -245,21 +271,17 @@ class SetUidSpec extends BaseSpec { uid = UUID.randomUUID().toString() } - def bidderName = RUBICON def uidsCookie = UidsCookie.defaultUidsCookie.tap { - def uidWithExpiry = defaultUidWithExpiry.tap { - expires.plusDays(10) - } - tempUIDs = [(APPNEXUS) : defaultUidWithExpiry, - (bidderName): uidWithExpiry] + tempUIDs = [(APPNEXUS): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY), + (RUBICON) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1)] } when: "PBS processes setuid request" def response = prebidServerService.sendSetUidRequest(request, uidsCookie) - and: "usersync.FAMILY.sizedout metric should be updated" + and: "usersync.FAMILY.sizeblocked metric should be updated" def metrics = prebidServerService.sendCollectedMetricsRequest() - assert metrics["usersync.${bidderName.value}.sizedout"] == 1 + assert metrics["usersync.${RUBICON.value}.sizeblocked"] == 1 then: "Response should contain uids cookies" assert response.uidsCookie.tempUIDs[APPNEXUS] @@ -269,7 +291,7 @@ class SetUidSpec extends BaseSpec { pbsServiceFactory.removeContainer(pbsConfig) } - def "PBS SetUid should remove oldest bidder from uids cookie in favor of prioritized bidder"() { + def "PBS set uid should emit sizeblocked metric and remove most distant expiration bidder from uids cookie for non-prioritized bidder"() { given: "PBS config" def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": "$OPENX.value, $GENERIC.value" as String] def prebidServerService = pbsServiceFactory.getService(pbsConfig) @@ -282,8 +304,8 @@ class SetUidSpec extends BaseSpec { and: "Set up set uid cookie" def uidsCookie = UidsCookie.defaultUidsCookie.tap { - it.tempUIDs = [(APPNEXUS): defaultUidWithExpiry, - (RUBICON) : defaultUidWithExpiry] + tempUIDs = [(APPNEXUS): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1), + (RUBICON) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY)] } and: "Flush metrics" @@ -296,14 +318,14 @@ class SetUidSpec extends BaseSpec { assert response.uidsCookie.tempUIDs[OPENX] and: "Response set cookie header size should be lowest or the same as max cookie config size" - assert response.headers.get("Set-Cookie").split("Secure;")[0].length() <= MAX_COOKIE_SIZE + assert getSetUidsHeaders(response).first.split("Secure;")[0].length() <= MAX_COOKIE_SIZE and: "Request bidder should contain uid from Set uid request" assert response.uidsCookie.tempUIDs[OPENX].uid == request.uid - and: "usersync.FAMILY.sizedout metric should be updated" + and: "usersync.FAMILY.sizeblocked metric should be updated" def metricsRequest = prebidServerService.sendCollectedMetricsRequest() - assert metricsRequest["usersync.${APPNEXUS.value}.sizedout"] == 1 + assert metricsRequest["usersync.${APPNEXUS.value}.sizeblocked"] == 1 and: "usersync.FAMILY.sets metric should be updated" assert metricsRequest["usersync.${OPENX.value}.sets"] == 1 @@ -312,6 +334,42 @@ class SetUidSpec extends BaseSpec { pbsServiceFactory.removeContainer(pbsConfig) } + def "PBS set uid should emit sizedout metric and remove most distant expiration bidder from uids cookie in prioritized bidder"() { + given: "PBS config" + def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": "$OPENX.value, $APPNEXUS.value, $RUBICON.value" as String] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Set uid request" + def request = SetuidRequest.defaultSetuidRequest + + and: "Set up set uid cookie" + def uidsCookie = UidsCookie.defaultUidsCookie.tap { + tempUIDs = [(APPNEXUS): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1), + (OPENX) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY), + (RUBICON) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY)] + } + + and: "Flush metrics" + flushMetrics(prebidServerService) + + when: "PBS processes set uid request" + def response = prebidServerService.sendSetUidRequest(request, uidsCookie) + + then: "Response should contain pri bidder in uids cookies" + assert response.uidsCookie.tempUIDs[OPENX] + assert response.uidsCookie.tempUIDs[RUBICON] + + and: "Response set cookie header size should be lowest or the same as max cookie config size" + assert getSetUidsHeaders(response).first.split("Secure;")[0].length() <= MAX_COOKIE_SIZE + + and: "usersync.FAMILY.sizedout metric should be updated" + def metricsRequest = prebidServerService.sendCollectedMetricsRequest() + assert metricsRequest["usersync.${APPNEXUS.value}.sizedout"] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + def "PBS setuid should reject request when requested bidder mismatching with cookie-family-name"() { given: "Default SetuidRequest" def request = SetuidRequest.getDefaultSetuidRequest().tap { @@ -319,7 +377,7 @@ class SetUidSpec extends BaseSpec { } when: "PBS processes setuid request" - prebidServerService.sendSetUidRequest(request, UidsCookie.defaultUidsCookie) + singleCookiesPbsService.sendSetUidRequest(request, UidsCookie.defaultUidsCookie) then: "Request should fail with error" def exception = thrown(PrebidServerException) @@ -329,4 +387,145 @@ class SetUidSpec extends BaseSpec { where: bidderName << [UNKNOWN, WILDCARD, GENERIC_CAMEL_CASE, ALIAS, ALIAS_CAMEL_CASE] } + + def "PBS should throw an exception when incoming request have optout flag"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + + and: "PBS service with optout cookies" + def pbsConfig = PBS_CONFIG + ["host-cookie.optout-cookie.name" : "uids", + "host-cookie.optout-cookie.value": Base64.urlEncoder.encodeToString(encode(genericUidsCookie).bytes)] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + when: "PBS processes setuid request" + prebidServerService.sendSetUidRequest(request, [genericUidsCookie]) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 401 + assert exception.responseBody == 'Unauthorized: Sync is not allowed for this uids' + } + + def "PBS should merge cookies when incoming request have multiple uids cookies"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest.tap { + uid = UUID.randomUUID().toString() + } + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + def rubiconUidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [genericUidsCookie, rubiconUidsCookie]) + + then: "Response should contain requested tempUIDs" + assert response.uidsCookie.tempUIDs[GENERIC] + assert response.uidsCookie.tempUIDs[RUBICON] + + and: "Headers uids cookies should contain same cookie as response" + def setUidsHeaders = getSetUidsHeaders(response) + def uidsCookie = extractHeaderTempUIDs(setUidsHeaders.first) + assert setUidsHeaders.size() == 1 + assert uidsCookie.tempUIDs[GENERIC] + assert uidsCookie.tempUIDs[RUBICON] + } + + def "PBS should send multiple uids cookies by priority and expiration timestamp"() { + given: "PBS config" + def pbsConfig = PBS_CONFIG + + UID_COOKIES_CONFIG + + ["cookie-sync.pri": "$OPENX.value, $GENERIC.value" as String] + + ["host-cookie.max-cookie-size-bytes": MAX_COOKIE_SIZE as String] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + + and: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest + + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC, RANDOM_EXPIRE_DAY + 1) + def rubiconUidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON, RANDOM_EXPIRE_DAY + 2) + def openxUidsCookie = UidsCookie.getDefaultUidsCookie(OPENX, RANDOM_EXPIRE_DAY + 3) + def appnexusUidsCookie = UidsCookie.getDefaultUidsCookie(APPNEXUS, RANDOM_EXPIRE_DAY) + + when: "PBS processes setuid request" + def response = prebidServerService.sendSetUidRequest(request, [appnexusUidsCookie, genericUidsCookie, rubiconUidsCookie, openxUidsCookie]) + + then: "Response should contain requested tempUIDs" + assert response.uidsCookie.tempUIDs.keySet() == new LinkedHashSet([GENERIC, OPENX, APPNEXUS, RUBICON]) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should remove duplicates when incoming cookie-family already exists in the working list"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest + + and: "Duplicated uids cookies" + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC, RANDOM_EXPIRE_DAY) + def duplicateUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC, RANDOM_EXPIRE_DAY + 1) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [genericUidsCookie, duplicateUidsCookie]) + + then: "Response should contain single generic uid with most distant expiration timestamp" + assert response.uidsCookie.tempUIDs.size() == 1 + assert response.uidsCookie.tempUIDs[GENERIC].uid == duplicateUidsCookie.tempUIDs[GENERIC].uid + assert response.uidsCookie.tempUIDs[GENERIC].expires == duplicateUidsCookie.tempUIDs[GENERIC].expires + } + + def "PBS should shouldn't modify uids cookie when uid is empty"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest.tap { + it.uid = null + it.bidder = GENERIC + } + + and: "Specific uids cookies" + def uidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [uidsCookie]) + + then: "Response should contain single generic uid" + assert response.uidsCookie.tempUIDs.size() == 1 + assert response.uidsCookie.tempUIDs[GENERIC].uid == uidsCookie.tempUIDs[GENERIC].uid + assert response.uidsCookie.tempUIDs[GENERIC].expires == uidsCookie.tempUIDs[GENERIC].expires + } + + def "PBS should include all cookies even empty when incoming request have multiple uids cookies"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest.tap { + uid = UUID.randomUUID().toString() + } + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + def rubiconUidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [genericUidsCookie, rubiconUidsCookie]) + + then: "Response should contain requested tempUIDs" + assert response.uidsCookie.tempUIDs[GENERIC] + assert response.uidsCookie.tempUIDs[RUBICON] + + and: "Headers uids cookies should contain same cookie as response" + assert getSetUidsHeaders(response).size() == 1 + assert getSetUidsHeaders(response, true).size() == MAX_NUMBER_OF_UID_COOKIES + } + + List getSetUidsHeaders(SetuidResponse response, boolean includeEmpty = false) { + response.headers.get("Set-Cookie").findAll { cookie -> + includeEmpty || !(cookie =~ /\buids\d*=\s*;/) + } + } + + static UidsCookie extractHeaderTempUIDs(String header) { + def uid = (header =~ /uids\d*=(\S+?);/)[0][1] + decodeWithBase64(uid as String, UidsCookie) + } + + def daysDifference(ZonedDateTime inputDate) { + ZonedDateTime now = ZonedDateTime.now(Clock.systemUTC()).minusHours(1) + return ChronoUnit.DAYS.between(now, inputDate) + } } diff --git a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy index e6b808cd2aa..3ab9e349ac9 100644 --- a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy @@ -29,6 +29,10 @@ trait ObjectMapperWrapper { mapper.readValue(jsonString, typeReference) } + final static T decodeWithBase64(String base64String, Class clazz) { + mapper.readValue(new String(Base64.decoder.decode(base64String)), clazz) + } + final static Map toMap(Object object) { mapper.convertValue(object, Map) }