diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java index 908fe9e2905..08c369b4f35 100644 --- a/src/main/java/org/prebid/server/cache/CoreCacheService.java +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -219,11 +219,12 @@ public Future cachePutObjects(List bidPutObjects Boolean isEventsEnabled, Set biddersAllowingVastUpdate, String accountId, + Integer accountTtl, String integration, Timeout timeout) { - final List cachedCreatives = - updatePutObjects(bidPutObjects, isEventsEnabled, biddersAllowingVastUpdate, accountId, integration); + final List cachedCreatives = updatePutObjects( + bidPutObjects, isEventsEnabled, biddersAllowingVastUpdate, accountId, accountTtl, integration); updateCreativeMetrics( cachedCreatives, @@ -237,6 +238,7 @@ private List updatePutObjects(List bidPutObjects, Boolean isEventsEnabled, Set allowedBidders, String accountId, + Integer accountTtl, String integration) { return bidPutObjects.stream() @@ -251,11 +253,18 @@ private List updatePutObjects(List bidPutObjects, putObject, accountId, integration)) + .ttlseconds(resolveVtrackTtl(putObject.getTtlseconds(), accountTtl)) .build()) .map(payload -> CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue()))) .toList(); } + private static Integer resolveVtrackTtl(Integer initialObjectTtl, Integer initialAccountTtl) { + final Integer accountTtl = initialAccountTtl != null && initialAccountTtl > 0 ? initialAccountTtl : null; + final Integer objectTtl = initialObjectTtl != null && initialObjectTtl > 0 ? initialObjectTtl : null; + return ObjectUtils.min(objectTtl, accountTtl); + } + public Future cacheBidsOpenrtb(List bidsToCache, AuctionContext auctionContext, CacheContext cacheContext, diff --git a/src/main/java/org/prebid/server/handler/PostVtrackHandler.java b/src/main/java/org/prebid/server/handler/PostVtrackHandler.java index 194d8a4f3fb..c52068427ce 100644 --- a/src/main/java/org/prebid/server/handler/PostVtrackHandler.java +++ b/src/main/java/org/prebid/server/handler/PostVtrackHandler.java @@ -29,6 +29,7 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountEventsConfig; +import org.prebid.server.settings.model.AccountVtrackConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.vertx.verticles.server.HttpEndpoint; import org.prebid.server.vertx.verticles.server.application.ApplicationResource; @@ -36,6 +37,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -180,10 +182,12 @@ private void handleAccountResult(AsyncResult asyncAccount, respondWithServerError(routingContext, "Error occurred while fetching account", asyncAccount.cause()); } else { // insert impression tracking if account allows events and bidder allows VAST modification - final Boolean isEventEnabled = accountEventsEnabled(asyncAccount.result()); + final Account account = asyncAccount.result(); + final Boolean isEventEnabled = accountEventsEnabled(account); + final Integer accountTtl = accountVtrackTtl(account); final Set allowedBidders = biddersAllowingVastUpdate(vtrackPuts); coreCacheService.cachePutObjects( - vtrackPuts, isEventEnabled, allowedBidders, accountId, integration, timeout) + vtrackPuts, isEventEnabled, allowedBidders, accountId, accountTtl, integration, timeout) .onComplete(asyncCache -> handleCacheResult(asyncCache, routingContext)); } } @@ -196,6 +200,12 @@ private static Boolean accountEventsEnabled(Account account) { return accountEventsConfig != null ? accountEventsConfig.getEnabled() : null; } + private static Integer accountVtrackTtl(Account account) { + return Optional.ofNullable(account.getVtrack()) + .map(AccountVtrackConfig::getTtl) + .orElse(null); + } + /** * Returns list of bidders that allow VAST XML modification. */ 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 4286c5077f0..2aa8c974ffa 100644 --- a/src/main/java/org/prebid/server/settings/model/Account.java +++ b/src/main/java/org/prebid/server/settings/model/Account.java @@ -30,6 +30,8 @@ public class Account { @JsonAlias("alternate-bidder-codes") AccountAlternateBidderCodes alternateBidderCodes; + AccountVtrackConfig vtrack; + public static Account empty(String id) { return Account.builder().id(id).build(); } diff --git a/src/main/java/org/prebid/server/settings/model/AccountVtrackConfig.java b/src/main/java/org/prebid/server/settings/model/AccountVtrackConfig.java new file mode 100644 index 00000000000..95939389e05 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountVtrackConfig.java @@ -0,0 +1,9 @@ +package org.prebid.server.settings.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountVtrackConfig { + + Integer ttl; +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy index 9dded674fee..83912535a57 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy @@ -26,6 +26,7 @@ class AccountConfig { AlternateBidderCodes alternateBidderCodes @JsonProperty("alternate_bidder_codes") AlternateBidderCodes alternateBidderCodesSnakeCase + AccountVtrackConfig vtrack static getDefaultAccountConfig() { new AccountConfig(status: AccountStatus.ACTIVE) diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountVtrackConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountVtrackConfig.groovy new file mode 100644 index 00000000000..77397e20b4c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountVtrackConfig.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AccountVtrackConfig { + + Integer ttl +} 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 45a1138cf71..9e760b7173e 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -215,7 +215,7 @@ class PrebidServerService implements ObjectMapperWrapper { } PrebidCacheResponse sendPostVtrackRequest(VtrackRequest request, String account) { - def response = given(requestSpecification).queryParam("a", account) + def response = given(requestSpecification).queryParams(["a": account]) .body(request) .post(VTRACK_ENDPOINT) diff --git a/src/test/groovy/org/prebid/server/functional/tests/CacheVtrackSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CacheVtrackSpec.groovy index e96fc519fe5..e5637fe80e2 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheVtrackSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheVtrackSpec.groovy @@ -4,6 +4,7 @@ import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountCacheConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountEventsConfig +import org.prebid.server.functional.model.config.AccountVtrackConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.vtrack.VtrackRequest import org.prebid.server.functional.model.request.vtrack.xml.Vast @@ -55,6 +56,212 @@ class CacheVtrackSpec extends BaseSpec { prebidCache.reset() } + def "PBS should update prebid_cache.creative_size.xml metric and adding tracking xml when xml creative contain #wrapper and impression are valid xml value"() { + given: "Current value of metric prebid_cache.vtrack.write.ok" + def initialOkVTrackValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC) + + and: "Create and save enabled events config in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + uuid = accountId + config = new AccountConfig().tap { + auction = new AccountAuctionConfig(events: new AccountEventsConfig(enabled: true)) + } + } + accountDao.save(account) + + and: "Set up prebid cache" + prebidCache.setResponse() + + and: "Vtrack request with custom tags" + def payload = PBSUtils.randomString + def creative = "<${wrapper}>prebid.org wrapper" + + "<![CDATA[//${payload}]]>" + + "<${impression}> <![CDATA[ ]]> " + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "Vast xml is modified" + def prebidCacheRequest = prebidCache.getXmlRecordedRequestsBody(payload) + assert prebidCacheRequest.size() == 1 + assert prebidCacheRequest[0].contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}") + + and: "prebid_cache.creative_size.xml metric should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + def ttlSeconds = request.puts[0].ttlseconds + assert metrics[VTRACK_WRITE_OK_METRIC] == initialOkVTrackValue + 1 + assert metrics[VTRACK_XML_CREATIVE_TTL_METRIC] == ttlSeconds + + and: "account..prebid_cache.vtrack.creative_size.xml should be updated" + assert metrics[ACCOUNT_VTRACK_WRITE_OK_METRIC.formatted(accountId) as String] == 1 + assert metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId) as String] == ttlSeconds + + where: + wrapper | impression + " wrapper " | " impression " + PBSUtils.getRandomCase(" wrapper ") | PBSUtils.getRandomCase(" impression ") + " wraPPer ${PBSUtils.getRandomString()} " | " imPreSSion ${PBSUtils.getRandomString()}" + " inLine " | " ImpreSSion $PBSUtils.randomNumber" + PBSUtils.getRandomCase(" inline ") | " ${PBSUtils.getRandomCase(" impression ")} $PBSUtils.randomNumber " + " inline ${PBSUtils.getRandomString()} " | " ImpreSSion " + } + + def "PBS should update prebid_cache.creative_size.xml metric when xml creative is received"() { + given: "Current value of metric prebid_cache.requests.ok" + def initialValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC) + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Default VtrackRequest" + def accountId = PBSUtils.randomNumber.toString() + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "prebid_cache.vtrack.creative_size.xml metric should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + def creativeSize = creative.bytes.length + assert metrics[VTRACK_WRITE_OK_METRIC] == initialValue + 1 + + and: "account..prebid_cache.creative_size.xml should be updated" + assert metrics[ACCOUNT_VTRACK_WRITE_OK_METRIC.formatted(accountId)] == 1 + assert metrics[ACCOUNT_VTRACK_XML_CREATIVE_SIZE_METRIC.formatted(accountId)] == creativeSize + } + + def "PBS should failed VTrack request when sending request without account"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, null) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Account 'a' is required query parameter and can't be empty" + } + + def "PBS shouldn't use negative value in tllSecond when account vtrack ttl is #accountTtl and request ttl second is #requestedTtl"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = requestedTtl + } + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: accountTtl) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "Pbs should emit creative_ttl.xml with lowest value" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId)] + == [requestedTtl, accountTtl].findAll { it -> it > 0 }.min() + where: + requestedTtl | accountTtl + PBSUtils.getRandomNumber(300, 1500) as Integer | PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer + PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer | PBSUtils.getRandomNumber(300, 1500) as Integer + PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer | PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer + } + + def "PBS should use lowest tllSecond when account vtrack ttl is #accountTtl and request ttl second is #requestedTtl"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = requestedTtl + } + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: accountTtl) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "Pbs should emit creative_ttl.xml with lowest value" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId)] == [requestedTtl, accountTtl].min() + + where: + requestedTtl | accountTtl + null | null + null | PBSUtils.getRandomNumber(300, 1500) as Integer + PBSUtils.getRandomNumber(300, 1500) as Integer | null + PBSUtils.getRandomNumber(300, 1500) as Integer | PBSUtils.getRandomNumber(300, 1500) as Integer + } + + def "PBS should proceed request when account ttl and request ttl second are empty"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = null + } + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: null) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "Pbs shouldn't emit creative_ttl.xml" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId)] + } + def "PBS should return 400 status code when get vtrack request without uuid"() { when: "PBS processes get vtrack request" defaultPbsService.sendGetVtrackRequest(["uuid": null]) @@ -222,90 +429,6 @@ class CacheVtrackSpec extends BaseSpec { assert exception.responseBody == "'uuid' is a required query parameter and can't be empty" } - def "PBS should update prebid_cache.creative_size.xml metric when xml creative is received"() { - given: "Current value of metric prebid_cache.requests.ok" - def initialValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC) - - and: "Cache set up response" - prebidCache.setResponse() - - and: "Default VtrackRequest" - def accountId = PBSUtils.randomNumber.toString() - def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) - def request = VtrackRequest.getDefaultVtrackRequest(creative) - - and: "Flush metrics" - flushMetrics(defaultPbsService) - - when: "PBS processes vtrack request" - defaultPbsService.sendPostVtrackRequest(request, accountId) - - then: "prebid_cache.vtrack.creative_size.xml metric should be updated" - def metrics = defaultPbsService.sendCollectedMetricsRequest() - def creativeSize = creative.bytes.length - assert metrics[VTRACK_WRITE_OK_METRIC] == initialValue + 1 - assert metrics[VTRACK_XML_CREATIVE_SIZE_METRIC] == creativeSize - - and: "account..prebid_cache.creative_size.xml should be updated" - assert metrics[ACCOUNT_VTRACK_WRITE_OK_METRIC.formatted(accountId)] == 1 - assert metrics[ACCOUNT_VTRACK_XML_CREATIVE_SIZE_METRIC.formatted(accountId)] == creativeSize - } - - def "PBS should update prebid_cache.creative_size.xml metric and adding tracking xml when xml creative contain #wrapper and impression are valid xml value"() { - given: "Current value of metric prebid_cache.vtrack.write.ok" - def initialOkVTrackValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC) - - and: "Create and save enabled events config in account" - def accountId = PBSUtils.randomNumber.toString() - def account = new Account().tap { - uuid = accountId - config = new AccountConfig().tap { - auction = new AccountAuctionConfig(events: new AccountEventsConfig(enabled: true)) - } - } - accountDao.save(account) - - and: "Set up prebid cache" - prebidCache.setResponse() - - and: "Vtrack request with custom tags" - def payload = PBSUtils.randomString - def creative = "<${wrapper}>prebid.org wrapper" + - "<![CDATA[//${payload}]]>" + - "<${impression}> <![CDATA[ ]]> " - def request = VtrackRequest.getDefaultVtrackRequest(creative) - - and: "Flush metrics" - flushMetrics(defaultPbsService) - - when: "PBS processes vtrack request" - defaultPbsService.sendPostVtrackRequest(request, accountId) - - then: "Vast xml is modified" - def prebidCacheRequest = prebidCache.getXmlRecordedRequestsBody(payload) - assert prebidCacheRequest.size() == 1 - assert prebidCacheRequest[0].contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}") - - and: "prebid_cache.creative_size.xml metric should be updated" - def metrics = defaultPbsService.sendCollectedMetricsRequest() - def ttlSeconds = request.puts[0].ttlseconds - assert metrics[VTRACK_WRITE_OK_METRIC] == initialOkVTrackValue + 1 - assert metrics[VTRACK_XML_CREATIVE_TTL_METRIC] == ttlSeconds - - and: "account..prebid_cache.vtrack.creative_size.xml should be updated" - assert metrics[ACCOUNT_VTRACK_WRITE_OK_METRIC.formatted(accountId) as String] == 1 - assert metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId) as String] == ttlSeconds - - where: - wrapper | impression - " wrapper " | " impression " - PBSUtils.getRandomCase(" wrapper ") | PBSUtils.getRandomCase(" impression ") - " wraPPer ${PBSUtils.getRandomString()} " | " imPreSSion ${PBSUtils.getRandomString()}" - " inLine " | " ImpreSSion $PBSUtils.randomNumber" - PBSUtils.getRandomCase(" inline ") | " ${PBSUtils.getRandomCase(" impression ")} $PBSUtils.randomNumber " - " inline ${PBSUtils.getRandomString()} " | " ImpreSSion " - } - def "PBS should update prebid_cache.creative_size.xml metric when account cache config #enabledCacheConcfig"() { given: "Current value of metric prebid_cache.requests.ok" def okInitialValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC) diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index 4a9fbabc15a..9138183597f 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -76,6 +76,7 @@ import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -785,7 +786,7 @@ public void cacheBidsOpenrtbShouldRemoveCatDurPrefixFromVideoUuidFromResponse() public void cachePutObjectsShouldTolerateGlobalTimeoutAlreadyExpired() { // when final Future future = target.cachePutObjects( - singletonList(BidPutObject.builder().build()), true, emptySet(), "", "", + singletonList(BidPutObject.builder().build()), true, emptySet(), "", 100, "", expiredTimeout); // then @@ -797,7 +798,7 @@ public void cachePutObjectsShouldTolerateGlobalTimeoutAlreadyExpired() { public void cachePutObjectsShouldReturnResultWithEmptyListWhenPutObjectsIsEmpty() { // when final Future result = target.cachePutObjects(emptyList(), true, - emptySet(), null, null, null); + emptySet(), null, 100, null, null); // then verifyNoInteractions(httpClient); @@ -847,6 +848,7 @@ public void cachePutObjectsShouldCacheObjects() throws IOException { true, singleton("bidder1"), "account", + null, "pbjs", timeout); @@ -863,6 +865,8 @@ public void cachePutObjectsShouldCacheObjects() throws IOException { verify(metrics).updateVtrackCacheWriteRequestTime(eq("account"), anyLong(), eq(MetricName.ok)); + verify(metrics).updateVtrackCacheWriteRequestTime(eq("account"), anyLong(), eq(MetricName.ok)); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), firstBidPutObject, "account", "pbjs"); verify(vastModifier).modifyVastXml(true, singleton("bidder1"), secondBidPutObject, "account", "pbjs"); @@ -888,6 +892,268 @@ public void cachePutObjectsShouldCacheObjects() throws IOException { .containsExactly(modifiedFirstBidPutObject, modifiedSecondBidPutObject, modifiedThirdBidPutObject); } + @Test + public void cachePutObjectsShouldCacheObjectsWithTtlDefinedAsMinBetweenRequestAndAccountTtl() throws IOException { + // given + final BidPutObject firstBidPutObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .ttlseconds(99) + .build(); + final BidPutObject secondBidPutObject = BidPutObject.builder() + .type("xml") + .bidid("bidId2") + .bidder("bidder2") + .timestamp(1L) + .value(new TextNode("VAST")) + .ttlseconds(null) + .build(); + final BidPutObject thirdBidPutObject = BidPutObject.builder() + .type("text") + .bidid("bidId3") + .bidder("bidder3") + .timestamp(1L) + .value(new TextNode("VAST")) + .ttlseconds(101) + .build(); + final BidPutObject fourthBidPutObject = BidPutObject.builder() + .type("json") + .bidid("bidId4") + .bidder("bidder4") + .timestamp(1L) + .value(new TextNode("VAST")) + .ttlseconds(-100) + .build(); + final BidPutObject fifthBidPutObject = BidPutObject.builder() + .type("xml") + .bidid("bidId4") + .bidder("bidder4") + .timestamp(1L) + .value(new TextNode("VAST")) + .ttlseconds(0) + .build(); + + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")) + .willReturn(new TextNode("VAST")) + .willReturn(new TextNode("updatedVast")); + + given(httpClient.post(anyString(), any(), any(), anyLong())).willReturn(Future.succeededFuture( + HttpClientResponse.of(200, null, mapper.writeValueAsString(BidCacheResponse.of( + List.of( + CacheObject.of("uuid1"), + CacheObject.of("uuid2"), + CacheObject.of("uuid3"), + CacheObject.of("uuid4"), + CacheObject.of("uuid5"))))))); + + // when + target.cachePutObjects( + asList(firstBidPutObject, secondBidPutObject, thirdBidPutObject, fourthBidPutObject, fifthBidPutObject), + true, + singleton("bidder1"), + "account", + 100, + "pbjs", + timeout); + + // then + verify(httpClient).post(eq("http://cache-service/cache"), any(), any(), anyLong()); + + verify(metrics).updateVtrackCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); + verify(metrics).updateVtrackCacheCreativeSize(eq("account"), eq(4), eq(MetricName.xml)); + verify(metrics).updateVtrackCacheCreativeSize(eq("account"), eq(11), eq(MetricName.unknown)); + verify(metrics).updateVtrackCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); + verify(metrics).updateVtrackCacheCreativeSize(eq("account"), eq(4), eq(MetricName.xml)); + + verify(metrics).updateVtrackCacheCreativeTtl(eq("account"), eq(99), eq(MetricName.json)); + verify(metrics, times(2)).updateVtrackCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.xml)); + verify(metrics).updateVtrackCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.unknown)); + verify(metrics).updateVtrackCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.json)); + + verify(metrics).updateVtrackCacheWriteRequestTime(eq("account"), anyLong(), eq(MetricName.ok)); + + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), firstBidPutObject, "account", "pbjs"); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), secondBidPutObject, "account", "pbjs"); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), thirdBidPutObject, "account", "pbjs"); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), fourthBidPutObject, "account", "pbjs"); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), fifthBidPutObject, "account", "pbjs"); + + final BidPutObject modifiedFirstBidPutObject = firstBidPutObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("modifiedVast")) + .ttlseconds(99) + .build(); + final BidPutObject modifiedSecondBidPutObject = secondBidPutObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .ttlseconds(100) + .build(); + final BidPutObject modifiedThirdBidPutObject = thirdBidPutObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("updatedVast")) + .ttlseconds(100) + .build(); + final BidPutObject modifiedFourthBidPutObject = fourthBidPutObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("updatedVast")) + .ttlseconds(100) + .build(); + final BidPutObject modifiedFifthBidPutObject = fifthBidPutObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("updatedVast")) + .ttlseconds(100) + .build(); + + assertThat(captureBidCacheRequest().getPuts()).containsExactly( + modifiedFirstBidPutObject, + modifiedSecondBidPutObject, + modifiedThirdBidPutObject, + modifiedFourthBidPutObject, + modifiedFifthBidPutObject); + } + + @Test + public void cachePutObjectsShouldCacheObjectsWithTtlDefinedAsPutObjectTtlWhenAccountTtlIsNegative() + throws IOException { + + // given + final BidPutObject bidObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .ttlseconds(100) + .build(); + + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")); + + given(httpClient.post(anyString(), any(), any(), anyLong())).willReturn(Future.succeededFuture( + HttpClientResponse.of(200, null, mapper.writeValueAsString(BidCacheResponse.of( + List.of(CacheObject.of("uuid1"))))))); + + // when + target.cachePutObjects( + singletonList(bidObject), + true, + singleton("bidder1"), + "account", + -20, + "pbjs", + timeout); + + // then + final BidPutObject expectedBidObject = bidObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("modifiedVast")) + .ttlseconds(100) + .build(); + + assertThat(captureBidCacheRequest().getPuts()).containsExactly(expectedBidObject); + } + + @Test + public void cachePutObjectsShouldCacheObjectsWithTtlDefinedAsPutObjectTtlWhenAccountTtlIsZero() + throws IOException { + + // given + final BidPutObject bidObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .ttlseconds(100) + .build(); + + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")); + + given(httpClient.post(anyString(), any(), any(), anyLong())).willReturn(Future.succeededFuture( + HttpClientResponse.of(200, null, mapper.writeValueAsString(BidCacheResponse.of( + List.of(CacheObject.of("uuid1"))))))); + + // when + target.cachePutObjects( + singletonList(bidObject), + true, + singleton("bidder1"), + "account", + 0, + "pbjs", + timeout); + + // then + final BidPutObject expectedBidObject = bidObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("modifiedVast")) + .ttlseconds(100) + .build(); + + assertThat(captureBidCacheRequest().getPuts()).containsExactly(expectedBidObject); + } + + @Test + public void cachePutObjectsShouldNotProvideTtlWhenAccountTtlIsNegativeAndPutObjectTtlIsZero() + throws IOException { + + // given + final BidPutObject bidObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .ttlseconds(0) + .build(); + + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")); + + given(httpClient.post(anyString(), any(), any(), anyLong())).willReturn(Future.succeededFuture( + HttpClientResponse.of(200, null, mapper.writeValueAsString(BidCacheResponse.of( + List.of(CacheObject.of("uuid1"))))))); + + // when + target.cachePutObjects( + singletonList(bidObject), + true, + singleton("bidder1"), + "account", + -10, + "pbjs", + timeout); + + // then + final BidPutObject expectedBidObject = bidObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("modifiedVast")) + .ttlseconds(null) + .build(); + + assertThat(captureBidCacheRequest().getPuts()).containsExactly(expectedBidObject); + } + @Test public void cachePutObjectsShouldLogErrorMetricsWhenStatusCodeIsNotOk() { // given @@ -914,6 +1180,7 @@ public void cachePutObjectsShouldLogErrorMetricsWhenStatusCodeIsNotOk() { true, singleton("bidder1"), "account", + 100, "pbjs", timeout); @@ -950,6 +1217,7 @@ public void cachePutObjectsShouldNotLogErrorMetricsWhenCacheServiceIsNotConnecte true, singleton("bidder1"), "account", + 100, "pbjs", timeout); @@ -993,7 +1261,7 @@ public void cachePutObjectsShouldCallInternalCacheEndpointWhenProvided() throws .willReturn(new TextNode("modifiedVast")); // when - target.cachePutObjects(asList(firstBidPutObject), true, singleton("bidder1"), "account", "pbjs", timeout); + target.cachePutObjects(asList(firstBidPutObject), true, singleton("bidder1"), "account", 100, "pbjs", timeout); // then verify(httpClient).post(eq("http://cache-service-internal/cache"), any(), any(), anyLong()); @@ -1046,6 +1314,7 @@ public void cachePutObjectsShouldUseApiKeyWhenProvided() throws MalformedURLExce true, singleton("bidder1"), "account", + 100, "pbjs", timeout); @@ -1281,6 +1550,7 @@ public void cachePutObjectsShouldPrependTraceInfoWhenEnabled() throws IOExceptio true, singleton("bidder1"), "account", + null, "pbjs", timeout); @@ -1329,6 +1599,7 @@ public void cachePutObjectsShouldPrependTraceInfoWithDatacenterWhenEnabled() thr true, singleton("bidder1"), "account", + null, "pbjs", timeout); @@ -1376,6 +1647,7 @@ public void cachePutObjectsShouldNotPrependTraceInfoToPassedInKey() throws IOExc true, singleton("bidder1"), "account", + null, "pbjs", timeout); @@ -1435,6 +1707,7 @@ public void cachePutObjectsShouldNotEmitEmptyTtlMetrics() { true, singleton("bidder1"), "account", + null, "pbjs", timeout); diff --git a/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java b/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java index 98011db6ecf..b714643d8e0 100644 --- a/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java @@ -27,6 +27,7 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountEventsConfig; +import org.prebid.server.settings.model.AccountVtrackConfig; import org.prebid.server.util.HttpUtil; import java.util.ArrayList; @@ -248,7 +249,7 @@ public void shouldRespondWithInternalServerErrorWhenCacheServiceReturnFailure() .events(AccountEventsConfig.of(true)) .build()) .build())); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) .willReturn(Future.failedFuture("error")); // when @@ -273,15 +274,15 @@ public void shouldTolerateNotFoundAccount() throws JsonProcessingException { given(applicationSettings.getAccountById(any(), any())) .willReturn(Future.failedFuture(new PreBidException("not found"))); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture(BidCacheResponse.of(emptyList()))); // when handler.handle(routingContext); // then - verify(coreCacheService).cachePutObjects(eq(bidPutObjects), any(), eq(singleton("bidder")), eq("accountId"), - eq("pbjs"), any()); + verify(coreCacheService).cachePutObjects( + eq(bidPutObjects), any(), eq(singleton("bidder")), eq("accountId"), any(), eq("pbjs"), any()); } @Test @@ -299,15 +300,39 @@ public void shouldSendToCacheNullInAccountEnabledAndValidBiddersWhenAccountEvent given(applicationSettings.getAccountById(any(), any())) .willReturn(Future.succeededFuture(Account.builder().build())); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) + .willReturn(Future.succeededFuture(BidCacheResponse.of(emptyList()))); + + // when + handler.handle(routingContext); + + // then + verify(coreCacheService).cachePutObjects( + eq(bidPutObjects), isNull(), eq(singleton("bidder")), eq("accountId"), isNull(), eq("pbjs"), any()); + } + + @Test + public void shouldSendToCacheAccountTtlWhenAccountTtlIsPresent() throws JsonProcessingException { + // given + final List bidPutObjects = singletonList( + BidPutObject.builder() + .bidid("bidId") + .bidder("bidder") + .type("xml") + .value(new TextNode(" expectedBidders = new HashSet<>(asList("bidder", "updatable_bidder")); verify(coreCacheService).cachePutObjects( - eq(bidPutObjects), any(), eq(expectedBidders), eq("accountId"), eq("pbjs"), any()); + eq(bidPutObjects), any(), eq(expectedBidders), eq("accountId"), isNull(), eq("pbjs"), any()); verify(httpResponse).end(eq("{\"responses\":[{\"uuid\":\"uuid1\"},{\"uuid\":\"uuid2\"}]}")); } @@ -428,7 +453,7 @@ public void shouldSendToCacheExpectedPutsWhenModifyVastForUnknownBidderAndAllowU .events(AccountEventsConfig.of(true)) .build()) .build())); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture(BidCacheResponse.of( asList(CacheObject.of("uuid1"), CacheObject.of("uuid2"))))); @@ -438,7 +463,7 @@ public void shouldSendToCacheExpectedPutsWhenModifyVastForUnknownBidderAndAllowU // then final HashSet expectedBidders = new HashSet<>(asList("bidder", "updatable_bidder")); verify(coreCacheService).cachePutObjects( - eq(bidPutObjects), any(), eq(expectedBidders), eq("accountId"), eq("pbjs"), any()); + eq(bidPutObjects), any(), eq(expectedBidders), eq("accountId"), isNull(), eq("pbjs"), any()); verify(httpResponse).end(eq("{\"responses\":[{\"uuid\":\"uuid1\"},{\"uuid\":\"uuid2\"}]}")); } @@ -471,7 +496,7 @@ public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenAllowUnkn .willReturn(Future.succeededFuture(Account.builder().auction(AccountAuctionConfig.builder() .events(AccountEventsConfig.of(true)).build()) .build())); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture(BidCacheResponse.of( asList(CacheObject.of("uuid1"), CacheObject.of("uuid2"))))); @@ -479,7 +504,7 @@ public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenAllowUnkn handler.handle(routingContext); // then - verify(coreCacheService).cachePutObjects(any(), any(), eq(emptySet()), any(), any(), any()); + verify(coreCacheService).cachePutObjects(any(), any(), eq(emptySet()), any(), any(), any(), any()); } @Test @@ -510,7 +535,7 @@ public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenModifyVas .willReturn(Future.succeededFuture(Account.builder().auction(AccountAuctionConfig.builder() .events(AccountEventsConfig.of(true)).build()) .build())); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture(BidCacheResponse.of( asList(CacheObject.of("uuid1"), CacheObject.of("uuid2"))))); @@ -518,7 +543,7 @@ public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenModifyVas handler.handle(routingContext); // then - verify(coreCacheService).cachePutObjects(any(), any(), eq(emptySet()), any(), any(), any()); + verify(coreCacheService).cachePutObjects(any(), any(), eq(emptySet()), any(), any(), any(), any()); } @SafeVarargs