From dab0f43346b834e5f59e8a2e1aa638c1044be18b Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Thu, 25 Sep 2025 15:44:12 +0300 Subject: [PATCH 1/3] Tests: Module Storage Metrics --- .../server/functional/model/ModuleName.groovy | 1 + .../functional/model/config/Audience.groovy | 11 ++ .../functional/model/config/AudienceId.groovy | 8 + .../model/config/CacheProperties.groovy | 21 ++ .../model/config/ExecutionGroup.groovy | 2 +- .../model/config/IdentifierType.groovy | 26 +++ .../config/ModuleHookImplementation.groovy | 1 + .../model/config/OperatingSystem.groovy | 19 ++ .../config/OptableTargetingConfig.groovy | 30 +++ .../model/config/PbsModulesConfig.groovy | 1 + .../model/config/TargetingOrtb.groovy | 10 + .../model/config/TargetingResult.groovy | 10 + .../scaffolding/StoredCache.groovy | 118 +++++++++++ .../tests/module/ModuleBaseSpec.groovy | 12 ++ .../optabletargeting/CacheStorageSpec.groovy | 187 ++++++++++++++++++ 15 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/Audience.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/AudienceId.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/CacheProperties.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/IdentifierType.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/OperatingSystem.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/OptableTargetingConfig.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/TargetingOrtb.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/TargetingResult.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/StoredCache.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/module/optabletargeting/CacheStorageSpec.groovy 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 2bc06ab7144..43c91f727ca 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') @JsonValue final String code 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..69e9a9224f5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Audience.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class Audience { + String provider + List ids + String keyspace + Integer rtbSegtax +} 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..03c74d8144e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AudienceId.groovy @@ -0,0 +1,8 @@ +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..0c1fab5810c --- /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 + int 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..c23e229416c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/IdentifierType.groovy @@ -0,0 +1,26 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue + +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 + } +} 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 247bdea4353..b2a4f56b376 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"), @JsonValue final String code 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..50d69e3c39d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/OptableTargetingConfig.groovy @@ -0,0 +1,30 @@ +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.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 59f640f966c..c22070e6325 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,4 +13,5 @@ class PbsModulesConfig { Ortb2BlockingConfig ortb2Blocking PbResponseCorrection pbResponseCorrection PbRequestCorrectionConfig pbRequestCorrection + OptableTargetingConfig optableTargeting } 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..a3372352259 --- /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('/stored-cache'), 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 453de43aa3c..261a55ad099 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 @@ -4,7 +4,9 @@ import org.prebid.server.functional.model.config.Endpoint import org.prebid.server.functional.model.config.ExecutionPlan import org.prebid.server.functional.model.config.Stage 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_REQUEST_CORRECTION import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION @@ -12,6 +14,7 @@ import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER 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 { @@ -52,6 +55,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..5dd8ba1d798 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/optabletargeting/CacheStorageSpec.groovy @@ -0,0 +1,187 @@ +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.OptableTargetingConfig +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.config.TargetingResult +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.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.model.config.IdentifierType.GOOGLE_GAID +import static org.prebid.server.functional.model.config.OperatingSystem.ANDROID +import static org.prebid.server.functional.model.request.auction.PublicCountryIp.USA_IP +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 final PrebidServerService prebidServerStoredCacheService = pbsServiceFactory.getService(getOptableTargetingSettings() + CACHE_STORAGE_CONFIG) + + def cleanup() { + storedCache.reset() + } + + def cleanupSpec() { + pbsServiceFactory.removeContainer(getOptableTargetingSettings() + CACHE_STORAGE_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 bidRequest = getBidRequestForModuleCacheStorage(randomIfa) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(GOOGLE_GAID): 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 bidRequest = getBidRequestForModuleCacheStorage(randomIfa) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(GOOGLE_GAID): 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 metrics for new saved text storage cache" + 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 bidRequest = getBidRequestForModuleCacheStorage(randomIfa) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(GOOGLE_GAID): 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] == getTargetingResultSize(targetingResult) + 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: "Default BidRequest with cache and device info" + def randomIfa = PBSUtils.randomString + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(GOOGLE_GAID): 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] + assert !metrics[METRIC_CREATIVE_TTL_TEXT] + assert !metrics[METRIC_CREATIVE_WRITE_OK] + } + + private static BidRequest getBidRequestForModuleCacheStorage(String ifa) { + BidRequest.defaultBidRequest.tap { + it.enableCache() + it.user = new User(id: PBSUtils.randomString, data: [Data.defaultData], eids: [Eid.defaultEid]) + it.device = new Device(geo: Geo.getFPDGeo(), ip: USA_IP.v4, ifa: ifa, os: ANDROID.value) + } + } + + 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) + } + + private static getTargetingResultSize(TargetingResult result) { + encodeBase64(encode(result).bytes).size() + } +} From 56c074edb061c0db5734fca3791a64455f235a0f Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Tue, 21 Oct 2025 14:57:42 +0300 Subject: [PATCH 2/3] update after review --- .../functional/model/config/Audience.groovy | 3 +- .../functional/model/config/AudienceId.groovy | 1 + .../model/config/CacheProperties.groovy | 4 +- .../config/OptableTargetingConfig.groovy | 3 ++ .../scaffolding/StoredCache.groovy | 2 +- .../optabletargeting/CacheStorageSpec.groovy | 47 +++++++++++-------- 6 files changed, 35 insertions(+), 25 deletions(-) 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 index 69e9a9224f5..9ea6345ff0e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Audience.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Audience.groovy @@ -4,8 +4,7 @@ import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) class Audience { + String provider List ids - String keyspace - Integer rtbSegtax } 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 index 03c74d8144e..e964b9f84b5 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AudienceId.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AudienceId.groovy @@ -4,5 +4,6 @@ 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 index 0c1fab5810c..028d5b203df 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/CacheProperties.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/CacheProperties.groovy @@ -9,8 +9,8 @@ import org.prebid.server.functional.util.PBSUtils @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class CacheProperties { - boolean enabled - int ttlSeconds + Boolean enabled + Integer ttlSeconds static CacheProperties getDefault() { new CacheProperties().tap { 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 index 50d69e3c39d..56e6f3415b8 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/OptableTargetingConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/OptableTargetingConfig.groovy @@ -21,6 +21,9 @@ class OptableTargetingConfig { 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 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 index a3372352259..6f772af6d70 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/StoredCache.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/StoredCache.groovy @@ -55,7 +55,7 @@ class StoredCache extends NetworkScaffolding { def targetingResult = getBodyByRequest(bidRequest) mockServerClient.when(request() .withMethod("GET") - .withPath('/stored-cache'), Times.unlimited(), TimeToLive.unlimited(), -10) + .withPath(endpoint), Times.unlimited(), TimeToLive.unlimited(), -10) .respond { response().withStatusCode(OK_200.code()).withBody(encode(targetingResult)) } targetingResult } 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 index 5dd8ba1d798..8bd0e318a91 100644 --- 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 @@ -4,7 +4,6 @@ import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountHooksConfiguration import org.prebid.server.functional.model.config.OptableTargetingConfig import org.prebid.server.functional.model.config.PbsModulesConfig -import org.prebid.server.functional.model.config.TargetingResult 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 @@ -27,13 +26,13 @@ import static org.prebid.server.functional.testcontainers.Dependencies.getNetwor 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_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 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) @@ -43,14 +42,15 @@ class CacheStorageSpec extends ModuleBaseSpec { 'cache.module.enabled' : 'true', 'pbc.api.key' : PBSUtils.randomString, 'cache.api-key-secured' : 'false'] - private final PrebidServerService prebidServerStoredCacheService = pbsServiceFactory.getService(getOptableTargetingSettings() + CACHE_STORAGE_CONFIG) + private static final Map MODULE_STORAGE_CACHE_CONFIG = getOptableTargetingSettings() + CACHE_STORAGE_CONFIG + private static final PrebidServerService prebidServerStoredCacheService = pbsServiceFactory.getService(MODULE_STORAGE_CACHE_CONFIG) - def cleanup() { + def setup() { storedCache.reset() } def cleanupSpec() { - pbsServiceFactory.removeContainer(getOptableTargetingSettings() + CACHE_STORAGE_CONFIG) + pbsServiceFactory.removeContainer(MODULE_STORAGE_CACHE_CONFIG) } def "PBS should update error metrics when no cached requests present"() { @@ -99,7 +99,7 @@ class CacheStorageSpec extends ModuleBaseSpec { when: "PBS processes auction request" prebidServerStoredCacheService.sendAuctionRequest(bidRequest) - then: "PBS should update metrics for new saved text storage cache" + then: "PBS should update error metrics" def metrics = prebidServerStoredCacheService.sendCollectedMetricsRequest() assert metrics[METRIC_CREATIVE_WRITE_ERR] == 1 @@ -129,13 +129,20 @@ class CacheStorageSpec extends ModuleBaseSpec { then: "PBS should update metrics for new saved text storage cache" def metrics = prebidServerStoredCacheService.sendCollectedMetricsRequest() - assert metrics[METRIC_CREATIVE_SIZE_TEXT] == getTargetingResultSize(targetingResult) + 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: "Default BidRequest with cache and device info" + 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 bidRequest = getBidRequestForModuleCacheStorage(randomIfa) @@ -159,9 +166,13 @@ class CacheStorageSpec extends ModuleBaseSpec { assert metrics[METRIC_CREATIVE_READ_OK] == 1 and: "No updates for new saved text storage metrics" - assert !metrics[METRIC_CREATIVE_SIZE_TEXT] - assert !metrics[METRIC_CREATIVE_TTL_TEXT] - assert !metrics[METRIC_CREATIVE_WRITE_OK] + 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) { @@ -180,8 +191,4 @@ class CacheStorageSpec extends ModuleBaseSpec { def accountConfig = new AccountConfig(hooks: accountHooksConfig) new Account(uuid: bidRequest.accountId, config: accountConfig) } - - private static getTargetingResultSize(TargetingResult result) { - encodeBase64(encode(result).bytes).size() - } } From e87c68564d817469333ad3582d02f2ccff12ee14 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Tue, 21 Oct 2025 20:27:43 +0300 Subject: [PATCH 3/3] update after review --- .../model/config/IdentifierType.groovy | 23 +++++++++++++ .../optabletargeting/CacheStorageSpec.groovy | 33 +++++++++++-------- 2 files changed, 43 insertions(+), 13 deletions(-) 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 index c23e229416c..7148c869c77 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/IdentifierType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/IdentifierType.groovy @@ -2,6 +2,12 @@ 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"), @@ -23,4 +29,21 @@ enum IdentifierType { 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/tests/module/optabletargeting/CacheStorageSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/optabletargeting/CacheStorageSpec.groovy index 8bd0e318a91..3d1665a101b 100644 --- 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 @@ -2,6 +2,8 @@ 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 @@ -10,6 +12,7 @@ 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 @@ -19,9 +22,6 @@ 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.model.config.IdentifierType.GOOGLE_GAID -import static org.prebid.server.functional.model.config.OperatingSystem.ANDROID -import static org.prebid.server.functional.model.request.auction.PublicCountryIp.USA_IP import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class CacheStorageSpec extends ModuleBaseSpec { @@ -56,10 +56,11 @@ class CacheStorageSpec extends ModuleBaseSpec { def "PBS should update error metrics when no cached requests present"() { given: "Default BidRequest with cache and device info" def randomIfa = PBSUtils.randomString - def bidRequest = getBidRequestForModuleCacheStorage(randomIfa) + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) and: "Account with optable targeting module" - def targetingConfig = OptableTargetingConfig.getDefault([(GOOGLE_GAID): randomIfa]) + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) accountDao.save(account) @@ -82,10 +83,11 @@ class CacheStorageSpec extends ModuleBaseSpec { 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 bidRequest = getBidRequestForModuleCacheStorage(randomIfa) + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) and: "Account with optable targeting module" - def targetingConfig = OptableTargetingConfig.getDefault([(GOOGLE_GAID): randomIfa]) + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) accountDao.save(account) @@ -110,10 +112,11 @@ class CacheStorageSpec extends ModuleBaseSpec { 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 bidRequest = getBidRequestForModuleCacheStorage(randomIfa) + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) and: "Account with optable targeting module" - def targetingConfig = OptableTargetingConfig.getDefault([(GOOGLE_GAID): randomIfa]) + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) accountDao.save(account) @@ -144,10 +147,11 @@ class CacheStorageSpec extends ModuleBaseSpec { and: "Default BidRequest with cache and device info" def randomIfa = PBSUtils.randomString - def bidRequest = getBidRequestForModuleCacheStorage(randomIfa) + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) and: "Account with optable targeting module" - def targetingConfig = OptableTargetingConfig.getDefault([(GOOGLE_GAID): randomIfa]) + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) accountDao.save(account) @@ -175,11 +179,14 @@ class CacheStorageSpec extends ModuleBaseSpec { assert metrics[METRIC_CREATIVE_WRITE_ERR] == writeErrorInitialValue } - private static BidRequest getBidRequestForModuleCacheStorage(String ifa) { + 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.getFPDGeo(), ip: USA_IP.v4, ifa: ifa, os: ANDROID.value) + it.device = new Device(geo: Geo.FPDGeo, + ip: PBSUtils.getRandomEnum(PublicCountryIp.class).v4, + ifa: ifa, + os: os) } }