diff --git a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy index 9aee1c69c68..19a29ae0058 100644 --- a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy @@ -8,6 +8,7 @@ enum ModuleName { PB_RESPONSE_CORRECTION ("pb-response-correction"), ORTB2_BLOCKING("ortb2-blocking"), PB_REQUEST_CORRECTION('pb-request-correction'), + OPTABLE_TARGETING('optable-targeting'), PB_RULE_ENGINE('pb-rule-engine') @JsonValue diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Audience.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Audience.groovy new file mode 100644 index 00000000000..9ea6345ff0e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Audience.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class Audience { + + String provider + List ids +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AudienceId.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AudienceId.groovy new file mode 100644 index 00000000000..e964b9f84b5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AudienceId.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AudienceId { + + String id +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/CacheProperties.groovy b/src/test/groovy/org/prebid/server/functional/model/config/CacheProperties.groovy new file mode 100644 index 00000000000..028d5b203df --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/CacheProperties.groovy @@ -0,0 +1,21 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +class CacheProperties { + + Boolean enabled + Integer ttlSeconds + + static CacheProperties getDefault() { + new CacheProperties().tap { + enabled = true + ttlSeconds = PBSUtils.randomNumber + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionGroup.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionGroup.groovy index 09e95d7753d..eb32b75e729 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionGroup.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionGroup.groovy @@ -18,7 +18,7 @@ class ExecutionGroup { static ExecutionGroup getModuleExecutionGroup(ModuleName name, Stage stage) { new ExecutionGroup().tap { - timeout = 100 + timeout = 1000 hookSequence = [new HookId(moduleCode: name.code, hookImplCode: ModuleHookImplementation.forValue(name, stage).code)] } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/IdentifierType.groovy b/src/test/groovy/org/prebid/server/functional/model/config/IdentifierType.groovy new file mode 100644 index 00000000000..7148c869c77 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/IdentifierType.groovy @@ -0,0 +1,49 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue + +import static org.prebid.server.functional.model.config.OperatingSystem.ANDROID +import static org.prebid.server.functional.model.config.OperatingSystem.FIRE +import static org.prebid.server.functional.model.config.OperatingSystem.IOS +import static org.prebid.server.functional.model.config.OperatingSystem.ROKU +import static org.prebid.server.functional.model.config.OperatingSystem.TIZEN + +enum IdentifierType { + + EMAIL_ADDRESS("e"), + PHONE_NUMBER("p"), + POSTAL_CODE("z"), + APPLE_IDFA("a"), + GOOGLE_GAID("g"), + ROKU_RIDA("r"), + SAMSUNG_TIFA("s"), + AMAZON_AFAI("f"), + NET_ID("n"), + ID5("id5"), + UTIQ("utiq"), + OPTABLE_VID("v") + + @JsonValue + final String value + + IdentifierType(String value) { + this.value = value + } + + static IdentifierType fromOS(OperatingSystem os) { + switch (os) { + case IOS: + return APPLE_IDFA + case ANDROID: + return GOOGLE_GAID + case ROKU: + return ROKU_RIDA + case TIZEN: + return SAMSUNG_TIFA + case FIRE: + return AMAZON_AFAI + default: + throw new IllegalArgumentException("Unsupported OS: " + os); + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy index 4d9424e1c39..af7bf670c95 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy @@ -11,6 +11,7 @@ enum ModuleHookImplementation { ORTB2_BLOCKING_BIDDER_REQUEST("ortb2-blocking-bidder-request"), ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response"), PB_REQUEST_CORRECTION_PROCESSED_AUCTION_REQUEST("pb-request-correction-processed-auction-request"), + OPTABLE_TARGETING_PROCESSED_AUCTION_REQUEST("optable-targeting-processed-auction-request-hook"), PB_RULES_ENGINE_PROCESSED_AUCTION_REQUEST("pb-rule-engine-processed-auction-request") @JsonValue diff --git a/src/test/groovy/org/prebid/server/functional/model/config/OperatingSystem.groovy b/src/test/groovy/org/prebid/server/functional/model/config/OperatingSystem.groovy new file mode 100644 index 00000000000..f10e28a4cb6 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/OperatingSystem.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue + +enum OperatingSystem { + + IOS("ios"), + ANDROID("android"), + ROKU("roku"), + TIZEN("tizen"), + FIRE("fire") + + @JsonValue + final String value + + OperatingSystem(String value) { + this.value = value; + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/OptableTargetingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/OptableTargetingConfig.groovy new file mode 100644 index 00000000000..56e6f3415b8 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/OptableTargetingConfig.groovy @@ -0,0 +1,33 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class OptableTargetingConfig { + + String apiEndpoint + String apiKey + String tenant + String origin + Map ppidMapping + Boolean adserverTargeting + Long timeout + String idPrefixOrder + CacheProperties cache + + static OptableTargetingConfig getDefault(Map ppidMapping) { + new OptableTargetingConfig().tap { + it.apiKey = PBSUtils.randomString + it.tenant = PBSUtils.randomString + it.origin = PBSUtils.randomString + it.apiEndpoint = PBSUtils.randomString + it.adserverTargeting = true + it.ppidMapping = ppidMapping + it.cache = CacheProperties.default + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy index 801178fd4d4..ac2685742b6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy @@ -13,5 +13,6 @@ class PbsModulesConfig { Ortb2BlockingConfig ortb2Blocking PbResponseCorrection pbResponseCorrection PbRequestCorrectionConfig pbRequestCorrection + OptableTargetingConfig optableTargeting PbRulesEngine pbRuleEngine } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/TargetingOrtb.groovy b/src/test/groovy/org/prebid/server/functional/model/config/TargetingOrtb.groovy new file mode 100644 index 00000000000..0e6b8d93119 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/TargetingOrtb.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.User + +@ToString(includeNames = true, ignoreNulls = true) +class TargetingOrtb { + + User user +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/TargetingResult.groovy b/src/test/groovy/org/prebid/server/functional/model/config/TargetingResult.groovy new file mode 100644 index 00000000000..d9f2bc7b30d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/TargetingResult.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class TargetingResult { + + List audience + TargetingOrtb ortb2 +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/StoredCache.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/StoredCache.groovy new file mode 100644 index 00000000000..6f772af6d70 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/StoredCache.groovy @@ -0,0 +1,118 @@ +package org.prebid.server.functional.testcontainers.scaffolding + +import org.mockserver.matchers.TimeToLive +import org.mockserver.matchers.Times +import org.mockserver.model.HttpRequest +import org.mockserver.model.HttpStatusCode +import org.prebid.server.functional.model.config.Audience +import org.prebid.server.functional.model.config.AudienceId +import org.prebid.server.functional.model.config.IdentifierType +import org.prebid.server.functional.model.config.OptableTargetingConfig +import org.prebid.server.functional.model.config.TargetingOrtb +import org.prebid.server.functional.model.config.TargetingResult +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.User +import org.prebid.server.functional.util.PBSUtils +import org.testcontainers.containers.MockServerContainer + +import java.nio.charset.StandardCharsets + +import static org.mockserver.model.HttpRequest.request +import static org.mockserver.model.HttpResponse.response +import static org.mockserver.model.HttpStatusCode.NO_CONTENT_204 +import static org.mockserver.model.HttpStatusCode.OK_200 + +class StoredCache extends NetworkScaffolding { + + private static final String CACHE_ENDPOINT = "/stored-cache" + + StoredCache(MockServerContainer mockServerContainer) { + super(mockServerContainer, CACHE_ENDPOINT) + } + + @Override + protected HttpRequest getRequest(String impId) {} + + @Override + HttpRequest getRequest() { + request().withMethod("GET") + .withPath(endpoint) + } + + @Override + void setResponse() {} + + TargetingResult setTargetingResponse(BidRequest bidRequest, OptableTargetingConfig config) { + def targetingResult = getBodyByRequest(bidRequest) + mockServerClient.when(request() + .withMethod("GET") + .withPath("$endpoint${QueryBuilder.buildQuery(bidRequest, config)}"), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond { response().withStatusCode(OK_200.code()).withBody(encode(targetingResult)) } + targetingResult + } + + TargetingResult setCachedTargetingResponse(BidRequest bidRequest) { + def targetingResult = getBodyByRequest(bidRequest) + mockServerClient.when(request() + .withMethod("GET") + .withPath(endpoint), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond { response().withStatusCode(OK_200.code()).withBody(encode(targetingResult)) } + targetingResult + } + + void setCachingResponse(HttpStatusCode statusCode = NO_CONTENT_204) { + mockServerClient.when(request() + .withMethod("POST") + .withPath(endpoint), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond { response().withStatusCode(statusCode.code()) } + } + + private static TargetingResult getBodyByRequest(BidRequest bidRequest) { + new TargetingResult().tap { + it.audience = [new Audience(ids: [new AudienceId(id: PBSUtils.randomString)], provider: PBSUtils.randomString)] + it.ortb2 = new TargetingOrtb(user: new User(data: bidRequest.user.data, eids: bidRequest.user.eids)) + } + } + + private class QueryBuilder { + + static String buildQuery(BidRequest bidRequest, OptableTargetingConfig config) { + buildIdsString(config) + buildAttributesString(bidRequest, config) + } + + private static String buildIdsString(OptableTargetingConfig config) { + def ppids = config.ppidMapping + if (!ppids) { + return '' + } + + def reorderedIds = reorderIds(ppids.keySet(), config.idPrefixOrder) + + reorderedIds.collect { id -> + def value = ppids[id] + "&id=${URLEncoder.encode("${id.value}:${value}", StandardCharsets.UTF_8)}" + }.join('') + } + + private static Set reorderIds(Set ids, String idPrefixOrder) { + if (!idPrefixOrder) { + return ids + } + def prefixOrder = idPrefixOrder.split(',') as List + def prefixToPriority = prefixOrder.collectEntries { v, i -> [(v): i] } + ids.sort { prefixToPriority.get(it.value, Integer.MAX_VALUE) } + } + + private static String buildAttributesString(BidRequest bidRequest, OptableTargetingConfig config) { + def regs = bidRequest.regs + def gdpr = regs?.gdpr + def gdprConsent = bidRequest.user?.consent + + [gdprConsent != null ? "&gdpr_consent=${gdprConsent}" : null, + "&gdpr=${gdpr ? 1 : 0}", + regs?.gpp ? "&gpp=${regs.gpp}" : null, + regs?.gppSid ? "&gpp_sid=${regs.gppSid.first()}" : null, + config?.timeout ? "&timeout=${config.timeout}ms" : null].findAll().join('') + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy index f721b93eac0..c0933a238e7 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy @@ -7,7 +7,9 @@ import org.prebid.server.functional.model.response.auction.AnalyticResult import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.InvocationResult import org.prebid.server.functional.tests.BaseSpec +import org.prebid.server.functional.util.PBSUtils +import static org.prebid.server.functional.model.ModuleName.OPTABLE_TARGETING import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER @@ -16,6 +18,7 @@ import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES import static org.prebid.server.functional.model.config.Stage.PROCESSED_AUCTION_REQUEST +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class ModuleBaseSpec extends BaseSpec { @@ -56,6 +59,15 @@ class ModuleBaseSpec extends BaseSpec { .collectEntries { key, value -> [(key.toString()): value.toString()] } } + protected static Map getOptableTargetingSettings(boolean isEnabled = true, Endpoint endpoint = OPENRTB2_AUCTION) { + ["hooks.${OPTABLE_TARGETING.code}.enabled": isEnabled as String, + "hooks.modules.${OPTABLE_TARGETING.code}.api-endpoint" : "$networkServiceContainer.rootUri/stored-cache".toString(), + "hooks.modules.${OPTABLE_TARGETING.code}.tenant" : PBSUtils.randomString, + "hooks.modules.${OPTABLE_TARGETING.code}.origin" : PBSUtils.randomString, + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, [(PROCESSED_AUCTION_REQUEST): [OPTABLE_TARGETING]]))] + .collectEntries { key, value -> [(key.toString()): value.toString()] } + } + protected static Map getOrtb2BlockingSettings(boolean isEnabled = true) { ["hooks.${ORTB2_BLOCKING.code}.enabled": isEnabled as String] } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/optabletargeting/CacheStorageSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/optabletargeting/CacheStorageSpec.groovy new file mode 100644 index 00000000000..3d1665a101b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/optabletargeting/CacheStorageSpec.groovy @@ -0,0 +1,201 @@ +package org.prebid.server.functional.tests.module.optabletargeting + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.IdentifierType +import org.prebid.server.functional.model.config.OperatingSystem +import org.prebid.server.functional.model.config.OptableTargetingConfig +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Data +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Eid +import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.PublicCountryIp +import org.prebid.server.functional.model.request.auction.User +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.scaffolding.StoredCache +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import static org.apache.commons.codec.binary.Base64.encodeBase64 +import static org.mockserver.model.HttpStatusCode.NOT_FOUND_404 +import static org.prebid.server.functional.model.ModuleName.OPTABLE_TARGETING +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class CacheStorageSpec extends ModuleBaseSpec { + + private static final String METRIC_CREATIVE_SIZE_TEXT = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.entry_size.text" + private static final String METRIC_CREATIVE_TTL_TEXT = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.entry_ttl.text" + + private static final String METRIC_CREATIVE_READ_OK = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.read.ok" + private static final String METRIC_CREATIVE_READ_ERR = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.read.err" + private static final String METRIC_CREATIVE_WRITE_OK = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.write.ok" + private static final String METRIC_CREATIVE_WRITE_ERR = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.write.err" + + private static final StoredCache storedCache = new StoredCache(networkServiceContainer) + + private static final Map CACHE_STORAGE_CONFIG = ['storage.pbc.path' : "$networkServiceContainer.rootUri/stored-cache".toString(), + 'storage.pbc.call-timeout-ms': '1000', + 'storage.pbc.enabled' : 'true', + 'cache.module.enabled' : 'true', + 'pbc.api.key' : PBSUtils.randomString, + 'cache.api-key-secured' : 'false'] + private static final Map MODULE_STORAGE_CACHE_CONFIG = getOptableTargetingSettings() + CACHE_STORAGE_CONFIG + private static final PrebidServerService prebidServerStoredCacheService = pbsServiceFactory.getService(MODULE_STORAGE_CACHE_CONFIG) + + def setup() { + storedCache.reset() + } + + def cleanupSpec() { + pbsServiceFactory.removeContainer(MODULE_STORAGE_CACHE_CONFIG) + } + + def "PBS should update error metrics when no cached requests present"() { + given: "Default BidRequest with cache and device info" + def randomIfa = PBSUtils.randomString + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) + def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(prebidServerStoredCacheService) + + when: "PBS processes auction request" + prebidServerStoredCacheService.sendAuctionRequest(bidRequest) + + then: "PBS should update metrics for new saved text storage cache" + def metrics = prebidServerStoredCacheService.sendCollectedMetricsRequest() + assert metrics[METRIC_CREATIVE_READ_ERR] == 1 + + and: "No updates for success metrics" + assert !metrics[METRIC_CREATIVE_SIZE_TEXT] + assert !metrics[METRIC_CREATIVE_TTL_TEXT] + assert !metrics[METRIC_CREATIVE_READ_OK] + } + + def "PBS should update error metrics when external service responded with invalid values"() { + given: "Default BidRequest with cache and device info" + def randomIfa = PBSUtils.randomString + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) + def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) + accountDao.save(account) + + and: "Mocked external request" + storedCache.setTargetingResponse(bidRequest, targetingConfig) + storedCache.setCachingResponse(NOT_FOUND_404) + + and: "Flash metrics" + flushMetrics(prebidServerStoredCacheService) + + when: "PBS processes auction request" + prebidServerStoredCacheService.sendAuctionRequest(bidRequest) + + then: "PBS should update error metrics" + def metrics = prebidServerStoredCacheService.sendCollectedMetricsRequest() + assert metrics[METRIC_CREATIVE_WRITE_ERR] == 1 + + and: "No updates for success metrics" + assert !metrics[METRIC_CREATIVE_WRITE_OK] + } + + def "PBS should update metrics for new saved text storage cache when no cached requests"() { + given: "Default BidRequest with cache and device info" + def randomIfa = PBSUtils.randomString + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) + def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) + accountDao.save(account) + + and: "Mocked external request" + def targetingResult = storedCache.setTargetingResponse(bidRequest, targetingConfig) + storedCache.setCachingResponse() + + and: "Flash metrics" + flushMetrics(prebidServerStoredCacheService) + + when: "PBS processes auction request" + prebidServerStoredCacheService.sendAuctionRequest(bidRequest) + + then: "PBS should update metrics for new saved text storage cache" + def metrics = prebidServerStoredCacheService.sendCollectedMetricsRequest() + assert metrics[METRIC_CREATIVE_SIZE_TEXT] == encodeBase64(encode(targetingResult).bytes).size() + assert metrics[METRIC_CREATIVE_TTL_TEXT] == targetingConfig.cache.ttlSeconds + assert metrics[METRIC_CREATIVE_WRITE_OK] == 1 + } + + def "PBS should update metrics for stored cached requests cache when proper record present"() { + given: "Current value of metric prebid cache" + def textInitialValue = getCurrentMetricValue(prebidServerStoredCacheService, METRIC_CREATIVE_SIZE_TEXT) + def ttlInitialValue = getCurrentMetricValue(prebidServerStoredCacheService, METRIC_CREATIVE_TTL_TEXT) + def writeInitialValue = getCurrentMetricValue(prebidServerStoredCacheService, METRIC_CREATIVE_WRITE_OK) + def readErrorInitialValue = getCurrentMetricValue(prebidServerStoredCacheService, METRIC_CREATIVE_READ_ERR) + def writeErrorInitialValue = getCurrentMetricValue(prebidServerStoredCacheService, METRIC_CREATIVE_WRITE_ERR) + + and: "Default BidRequest with cache and device info" + def randomIfa = PBSUtils.randomString + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) + def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) + accountDao.save(account) + + and: "Mocked external request" + storedCache.setCachedTargetingResponse(bidRequest) + storedCache.setCachingResponse() + + and: "Flash metrics" + flushMetrics(prebidServerStoredCacheService) + + when: "PBS processes auction request" + prebidServerStoredCacheService.sendAuctionRequest(bidRequest) + + then: "PBS should update metrics for stored cached requests" + def metrics = prebidServerStoredCacheService.sendCollectedMetricsRequest() + assert metrics[METRIC_CREATIVE_READ_OK] == 1 + + and: "No updates for new saved text storage metrics" + assert metrics[METRIC_CREATIVE_SIZE_TEXT] == textInitialValue + assert metrics[METRIC_CREATIVE_TTL_TEXT] == ttlInitialValue + assert metrics[METRIC_CREATIVE_WRITE_OK] == writeInitialValue + + and: "No update for error metrics" + assert metrics[METRIC_CREATIVE_READ_ERR] == readErrorInitialValue + assert metrics[METRIC_CREATIVE_WRITE_ERR] == writeErrorInitialValue + } + + private static BidRequest getBidRequestForModuleCacheStorage(String ifa, OperatingSystem os) { + BidRequest.defaultBidRequest.tap { + it.enableCache() + it.user = new User(id: PBSUtils.randomString, data: [Data.defaultData], eids: [Eid.defaultEid]) + it.device = new Device(geo: Geo.FPDGeo, + ip: PBSUtils.getRandomEnum(PublicCountryIp.class).v4, + ifa: ifa, + os: os) + } + } + + private static Account createAccountWithRequestCorrectionConfig(BidRequest bidRequest, + OptableTargetingConfig optableTargetingConfig) { + + def pbsModulesConfig = new PbsModulesConfig(optableTargeting: optableTargetingConfig) + def accountHooksConfig = new AccountHooksConfiguration(modules: pbsModulesConfig) + def accountConfig = new AccountConfig(hooks: accountHooksConfig) + new Account(uuid: bidRequest.accountId, config: accountConfig) + } +}