From 857f9896fe9febfbea129a323048f8df61687456 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Fri, 4 Jul 2025 19:27:15 +0300 Subject: [PATCH 01/13] Tests: Add profiles --- .../model/config/AccountAuctionConfig.groovy | 1 + .../config/AccountProfilesConfigs.groovy | 17 + .../model/db/StoredProfileImp.groovy | 49 + .../model/db/StoredProfileRequest.groovy | 49 + .../functional/model/db/StoredRequest.groovy | 4 +- .../functional/model/db/StoredResponse.groovy | 4 +- ...y => BidRequestConfigTypeConverter.groovy} | 2 +- ... => BidResponseConfigTypeConverter.groovy} | 2 +- .../ProfileMergePrecedenceConvert.groovy | 18 + .../typeconverter/ProfileTypeConvert.groovy | 18 + .../model/request/auction/Device.groovy | 13 + .../model/request/auction/ImpExtPrebid.groovy | 3 +- .../model/request/auction/Prebid.groovy | 2 + .../model/request/profile/Profile.groovy | 22 + .../model/request/profile/ProfileImp.groovy | 23 + .../profile/ProfileMergePrecedence.groovy | 19 + .../request/profile/ProfileRequest.groovy | 23 + .../model/request/profile/ProfileType.groovy | 19 + .../model/response/auction/BidResponse.groovy | 4 +- .../HibernateRepositoryService.groovy | 12 + .../repository/dao/ProfileImpDao.groovy | 11 + .../repository/dao/ProfileRequestDao.groovy | 11 + .../service/PrebidServerService.groovy | 4 + .../container/PrebidServerContainer.groovy | 59 + .../functional/tests/ProfileSpec.groovy | 1051 +++++++++++++++++ .../server/functional/db_mysql_schema.sql | 9 + 26 files changed, 1441 insertions(+), 8 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/AccountProfilesConfigs.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy rename src/test/groovy/org/prebid/server/functional/model/db/typeconverter/{StoredRequestConfigTypeConverter.groovy => BidRequestConfigTypeConverter.groovy} (81%) rename src/test/groovy/org/prebid/server/functional/model/db/typeconverter/{StoredBidResponseConfigTypeConverter.groovy => BidResponseConfigTypeConverter.groovy} (81%) create mode 100644 src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileImp.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileRequest.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/repository/dao/ProfileImpDao.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/repository/dao/ProfileRequestDao.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy index bf49ce7c874..b332e3f1fc8 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy @@ -26,6 +26,7 @@ class AccountAuctionConfig { AccountCacheConfig cache AccountRankingConfig ranking AccountPriceFloorsConfig priceFloors + AccountProfilesConfigs profiles Targeting targeting PaaFormat paaformat @JsonProperty("preferredmediatype") diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountProfilesConfigs.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountProfilesConfigs.groovy new file mode 100644 index 00000000000..71852279fb8 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountProfilesConfigs.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AccountProfilesConfigs { + + Integer limit + Boolean failOnUnknown + + @JsonProperty("fail_on_unknown") + Boolean failOnUnknownSnakeCase +} diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy new file mode 100644 index 00000000000..26fc717ea3c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy @@ -0,0 +1,49 @@ +package org.prebid.server.functional.model.db + +import groovy.transform.ToString +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.prebid.server.functional.model.db.typeconverter.ImpConfigTypeConverter +import org.prebid.server.functional.model.db.typeconverter.ProfileMergePrecedenceConvert +import org.prebid.server.functional.model.db.typeconverter.ProfileTypeConvert +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.profile.ProfileImp +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence +import org.prebid.server.functional.model.request.profile.ProfileType + +import static jakarta.persistence.GenerationType.IDENTITY + +@Entity +@Table(name = "profiles_profile") +@ToString(includeNames = true) +class StoredProfileImp { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id") + Integer id + @Column(name = "profileName") + String profileRecordName + @Column(name = "mergePrecedence") + @Convert(converter = ProfileMergePrecedenceConvert) + ProfileMergePrecedence mergePrecedence + @Column(name = "profileType") + @Convert(converter = ProfileTypeConvert) + ProfileType type + @Column(name = "profileBody") + @Convert(converter = ImpConfigTypeConverter) + Imp impBody + + static StoredProfileImp getProfile(ProfileImp profile) { + new StoredProfileImp().tap { + it.profileRecordName = profile.recordName + it.mergePrecedence = profile.mergePrecedence + it.type = profile.type + it.impBody = profile.body + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy new file mode 100644 index 00000000000..2a187e0731a --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy @@ -0,0 +1,49 @@ +package org.prebid.server.functional.model.db + +import groovy.transform.ToString +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.prebid.server.functional.model.db.typeconverter.ProfileMergePrecedenceConvert +import org.prebid.server.functional.model.db.typeconverter.ProfileTypeConvert +import org.prebid.server.functional.model.db.typeconverter.BidRequestConfigTypeConverter +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence +import org.prebid.server.functional.model.request.profile.ProfileRequest +import org.prebid.server.functional.model.request.profile.ProfileType + +import static jakarta.persistence.GenerationType.IDENTITY + +@Entity +@Table(name = "profiles_profile") +@ToString(includeNames = true) +class StoredProfileRequest { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id") + Integer id + @Column(name = "profileName") + String profileRecordName + @Column(name = "mergePrecedence") + @Convert(converter = ProfileMergePrecedenceConvert) + ProfileMergePrecedence mergePrecedence + @Column(name = "profileType") + @Convert(converter = ProfileTypeConvert) + ProfileType type + @Column(name = "profileBody") + @Convert(converter = BidRequestConfigTypeConverter) + BidRequest requestBody + + static StoredProfileRequest getProfile(ProfileRequest profile) { + new StoredProfileRequest().tap { + it.profileRecordName = profile.recordName + it.mergePrecedence = profile.mergePrecedence + it.type = profile.type + it.requestBody = profile.body + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy index 69264aa04eb..5bf47b830fc 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy @@ -7,7 +7,7 @@ import jakarta.persistence.Entity import jakarta.persistence.GeneratedValue import jakarta.persistence.Id import jakarta.persistence.Table -import org.prebid.server.functional.model.db.typeconverter.StoredRequestConfigTypeConverter +import org.prebid.server.functional.model.db.typeconverter.BidRequestConfigTypeConverter import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest @@ -27,7 +27,7 @@ class StoredRequest { @Column(name = "reqId") String requestId @Column(name = "requestData") - @Convert(converter = StoredRequestConfigTypeConverter) + @Convert(converter = BidRequestConfigTypeConverter) BidRequest requestData static StoredRequest getStoredRequest(AmpRequest ampRequest, BidRequest storedRequest) { diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy index b57e793d22e..ebfc31f3c6d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy @@ -8,7 +8,7 @@ import jakarta.persistence.GeneratedValue import jakarta.persistence.Id import jakarta.persistence.Table import org.prebid.server.functional.model.db.typeconverter.StoredAuctionResponseConfigTypeConverter -import org.prebid.server.functional.model.db.typeconverter.StoredBidResponseConfigTypeConverter +import org.prebid.server.functional.model.db.typeconverter.BidResponseConfigTypeConverter import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.SeatBid @@ -29,6 +29,6 @@ class StoredResponse { @Convert(converter = StoredAuctionResponseConfigTypeConverter) SeatBid storedAuctionResponse @Column(name = "storedBidResponse") - @Convert(converter = StoredBidResponseConfigTypeConverter) + @Convert(converter = BidResponseConfigTypeConverter) BidResponse storedBidResponse } diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredRequestConfigTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidRequestConfigTypeConverter.groovy similarity index 81% rename from src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredRequestConfigTypeConverter.groovy rename to src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidRequestConfigTypeConverter.groovy index 3e968d39565..b3761640226 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredRequestConfigTypeConverter.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidRequestConfigTypeConverter.groovy @@ -4,7 +4,7 @@ import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.util.ObjectMapperWrapper -class StoredRequestConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { +class BidRequestConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { @Override String convertToDatabaseColumn(BidRequest bidRequest) { diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredBidResponseConfigTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidResponseConfigTypeConverter.groovy similarity index 81% rename from src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredBidResponseConfigTypeConverter.groovy rename to src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidResponseConfigTypeConverter.groovy index 43120fcad65..789d57e045e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredBidResponseConfigTypeConverter.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidResponseConfigTypeConverter.groovy @@ -4,7 +4,7 @@ import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.util.ObjectMapperWrapper -class StoredBidResponseConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { +class BidResponseConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { @Override String convertToDatabaseColumn(BidResponse bidResponse) { diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy new file mode 100644 index 00000000000..d81ae72ad87 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.db.typeconverter + +import jakarta.persistence.AttributeConverter +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence +import org.prebid.server.functional.util.ObjectMapperWrapper + +class ProfileMergePrecedenceConvert implements AttributeConverter, ObjectMapperWrapper { + + @Override + String convertToDatabaseColumn(ProfileMergePrecedence profileMergePrecedence) { + profileMergePrecedence?.value + } + + @Override + ProfileMergePrecedence convertToEntityAttribute(String value) { + value ? ProfileMergePrecedence.forValue(value) : null + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy new file mode 100644 index 00000000000..a25d22a3207 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.db.typeconverter + +import jakarta.persistence.AttributeConverter +import org.prebid.server.functional.model.request.profile.ProfileType +import org.prebid.server.functional.util.ObjectMapperWrapper + +class ProfileTypeConvert implements AttributeConverter, ObjectMapperWrapper { + + @Override + String convertToDatabaseColumn(ProfileType profileMergePrecedence) { + profileMergePrecedence?.value + } + + @Override + ProfileType convertToEntityAttribute(String value) { + value ? ProfileType.forValue(value) : null + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy index fbce486ac47..91a7e54dc37 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.model.request.auction import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils @ToString(includeNames = true, ignoreNulls = true) class Device { @@ -38,4 +39,16 @@ class Device { String macsha1 String macmd5 DeviceExt ext + + static Device getDefault() { + new Device().tap { + didsha1 = PBSUtils.randomString + didmd5 = PBSUtils.randomString + dpidsha1 = PBSUtils.randomString + ifa = PBSUtils.randomString + macsha1 = PBSUtils.randomString + macmd5 = PBSUtils.randomString + dpidmd5 = PBSUtils.randomString + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy index 6ff83b53b9c..993389b576f 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy @@ -21,7 +21,8 @@ class ImpExtPrebid { Map imp String adUnitCode PrebidOptions options - + @JsonProperty("profiles") + List profilesNames static ImpExtPrebid getDefaultImpExtPrebid() { new ImpExtPrebid().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy index 499dccea5a3..26f0800b332 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy @@ -45,6 +45,8 @@ class Prebid { PaaFormat paaFormat @JsonProperty("alternatebiddercodes") AlternateBidderCodes alternateBidderCodes + @JsonProperty("profiles") + List profilesNames static class Channel { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy new file mode 100644 index 00000000000..ddfd1d1cd00 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy @@ -0,0 +1,22 @@ +package org.prebid.server.functional.model.request.profile + +import com.fasterxml.jackson.annotation.JsonIgnore + +abstract class Profile { + + @JsonIgnore + String accountId + @JsonIgnore + String name + ProfileType type + ProfileMergePrecedence mergePrecedence + T body + + String getRecordName() { + "${accountId}_${name}" + } + + String getFileName() { + "${recordName}.json" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileImp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileImp.groovy new file mode 100644 index 00000000000..7562d6c2cd2 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileImp.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.request.profile + +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.util.PBSUtils + +import static ProfileMergePrecedence.PROFILE + +@ToString(includeNames = true, ignoreNulls = true) +class ProfileImp extends Profile { + + static getProfile(String accountId, + Imp imp = Imp.defaultImpression, + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { + + new ProfileImp(accountId: accountId, + name: name, + type: ProfileType.IMP, + mergePrecedence: mergePrecedence, + body: imp) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy new file mode 100644 index 00000000000..8d6b40203d2 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.request.profile + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum ProfileMergePrecedence { + + REQUEST, PROFILE, UNKNOWN + + @JsonValue + String getValue() { + name().toLowerCase() + } + + static ProfileMergePrecedence forValue(String value) { + values().find { it.value == value } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileRequest.groovy new file mode 100644 index 00000000000..be7132696e4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileRequest.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.request.profile + +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.util.PBSUtils + +import static ProfileMergePrecedence.PROFILE + +@ToString(includeNames = true, ignoreNulls = true) +class ProfileRequest extends Profile { + + static getProfile(String accountId, + BidRequest request = BidRequest.defaultBidRequest.tap { imp = null }, + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { + + new ProfileRequest(accountId: accountId, + name: name, + type: ProfileType.REQUEST, + mergePrecedence: mergePrecedence, + body: request) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy new file mode 100644 index 00000000000..044ec77c271 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.request.profile + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum ProfileType { + + REQUEST, IMP + + @JsonValue + String getValue() { + name().toLowerCase() + } + + static ProfileType forValue(String value) { + values().find { it.value == value } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy index fab76cfc309..353f89c454e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.response.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency @@ -18,7 +19,8 @@ class BidResponse implements ResponseModel { String bidid Currency cur String customdata - NoBidResponse nbr + @JsonProperty("nbr") + NoBidResponse noBidResponse BidResponseExt ext static BidResponse getDefaultBidResponse(BidRequest bidRequest, BidderName bidderName = GENERIC) { diff --git a/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy b/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy index d6ee8d65c10..cd1b9706f79 100644 --- a/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy +++ b/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy @@ -3,10 +3,14 @@ package org.prebid.server.functional.repository import org.hibernate.SessionFactory import org.hibernate.cfg.Configuration import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredProfileImp +import org.prebid.server.functional.model.db.StoredProfileRequest import org.prebid.server.functional.model.db.StoredImp import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.db.StoredResponse import org.prebid.server.functional.repository.dao.AccountDao +import org.prebid.server.functional.repository.dao.ProfileImpDao +import org.prebid.server.functional.repository.dao.ProfileRequestDao import org.prebid.server.functional.repository.dao.StoredImpDao import org.prebid.server.functional.repository.dao.StoredRequestDao import org.prebid.server.functional.repository.dao.StoredResponseDao @@ -23,6 +27,8 @@ class HibernateRepositoryService { StoredImpDao storedImpDao StoredRequestDao storedRequestDao StoredResponseDao storedResponseDao + ProfileImpDao profileImpDao + ProfileRequestDao profileRequestDao HibernateRepositoryService(JdbcDatabaseContainer container) { def jdbcUrl = container.jdbcUrl @@ -38,6 +44,8 @@ class HibernateRepositoryService { storedImpDao = new StoredImpDao(entityManagerUtil) storedRequestDao = new StoredRequestDao(entityManagerUtil) storedResponseDao = new StoredResponseDao(entityManagerUtil) + profileImpDao = new ProfileImpDao(entityManagerUtil) + profileRequestDao = new ProfileRequestDao(entityManagerUtil) } private static SessionFactory configureHibernate(String jdbcUrl, @@ -59,6 +67,8 @@ class HibernateRepositoryService { configuration.addAnnotatedClass(StoredImp) configuration.addAnnotatedClass(StoredRequest) configuration.addAnnotatedClass(StoredResponse) + configuration.addAnnotatedClass(StoredProfileImp) + configuration.addAnnotatedClass(StoredProfileRequest) SessionFactory sessionFactory = configuration.addProperties(properties).buildSessionFactory() sessionFactory @@ -69,5 +79,7 @@ class HibernateRepositoryService { storedImpDao.removeAll() storedRequestDao.removeAll() storedResponseDao.removeAll() + profileImpDao.removeAll() + profileRequestDao.removeAll() } } diff --git a/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileImpDao.groovy b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileImpDao.groovy new file mode 100644 index 00000000000..00531ff9a7b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileImpDao.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.repository.dao + +import org.prebid.server.functional.model.db.StoredProfileImp +import org.prebid.server.functional.repository.EntityManagerUtil + +class ProfileImpDao extends EntityDao { + + ProfileImpDao(EntityManagerUtil entityManagerUtil) { + super(entityManagerUtil, StoredProfileImp) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileRequestDao.groovy b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileRequestDao.groovy new file mode 100644 index 00000000000..455c64a3fd8 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileRequestDao.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.repository.dao + +import org.prebid.server.functional.model.db.StoredProfileRequest +import org.prebid.server.functional.repository.EntityManagerUtil + +class ProfileRequestDao extends EntityDao { + + ProfileRequestDao(EntityManagerUtil entityManagerUtil) { + super(entityManagerUtil, StoredProfileRequest) + } +} 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 b9c173baa54..a6c31644caa 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -431,6 +431,10 @@ class PrebidServerService implements ObjectMapperWrapper { }) } + Boolean copyToContainer(String content, String containerPath) { + pbsContainer.copyToContainer(content, containerPath) + } + Boolean isFileExist(String path) { pbsContainer.execInContainer("test", "-f", path).getExitCode() == 0 } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy index b3f938a7ca0..32fa26b6115 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy @@ -1,11 +1,23 @@ package org.prebid.server.functional.testcontainers.container +import com.github.dockerjava.api.DockerClient +import com.github.dockerjava.api.command.InspectContainerResponse +import org.apache.commons.compress.archivers.tar.TarArchiveEntry +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream import org.prebid.server.functional.testcontainers.Dependencies import org.prebid.server.functional.testcontainers.PbsConfig import org.prebid.server.functional.util.SystemProperties +import org.testcontainers.DockerClientFactory import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.utility.MountableFile +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +import static java.nio.charset.StandardCharsets.UTF_8 import static org.prebid.server.functional.testcontainers.PbsConfig.DEFAULT_ENV class PrebidServerContainer extends GenericContainer { @@ -95,6 +107,53 @@ class PrebidServerContainer extends GenericContainer { .replace("]", "_") } + PrebidServerContainer copyToContainer(String content, String containerPath) { + String parentDir = getParentDirectory(containerPath) + createDirectoryInContainer(parentDir) + + byte[] archive = createTarArchive(content, getFileName(containerPath)) + copyArchiveToContainer(archive, parentDir) + + return self() + } + + private static String getParentDirectory(String containerPath) { + Path path = Paths.get(containerPath) + return path.getParent().toString() + } + + private static String getFileName(String containerPath) { + return Paths.get(containerPath).getFileName().toString() + } + + private void createDirectoryInContainer(String directory) { + execInContainer("mkdir", "-p", directory) + } + + private static byte[] createTarArchive(String content, String fileName) { + byte[] data = content.getBytes(UTF_8) + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + TarArchiveOutputStream tar = new TarArchiveOutputStream(outputStream) + tar.longFileMode = TarArchiveOutputStream.LONGFILE_POSIX + + TarArchiveEntry entry = new TarArchiveEntry(fileName) + entry.size = data.length + tar.putArchiveEntry(entry) + tar.write(data) + tar.closeArchiveEntry() + tar.finish() + + return outputStream.toByteArray() + } + + private void copyArchiveToContainer(byte[] archive, String destination) { + DockerClient client = DockerClientFactory.instance().client() + client.copyArchiveToContainerCmd(getContainerId()) + .withTarInputStream(new ByteArrayInputStream(archive)) + .withRemotePath(destination) + .exec() + } + // This is a workaround for cases when container is killed mid-test due to OOM void refresh() { if (!running) { diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy new file mode 100644 index 00000000000..8bb59f62ece --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -0,0 +1,1051 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountProfilesConfigs +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredProfileImp +import org.prebid.server.functional.model.db.StoredProfileRequest +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.request.auction.App +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.ImpExt +import org.prebid.server.functional.model.request.auction.ImpExtPrebid +import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.request.auction.StoredBidResponse +import org.prebid.server.functional.model.request.profile.Profile +import org.prebid.server.functional.model.request.profile.ProfileImp +import org.prebid.server.functional.model.request.profile.ProfileRequest +import org.prebid.server.functional.model.request.profile.ProfileType +import org.prebid.server.functional.model.request.auction.Site +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.ErrorType +import org.prebid.server.functional.model.response.auction.NoBidResponse +import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.repository.dao.ProfileImpDao +import org.prebid.server.functional.repository.dao.ProfileRequestDao +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.PbsServiceFactory +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.PROFILE +import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.REQUEST +import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.UNKNOWN +import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class ProfileSpec extends BaseSpec { + + private static final String PROFILES_PATH = '/app/prebid-server/profiles' + private static final Integer LIMIT_HOST_PROFILE = 2 + + private static final Map PROFILES_CONFIG = [ + "adapters.openx.enabled" : "true", + "adapters.openx.endpoint" : "$networkServiceContainer.rootUri/openx/auction".toString(), + "auction.profiles.limit" : LIMIT_HOST_PROFILE.toString(), + "auction.profiles.fail-on-unknown": "false", + "settings.filesystem.profiles-dir": PROFILES_PATH, + "settings.database.profiles-query": 'SELECT profileName, reqId, mergePrecedence, profileType, profileBody FROM profiles_profile WHERE reqId IN (%REQUEST_ID_LIST%)'] + ProfileImpDao profileImpDao = repository.profileImpDao + ProfileRequestDao profileRequestDao = repository.profileRequestDao + private static PrebidServerService pbsWithStoredProfiles = PbsServiceFactory.getService(PROFILES_CONFIG) + private static final String REJECT_ERROR_MESSAGE = 'replace' + private static final String MISSING_ERROR_MESSAGE = 'replace' + private static final String REJECT_PROFILE_METRIC = 'profile.rejected' + private static final String REJECT_ACCOUNT_PROFILE_METRIC = "account.%s.profile.rejected" + private static final String MISSING_PROFILE_METRIC = 'profile.rejected' + private static final String MISSING_ACCOUNT_PROFILE_METRIC = "account.%s.profile.rejected" + + def cleanupSpec() { + PbsServiceFactory.removeContainer(PROFILES_CONFIG) + } + + def "PBS should use profile for request when it exist in database"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = ProfileRequest.getProfile(accountId) + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == requestProfile.body.site + it.device == requestProfile.body.device + } + } + + def "PBS should use imp profile for request when it exist in database"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def impProfile = ProfileImp.getProfile(accountId) + def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { + imp.first.banner = null + } as BidRequest + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + assert bidder.getBidderRequest(bidRequest.id).imp.first == impProfile.body + } + + def "PBS should use profile for request when it exist in filesystem"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def profile = ProfileRequest.getProfile(accountId) + def bidRequest = getRequestWithProfiles(accountId, [profile]) + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Profile fine in PBS container" + pbsWithStoredProfiles.copyToContainer(encode(profile), "$PROFILES_PATH/${profile.fileName}") + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == profile.body.site + it.device == profile.body.device + } + } + + def "PBS should use imp profile for request when it exist in filesystem"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def profile = ProfileImp.getProfile(accountId) + def bidRequest = getRequestWithProfiles(accountId, [profile]).tap { + imp.first.banner = null + } as BidRequest + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Profile fine in PBS container" + pbsWithStoredProfiles.copyToContainer(encode(profile), "$PROFILES_PATH/${profile.fileName}") + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + assert bidder.getBidderRequest(bidRequest.id).imp.first == profile.body + } + + def "PBS should emit error for request when same profile exist in filesystem and database"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def profile = ProfileRequest.getProfile(accountId) + def bidRequest = getRequestWithProfiles(accountId, [profile]) + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Profile fine in PBS container" + pbsWithStoredProfiles.copyToContainer(encode(profile), "$PROFILES_PATH/${profile.fileName}") + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(profile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [REJECT_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS log should contain error" + assert pbsWithStoredProfiles.isContainLogsByValue(REJECT_ERROR_MESSAGE) + + and: "Reject metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[REJECT_PROFILE_METRIC] == 1 + assert metrics[REJECT_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from original request" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == bidRequest.site + it.device == bidRequest.device + } + } + + def "PBS should prioritise original request data over profile when merge strategy #mergeStrategy"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = ProfileRequest.getProfile(accountId).tap { + mergePrecedence = mergeStrategy + } + BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profilesNames = [requestProfile.name] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == requestProfile.body.site + it.device == requestProfile.body.device + } + + where: + mergeStrategy << [null, UNKNOWN, REQUEST] + } + + def "PBS should prioritise original imp data over profile when merge strategy #mergeStrategy"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def impProfile = ProfileImp.getProfile(accountId).tap { + mergePrecedence = mergeStrategy + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + assert bidder.getBidderRequest(bidRequest.id).imp == [impProfile.body] + + where: + mergeStrategy << [null, UNKNOWN, REQUEST] + } + + def "PBS should marge earliest-specified profile profile with request merge precedence when there marge conflict"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getRequestWithProfiles(accountId, [firstProfile, secondProfile]) + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profiles" + def mergedRequest = [firstProfile, secondProfile].find { it.mergePrecedence == REQUEST }.body + assert bidder.getBidderRequest(bidRequest.id).site == mergedRequest.site + + where: + firstProfile | secondProfile + ProfileRequest.getProfile(accountId).tap { mergePrecedence = REQUEST } | ProfileRequest.getProfile(accountId) + ProfileRequest.getProfile(accountId) | ProfileRequest.getProfile(accountId).tap { mergePrecedence = REQUEST } + ProfileRequest.getProfile(accountId).tap { mergePrecedence = REQUEST } | ProfileRequest.getProfile(accountId).tap { mergePrecedence = REQUEST } + } + + def "PBS should marge latest-specified profile profile with request merge precedence when there marge conflict"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def firstRequestProfile = ProfileRequest.getProfile(accountId).tap { + body.device = Device.default + body.site = Site.defaultSite + } + def secondRequestProfile = ProfileRequest.getProfile(accountId).tap { + body.device = Device.default + body.site = Site.defaultSite + } + def bidRequest = getRequestWithProfiles(accountId, [firstRequestProfile, secondRequestProfile]) + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profiles" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == secondRequestProfile.body.site + it.device == secondRequestProfile.body.device + } + } + + def "PBS should prioritise profile for request when request is overloaded by profiles"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def profileSite = Site.defaultSite + def profileDevice = Device.default + def firstRequestProfile = ProfileRequest.getProfile(accountId).tap { + body.site = profileSite + } + def secondRequestProfile = ProfileRequest.getProfile(accountId).tap { + body.device = profileDevice + } + def impProfile = ProfileImp.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) + def bidRequest = getRequestWithProfiles(accountId, [impProfile, firstRequestProfile, secondRequestProfile]) + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS log should contain error" + assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_PROFILE_METRIC] == 1 + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profiles" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site == profileSite + it.device == profileDevice + } + + and: "Bidder imp should contain original data from request" + assert bidderRequest.imp == bidRequest.imp + } + + def "PBS should be able override profile limit by account config and use remaining limits for each imp separately"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def profileSite = Site.defaultSite + def profileDevice = Device.default + def firstRequestProfile = ProfileRequest.getProfile(accountId).tap { + body.site = profileSite + } + def secondRequestProfile = ProfileRequest.getProfile(accountId).tap { + body.device = profileDevice + } + def firstImpProfile = ProfileImp.getProfile(accountId) + def secondImpProfile = ProfileImp.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) + def thirdImpProfile = ProfileImp.getProfile(accountId, Imp.getDefaultImpression(NATIVE)) + def bidRequest = getRequestWithProfiles(accountId, [firstImpProfile, secondImpProfile, firstRequestProfile, secondRequestProfile]).tap { + imp << new Imp(ext: new ImpExt(prebid: new ImpExtPrebid(profilesNames: [secondImpProfile, thirdImpProfile].name))) + } as BidRequest + + and: "Default account" + def profilesConfigs = new AccountProfilesConfigs(limit: LIMIT_HOST_PROFILE + 2) + def accountAuctionConfig = new AccountAuctionConfig(profiles: profilesConfigs) + def accountConfig = new AccountConfig(auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE, config: accountConfig) + accountDao.save(account) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) + profileImpDao.save(StoredProfileImp.getProfile(firstImpProfile)) + profileImpDao.save(StoredProfileImp.getProfile(secondImpProfile)) + profileImpDao.save(StoredProfileImp.getProfile(thirdImpProfile)) + profileImpDao.save(StoredProfileImp.getProfile(fourthImpProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "PBS log should contain error" + assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) + + and: "Missing metric shouldn't increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert !metrics[MISSING_PROFILE_METRIC] + assert !metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] + + and: "Bidder request should contain data from profiles" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site == profileSite + it.device == profileDevice + } + + and: "Bidder imps should contain data from profiles" + assert bidderRequest.imp.first.banner == firstImpProfile.body.banner + assert bidderRequest.imp.first.video == secondImpProfile.body.video + assert bidderRequest.imp.last.video == secondImpProfile.body.video + assert bidderRequest.imp.first.video == thirdImpProfile.body.video + assert bidderRequest.imp.banner == [firstImpProfile.body.banner] + } + + def "PBS should include invalid or missing profiles into limit count"() { + given: "Default bidRequest with request profiles" + def accountId = PBSUtils.randomNumber as String + def invalidProfileRequest = ProfileRequest.getProfile(accountId).tap { + body = null + } + def impProfile = ProfileImp.getProfile(accountId) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp.first.ext.prebid.profilesNames = [impProfile.name] + it.ext.prebid.profilesNames = [invalidProfileRequest.name, PBSUtils.randomString] + setAccountId(accountId) + } + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(invalidProfileRequest)) + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS log should contain error" + assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) + + and: "Missing metric shouldn't increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert !metrics[MISSING_PROFILE_METRIC] + assert !metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] + + and: "Bidder request should contain data from profiles" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site == bidRequest.site + it.device == bidRequest.device + } + + and: "Bidder imp should contain original data from request" + assert bidderRequest.imp == bidRequest.imp + } + + def "PBS should include data from storedBidResponses when it specified in profiles"() { + given: "Default basic BidRequest with stored response" + def accountId = PBSUtils.randomNumber as String + def storedResponseId = PBSUtils.randomNumber + def impProfile = ProfileImp.getProfile(accountId).tap { + it.body.ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Stored bid response in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should contain information from stored bid response" + assert response.id == bidRequest.id + assert response.seatbid[0]?.seat == storedBidResponse.seatbid[0].seat + assert response.seatbid[0]?.bid?.size() == storedBidResponse.seatbid[0].bid.size() + assert response.seatbid[0]?.bid[0]?.impid == storedBidResponse.seatbid[0].bid[0].impid + assert response.seatbid[0]?.bid[0]?.price == storedBidResponse.seatbid[0].bid[0].price + assert response.seatbid[0]?.bid[0]?.id == storedBidResponse.seatbid[0].bid[0].id + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should include data from storedAuctionResponse when it specified in profiles"() { + given: "Default basic BidRequest with stored response" + def accountId = PBSUtils.randomNumber as String + def storedAuctionId = PBSUtils.randomNumber + def impProfile = ProfileImp.getProfile(accountId).tap { + it.body.ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedAuctionId) + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Stored response in DB" + def storedAuctionResponse = SeatBid.getStoredResponse(BidRequest.defaultBidRequest) + def storedResponse = new StoredResponse(responseId: storedAuctionId, + storedAuctionResponse: storedAuctionResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should contain information from stored bid response" + assert response.id == bidRequest.id + assert response.seatbid[0]?.seat == storedAuctionResponse.seat + assert response.seatbid[0]?.bid?.size() == storedAuctionResponse.bid.size() + assert response.seatbid[0]?.bid[0]?.impid == storedAuctionResponse.bid[0].impid + assert response.seatbid[0]?.bid[0]?.price == storedAuctionResponse.bid[0].price + assert response.seatbid[0]?.bid[0]?.id == storedAuctionResponse.bid[0].id + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should fail auction when fail-on-unknown-profile enabled and profile is missing"() { + given: "PBS with profiles.fail-on-unknown config" + def failOnUnknownProfilesConfig = new HashMap<>(PROFILES_CONFIG) + failOnUnknownProfilesConfig["auction.profiles.fail-on-unknown"] = "true" + def prebidServerService = pbsServiceFactory.getService(failOnUnknownProfilesConfig) + + and: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profilesNames = [PBSUtils.randomString] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(prebidServerService) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS should fail auction" + assert !response.seatbid.bid + assert response.noBidResponse == NoBidResponse.INVALID_REQUEST + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(failOnUnknownProfilesConfig) + } + + def "PBS should fail auction when fail-on-unknown-profile default and profile is missing"() { + given: "PBS without profiles.fail-on-unknown config" + def failOnUnknownProfilesConfig = new HashMap<>(PROFILES_CONFIG) + failOnUnknownProfilesConfig.remove("auction.profiles.fail-on-unknown") + def prebidServerService = pbsServiceFactory.getService(failOnUnknownProfilesConfig) + + and: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profilesNames = [PBSUtils.randomString] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(prebidServerService) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS should fail auction" + assert !response.seatbid.bid + assert response.noBidResponse == NoBidResponse.INVALID_REQUEST + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(failOnUnknownProfilesConfig) + } + + def "PBS should prioritise fail-on-unknown-profile from account over host config"() { + given: "PBS with profiles.fail-on-unknown config" + def failOnUnknownProfilesConfig = new HashMap<>(PROFILES_CONFIG) + failOnUnknownProfilesConfig["auction.profiles.fail-on-unknown"] = "true" + def prebidServerService = pbsServiceFactory.getService(failOnUnknownProfilesConfig) + + and: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profilesNames = [PBSUtils.randomString] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + and: "Default account" + def accountAuctionConfig = new AccountAuctionConfig(profiles: profilesConfigs) + def accountConfig = new AccountConfig(auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(prebidServerService) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS log should contain error" + assert prebidServerService.isContainLogsByValue(MISSING_ERROR_MESSAGE) + + and: "Missing metric should increments" + def metrics = prebidServerService.sendCollectedMetricsRequest() + assert metrics[MISSING_PROFILE_METRIC] == 1 + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from original request" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == bidRequest.site + it.device == bidRequest.device + } + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(failOnUnknownProfilesConfig) + + where: + profilesConfigs << [ + new AccountProfilesConfigs(failOnUnknown: true), + new AccountProfilesConfigs(failOnUnknownSnakeCase: true), + ] + } + + def "PBS should ignore inner request profiles when stored request profile contain link for another profile"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def innerRequestProfile = ProfileRequest.getProfile(accountId).tap { + it.body.app = App.defaultApp + } + + def requestProfile = ProfileRequest.getProfile(accountId).tap { + it.body.ext.prebid.profilesNames = [innerRequestProfile.name] + } + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(innerRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site == requestProfile.body.site + it.device == requestProfile.body.device + } + + and: "Bidder request shouldn't contain data from inner profile" + assert !bidderRequest.app + } + + def "PBS should ignore inner imp profiles when stored imp profile contain link for another profile"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def innerImpProfile = ProfileImp.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) + def impProfile = ProfileImp.getProfile(accountId).tap { + body.ext.prebid.profilesNames = [innerImpProfile.name] + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { + imp.first.banner = null + } as BidRequest + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profiles in database" + profileImpDao.save(StoredProfileImp.getProfile(innerImpProfile)) + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert bidderImp.banner == impProfile.body.banner + + and: "Bidder request imp shouldn't contain data from inner profile" + assert bidderImp.video == impProfile.body.video + } + + def "PBS shouldn't validate profiles and imp before margining"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def impProfile = ProfileImp.getProfile(accountId).tap { + body.banner.weight = null + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { + imp.first.banner.height = null + } as BidRequest + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + assert bidder.getBidderRequest(bidRequest.id).imp.first == impProfile.body + } + + def "PBS should ignore imp data from request profile when imp for profile not null"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = ProfileRequest.getProfile(accountId, + BidRequest.defaultBidRequest, + PBSUtils.randomString, + mergePrecedence) + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + assert bidder.getBidderRequest(bidRequest.id).imp == bidRequest.imp + + where: + mergePrecedence << [REQUEST, PROFILE] + } + + def "PBS should emit error and metrics when request profile called from imp level"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = ProfileRequest.getProfile(accountId) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profilesNames = [requestProfile.recordName] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [REJECT_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS log should contain error" + assert pbsWithStoredProfiles.isContainLogsByValue(REJECT_ERROR_MESSAGE) + + and: "Reject metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[REJECT_PROFILE_METRIC] == 1 + assert metrics[REJECT_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from original request" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == bidRequest.site + it.device == bidRequest.device + } + } + + def "PBS should emit error and metrics when imp profile called from request level"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = ProfileImp.getProfile(accountId) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profilesNames = [requestProfile.recordName] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(requestProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS log should contain error" + assert pbsWithStoredProfiles.isContainLogsByValue(ERROR_MESSAGE) + + and: "Reject metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[REJECT_PROFILE_METRIC] == 1 + assert metrics[REJECT_ACCOUNT_PROFILE_METRIC] == 1 + + and: "Bidder request should contain data from original request" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == bidRequest.site + it.device == bidRequest.device + } + } + + def "PBS should emit error and metrics when request profile missing"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profilesNames = [PBSUtils.randomString] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS log should contain error" + assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_PROFILE_METRIC] == 1 + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from original request" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == bidRequest.site + it.device == bidRequest.device + } + } + + def "PBS should emit error and metrics when imp profile missing"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profilesNames = [PBSUtils.randomString] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS log should contain error" + assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_PROFILE_METRIC] == 1 + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from original request" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == bidRequest.site + it.device == bidRequest.device + } + } + + private static BidRequest getRequestWithProfiles(String accountId, List profiles) { + BidRequest.getDefaultBidRequest().tap { + if (profiles.type.contains(ProfileType.IMP)) { + imp = [new Imp(ext: new ImpExt(prebid: new ImpExtPrebid(profilesNames: profiles.findAll { it.type == ProfileType.IMP }*.name)))] + } + imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.name + it.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.REQUEST }*.name + it.site = new Site() + it.device = null + setAccountId(accountId) + } + } +} diff --git a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql index 9d1732c97b0..0a17a030905 100644 --- a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql +++ b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql @@ -42,5 +42,14 @@ CREATE TABLE stored_responses storedBidResponse varchar(1024) ); +CREATE TABLE profiles_profile +( + id SERIAL PRIMARY KEY, + profileName varchar(64) UNIQUE, + mergePrecedence enum ('request','profile'), + profileType enum('request', 'imp'), + profileBody json +); + -- set session wait timeout to 1 minute SET SESSION wait_timeout = 60000; From 53f60b44568a9b1e309784a3f4e5c0c9a4cb3e90 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Mon, 14 Jul 2025 15:23:29 +0300 Subject: [PATCH 02/13] Tests: Add additional cases --- .../functional/tests/ProfileSpec.groovy | 81 ++++++++++++++++++- .../server/functional/util/PBSUtils.groovy | 8 ++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index 8bb59f62ece..c1bf4a902aa 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -37,7 +37,6 @@ import static org.prebid.server.functional.model.request.profile.ProfileMergePre import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.UNKNOWN import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO -import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class ProfileSpec extends BaseSpec { @@ -46,8 +45,6 @@ class ProfileSpec extends BaseSpec { private static final Map PROFILES_CONFIG = [ "adapters.openx.enabled" : "true", - "adapters.openx.endpoint" : "$networkServiceContainer.rootUri/openx/auction".toString(), - "auction.profiles.limit" : LIMIT_HOST_PROFILE.toString(), "auction.profiles.fail-on-unknown": "false", "settings.filesystem.profiles-dir": PROFILES_PATH, "settings.database.profiles-query": 'SELECT profileName, reqId, mergePrecedence, profileType, profileBody FROM profiles_profile WHERE reqId IN (%REQUEST_ID_LIST%)'] @@ -829,6 +826,35 @@ class ProfileSpec extends BaseSpec { assert bidder.getBidderRequest(bidRequest.id).imp.first == impProfile.body } + def "PBS shouldn't emit error or warnings when bidRequest contains multiple imps with same profile"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def impProfile = ProfileImp.getProfile(accountId, Imp.defaultImpression) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + addImp(Imp.getDefaultImpression()) + setAccountId(accountId) + imp.each { it.ext.prebid.profilesNames = [impProfile.name] } + } as BidRequest + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imps should contain data from profile" + assert bidder.getBidderRequest(bidRequest.id).imp.first == impProfile.body + assert bidder.getBidderRequest(bidRequest.id).imp.last == impProfile.body + } + def "PBS should ignore imp data from request profile when imp for profile not null"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String @@ -859,6 +885,55 @@ class ProfileSpec extends BaseSpec { mergePrecedence << [REQUEST, PROFILE] } + def "PBS should emit error and metrics when profile name is invalid"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def impProfile = ProfileImp.getProfile(accountId, Imp.defaultImpression, invalidProfileName) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profilesNames = [PBSUtils.randomString] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS log should contain error" + assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_PROFILE_METRIC] == 1 + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from original request" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == bidRequest.site + it.device == bidRequest.device + } + + where: + invalidProfileName << [PBSUtils.randomSpecialChars, PBSUtils.randomStringWithSpecials] + } + def "PBS should emit error and metrics when request profile called from imp level"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String diff --git a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy index e1e7750ea05..de518ea4209 100644 --- a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy @@ -50,6 +50,14 @@ class PBSUtils implements ObjectMapperWrapper { RandomStringUtils.randomAlphanumeric(stringLength) } + static String getRandomSpecialChars(int stringLength = 20) { + RandomStringUtils.random(stringLength, "!@#\$%^&*()-_=+[]{}|;:'\",.<>/?") + } + + static String getRandomStringWithSpecials(int stringLength = 20) { + RandomStringUtils.randomAscii(stringLength) + } + static Boolean getRandomBoolean() { new Random().nextBoolean() } From 800b82a7584615f917f530989c7036e5d44741d4 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Sun, 31 Aug 2025 21:15:16 +0300 Subject: [PATCH 03/13] Update functional test --- .../model/db/StoredProfileImp.groovy | 25 +- .../model/db/StoredProfileRequest.groovy | 25 +- .../model/filesystem/FilesystemAccount.groovy | 32 + .../filesystem/FilesystemAccounts.groovy | 13 + .../model/request/auction/Banner.groovy | 2 + .../request/auction/BidRequestExt.groovy | 2 + .../model/request/auction/Format.groovy | 8 + .../model/request/auction/Site.groovy | 2 + .../model/request/auction/SiteExtData.groovy | 2 + .../{ProfileImp.groovy => ImpProfile.groovy} | 4 +- .../model/request/profile/Profile.groovy | 4 +- .../request/profile/ProfileRequest.groovy | 23 - .../request/profile/RequestProfile.groovy | 36 + .../testcontainers/PbsConfig.groovy | 3 +- .../container/PrebidServerContainer.groovy | 9 + .../functional/tests/ProfileSpec.groovy | 867 ++++++++++++------ .../util/ObjectMapperWrapper.groovy | 19 + .../server/functional/db_mysql_schema.sql | 12 +- 18 files changed, 738 insertions(+), 350 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccount.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccounts.groovy rename src/test/groovy/org/prebid/server/functional/model/request/profile/{ProfileImp.groovy => ImpProfile.groovy} (88%) delete mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileRequest.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy index 26fc717ea3c..bbc91786797 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy @@ -4,43 +4,40 @@ import groovy.transform.ToString import jakarta.persistence.Column import jakarta.persistence.Convert import jakarta.persistence.Entity -import jakarta.persistence.GeneratedValue import jakarta.persistence.Id import jakarta.persistence.Table import org.prebid.server.functional.model.db.typeconverter.ImpConfigTypeConverter import org.prebid.server.functional.model.db.typeconverter.ProfileMergePrecedenceConvert import org.prebid.server.functional.model.db.typeconverter.ProfileTypeConvert import org.prebid.server.functional.model.request.auction.Imp -import org.prebid.server.functional.model.request.profile.ProfileImp +import org.prebid.server.functional.model.request.profile.ImpProfile import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence import org.prebid.server.functional.model.request.profile.ProfileType -import static jakarta.persistence.GenerationType.IDENTITY - @Entity -@Table(name = "profiles_profile") +@Table(name = "profiles") @ToString(includeNames = true) class StoredProfileImp { @Id - @GeneratedValue(strategy = IDENTITY) - @Column(name = "id") - Integer id - @Column(name = "profileName") - String profileRecordName + @Column(name = "profileId") + String profileName + @Column(name = "accountId") + String accountId @Column(name = "mergePrecedence") @Convert(converter = ProfileMergePrecedenceConvert) ProfileMergePrecedence mergePrecedence - @Column(name = "profileType") + @Column(name = "type") @Convert(converter = ProfileTypeConvert) ProfileType type - @Column(name = "profileBody") + @Column(name = "profile") @Convert(converter = ImpConfigTypeConverter) Imp impBody - static StoredProfileImp getProfile(ProfileImp profile) { + static StoredProfileImp getProfile(ImpProfile profile) { new StoredProfileImp().tap { - it.profileRecordName = profile.recordName + it.profileName = profile.name + it.accountId = profile.accountId it.mergePrecedence = profile.mergePrecedence it.type = profile.type it.impBody = profile.body diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy index 2a187e0731a..1bd3681b159 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy @@ -4,7 +4,6 @@ import groovy.transform.ToString import jakarta.persistence.Column import jakarta.persistence.Convert import jakarta.persistence.Entity -import jakarta.persistence.GeneratedValue import jakarta.persistence.Id import jakarta.persistence.Table import org.prebid.server.functional.model.db.typeconverter.ProfileMergePrecedenceConvert @@ -12,35 +11,33 @@ import org.prebid.server.functional.model.db.typeconverter.ProfileTypeConvert import org.prebid.server.functional.model.db.typeconverter.BidRequestConfigTypeConverter import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence -import org.prebid.server.functional.model.request.profile.ProfileRequest +import org.prebid.server.functional.model.request.profile.RequestProfile import org.prebid.server.functional.model.request.profile.ProfileType -import static jakarta.persistence.GenerationType.IDENTITY - @Entity -@Table(name = "profiles_profile") +@Table(name = "profiles") @ToString(includeNames = true) class StoredProfileRequest { @Id - @GeneratedValue(strategy = IDENTITY) - @Column(name = "id") - Integer id - @Column(name = "profileName") - String profileRecordName + @Column(name = "profileId") + String profileName + @Column(name = "accountId") + String accountId @Column(name = "mergePrecedence") @Convert(converter = ProfileMergePrecedenceConvert) ProfileMergePrecedence mergePrecedence - @Column(name = "profileType") + @Column(name = "type") @Convert(converter = ProfileTypeConvert) ProfileType type - @Column(name = "profileBody") + @Column(name = "profile") @Convert(converter = BidRequestConfigTypeConverter) BidRequest requestBody - static StoredProfileRequest getProfile(ProfileRequest profile) { + static StoredProfileRequest getProfile(RequestProfile profile) { new StoredProfileRequest().tap { - it.profileRecordName = profile.recordName + it.profileName = profile.name + it.accountId = profile.accountId it.mergePrecedence = profile.mergePrecedence it.type = profile.type it.requestBody = profile.body diff --git a/src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccount.groovy b/src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccount.groovy new file mode 100644 index 00000000000..529e4a3472d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccount.groovy @@ -0,0 +1,32 @@ +package org.prebid.server.functional.model.filesystem + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.AccountStatus +import org.prebid.server.functional.model.config.AccountConfig + + +import java.sql.Timestamp + + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) +class FilesystemAccount { + + Integer id + String priceGranularity + Integer bannerCacheTtl + Integer videoCacheTtl + Boolean eventsEnabled + String tcfConfig + Integer truncateTargetAttr + String defaultIntegration + String analyticsConfig + String bidValidations + AccountStatus status + AccountConfig config + Integer updatedBy + String updatedByUser + Timestamp updated +} diff --git a/src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccounts.groovy b/src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccounts.groovy new file mode 100644 index 00000000000..317b922c098 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccounts.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.filesystem + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.config.AccountConfig + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) +class FilesystemAccounts { + + List accounts +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy index b4d6c23f4f5..32bd83365f9 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class Banner { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy index c253291dbf8..3c39de5781e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.bidder.AppNexus +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class BidRequestExt { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy index f6d1798ca57..326ba14ed5d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy @@ -3,6 +3,7 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils @EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) @@ -25,4 +26,11 @@ class Format { height = 250 } } + + static Format getRandomFormat() { + new Format().tap { + weight = PBSUtils.randomNumber + height = PBSUtils.randomNumber + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Site.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Site.groovy index b74d83ff8fb..c8dfcbdbe79 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Site.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Site.groovy @@ -2,9 +2,11 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class Site { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy index 87b5e19c21c..7e8ecd556fa 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class SiteExtData { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileImp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy similarity index 88% rename from src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileImp.groovy rename to src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy index 7562d6c2cd2..ae88fdb01b7 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileImp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy @@ -7,14 +7,14 @@ import org.prebid.server.functional.util.PBSUtils import static ProfileMergePrecedence.PROFILE @ToString(includeNames = true, ignoreNulls = true) -class ProfileImp extends Profile { +class ImpProfile extends Profile { static getProfile(String accountId, Imp imp = Imp.defaultImpression, String name = PBSUtils.randomString, ProfileMergePrecedence mergePrecedence = PROFILE) { - new ProfileImp(accountId: accountId, + new ImpProfile(accountId: accountId, name: name, type: ProfileType.IMP, mergePrecedence: mergePrecedence, diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy index ddfd1d1cd00..82e7ad103c7 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.model.request.profile import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty abstract class Profile { @@ -9,11 +10,12 @@ abstract class Profile { @JsonIgnore String name ProfileType type + @JsonProperty("mergeprecedence") ProfileMergePrecedence mergePrecedence T body String getRecordName() { - "${accountId}_${name}" + "${accountId}-${name}" } String getFileName() { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileRequest.groovy deleted file mode 100644 index be7132696e4..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileRequest.groovy +++ /dev/null @@ -1,23 +0,0 @@ -package org.prebid.server.functional.model.request.profile - -import groovy.transform.ToString -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.util.PBSUtils - -import static ProfileMergePrecedence.PROFILE - -@ToString(includeNames = true, ignoreNulls = true) -class ProfileRequest extends Profile { - - static getProfile(String accountId, - BidRequest request = BidRequest.defaultBidRequest.tap { imp = null }, - String name = PBSUtils.randomString, - ProfileMergePrecedence mergePrecedence = PROFILE) { - - new ProfileRequest(accountId: accountId, - name: name, - type: ProfileType.REQUEST, - mergePrecedence: mergePrecedence, - body: request) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy new file mode 100644 index 00000000000..74fcc404e0d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy @@ -0,0 +1,36 @@ +package org.prebid.server.functional.model.request.profile + +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Site +import org.prebid.server.functional.util.PBSUtils + +import static ProfileMergePrecedence.PROFILE + +@ToString(includeNames = true, ignoreNulls = true) +class RequestProfile extends Profile { + + static getProfile(String accountId = PBSUtils.randomString, + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { + BidRequest request = BidRequest.defaultBidRequest.tap { + it.imp = null + it.site = Site.configFPDSite + it.device = Device.default + } + getProfile(accountId, request, name, mergePrecedence) + } + + static getProfile(String accountId, + BidRequest request, + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { + + new RequestProfile(accountId: accountId, + name: name, + type: ProfileType.REQUEST, + mergePrecedence: mergePrecedence, + body: request) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy index 052bcf2f69f..6a06a9f2e8a 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy @@ -38,7 +38,8 @@ LIMIT 1 "settings.database.account-query" : DB_ACCOUNT_QUERY, "settings.database.stored-requests-query" : "SELECT accountId, reqId, requestData, 'request' as dataType FROM stored_requests WHERE reqId IN (%REQUEST_ID_LIST%) UNION ALL SELECT accountId, impId, impData, 'imp' as dataType FROM stored_imps WHERE impId IN (%IMP_ID_LIST%)", "settings.database.amp-stored-requests-query": "SELECT accountId, reqId, requestData, 'request' as dataType FROM stored_requests WHERE reqId IN (%REQUEST_ID_LIST%)", - "settings.database.stored-responses-query" : "SELECT resId, COALESCE(storedAuctionResponse, storedBidResponse) as responseData FROM stored_responses WHERE resId IN (%RESPONSE_ID_LIST%)" + "settings.database.stored-responses-query" : "SELECT resId, COALESCE(storedAuctionResponse, storedBidResponse) as responseData FROM stored_responses WHERE resId IN (%RESPONSE_ID_LIST%)", + 'settings.database.profiles-query' : "SELECT accountId, profileId, profile, mergePrecedence, type FROM profiles WHERE profileId in (%REQUEST_ID_LIST%, %IMP_ID_LIST%)" ].asImmutable() static Map getPubstackAnalyticsConfig(String scopeId) { diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy index 32fa26b6115..71bf050ecb7 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy @@ -10,6 +10,7 @@ import org.prebid.server.functional.util.SystemProperties import org.testcontainers.DockerClientFactory import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.images.builder.Transferable import org.testcontainers.utility.MountableFile import java.nio.charset.StandardCharsets @@ -117,6 +118,14 @@ class PrebidServerContainer extends GenericContainer { return self() } + PrebidServerContainer withFolder(String containerPath) { + this.withCopyToContainer( + Transferable.of(new byte[0], 010755), + containerPath + "/.keep" + ) + return this + } + private static String getParentDirectory(String containerPath) { Path path = Paths.get(containerPath) return path.getParent().toString() diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index c1bf4a902aa..1cf06f9c272 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.tests + import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountProfilesConfigs @@ -7,65 +8,108 @@ import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredProfileImp import org.prebid.server.functional.model.db.StoredProfileRequest import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.filesystem.FilesystemAccounts import org.prebid.server.functional.model.request.auction.App import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Format import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.model.request.auction.ImpExt import org.prebid.server.functional.model.request.auction.ImpExtPrebid import org.prebid.server.functional.model.request.auction.StoredAuctionResponse import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.request.profile.Profile -import org.prebid.server.functional.model.request.profile.ProfileImp -import org.prebid.server.functional.model.request.profile.ProfileRequest +import org.prebid.server.functional.model.request.profile.ImpProfile +import org.prebid.server.functional.model.request.profile.RequestProfile import org.prebid.server.functional.model.request.profile.ProfileType import org.prebid.server.functional.model.request.auction.Site +import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ErrorType -import org.prebid.server.functional.model.response.auction.NoBidResponse import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.repository.dao.ProfileImpDao import org.prebid.server.functional.repository.dao.ProfileRequestDao +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService -import org.prebid.server.functional.testcontainers.PbsServiceFactory +import org.prebid.server.functional.testcontainers.container.PrebidServerContainer import org.prebid.server.functional.util.PBSUtils +import org.testcontainers.images.builder.Transferable import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.PROFILE import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.REQUEST import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.UNKNOWN -import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO class ProfileSpec extends BaseSpec { private static final String PROFILES_PATH = '/app/prebid-server/profiles' + private static final String REQUESTS_PATH = '/app/prebid-server/requests' + private static final String IMPS_PATH = '/app/prebid-server/imps' + private static final String RESPONSES_PATH = '/app/prebid-server/responses' + private static final String CATEGORIES_PATH = '/app/prebid-server/categories' + private static final String SETTINGS_FILENAME = '/app/prebid-server/settings.yaml' private static final Integer LIMIT_HOST_PROFILE = 2 + private static final Integer ACCOUNT_ID_FILE_STORAGE = PBSUtils.randomNumber + + private static final Map FILESYSTEM_CONFIG = [ + 'settings.filesystem.settings-filename' : SETTINGS_FILENAME, + 'settings.filesystem.profiles-dir' : PROFILES_PATH, + 'settings.filesystem.stored-requests-dir' : REQUESTS_PATH, + 'settings.filesystem.stored-imps-dir' : IMPS_PATH, + 'settings.filesystem.stored-responses-dir': RESPONSES_PATH, + 'settings.filesystem.categories-dir' : CATEGORIES_PATH + ] private static final Map PROFILES_CONFIG = [ - "adapters.openx.enabled" : "true", - "auction.profiles.fail-on-unknown": "false", - "settings.filesystem.profiles-dir": PROFILES_PATH, - "settings.database.profiles-query": 'SELECT profileName, reqId, mergePrecedence, profileType, profileBody FROM profiles_profile WHERE reqId IN (%REQUEST_ID_LIST%)'] + 'adapters.openx.enabled' : "true", + 'auction.profiles.fail-on-unknown': "false", + 'auction.profiles.limit' : LIMIT_HOST_PROFILE.toString(), + 'settings.database.profiles-query': "SELECT accountId, profileId, profile, mergePrecedence, type FROM profiles WHERE profileId in (%REQUEST_ID_LIST%, %IMP_ID_LIST%)".toString()] + ProfileImpDao profileImpDao = repository.profileImpDao ProfileRequestDao profileRequestDao = repository.profileRequestDao - private static PrebidServerService pbsWithStoredProfiles = PbsServiceFactory.getService(PROFILES_CONFIG) + private static PrebidServerContainer pbsContainer + private static PrebidServerService pbsWithStoredProfiles + private static RequestProfile requestProfile + private static ImpProfile impProfile + private static final String REJECT_ERROR_MESSAGE = 'replace' - private static final String MISSING_ERROR_MESSAGE = 'replace' - private static final String REJECT_PROFILE_METRIC = 'profile.rejected' + private static final String LIMIT_ERROR_MESSAGE = 'Profiles exceeded the limit.' + private static final String INVALID_REQEUST_PREFIX = 'Invalid request format: Error during processing profiles: ' + private static final String NO_IMP_PROFILE_MESSAGE = "No imp profiles for ids [%s] were found" + private static final String NO_REQUEST_PROFILE_MESSAGE = "No request profiles for ids [%s] were found" + private static final String NO_PROFILE_MESSAGE = "No profile found for id: %s" private static final String REJECT_ACCOUNT_PROFILE_METRIC = "account.%s.profile.rejected" private static final String MISSING_PROFILE_METRIC = 'profile.rejected' - private static final String MISSING_ACCOUNT_PROFILE_METRIC = "account.%s.profile.rejected" + private static final String LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.limit_exceeded" + private static final String MISSING_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.missing" + + def setupSpec() { + pbsContainer = new PrebidServerContainer(FILESYSTEM_CONFIG + PROFILES_CONFIG) + requestProfile = RequestProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()) + impProfile = ImpProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()) + pbsContainer.withCopyToContainer(Transferable.of(encode(requestProfile)), "$PROFILES_PATH/${requestProfile.fileName}") + pbsContainer.withCopyToContainer(Transferable.of(encode(impProfile)), "$PROFILES_PATH/${impProfile.fileName}") + pbsContainer.withFolder(REQUESTS_PATH) + pbsContainer.withFolder(IMPS_PATH) + pbsContainer.withFolder(RESPONSES_PATH) + pbsContainer.withFolder(CATEGORIES_PATH) + pbsContainer.withCopyToContainer(Transferable.of(encodeYaml(new FilesystemAccounts(accounts: [new AccountConfig(id: ACCOUNT_ID_FILE_STORAGE, status: ACTIVE)]))), + SETTINGS_FILENAME) + pbsContainer.start() + pbsWithStoredProfiles = new PrebidServerService(pbsContainer) + } def cleanupSpec() { - PbsServiceFactory.removeContainer(PROFILES_CONFIG) + pbsContainer.stop() } def "PBS should use profile for request when it exist in database"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def requestProfile = ProfileRequest.getProfile(accountId) + def requestProfile = RequestProfile.getProfile(accountId) def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) and: "Default account" @@ -83,16 +127,36 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request should contain data from profile" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - it.site == requestProfile.body.site - it.device == requestProfile.body.device + verifyAll(response.ext.debug.resolvedRequest) { + it.site.id == requestProfile.body.site.id + it.site.name == requestProfile.body.site.name + it.site.domain == requestProfile.body.site.domain + it.site.cat == requestProfile.body.site.cat + it.site.sectionCat == requestProfile.body.site.sectionCat + it.site.pageCat == requestProfile.body.site.pageCat + it.site.page == requestProfile.body.site.page + it.site.ref == requestProfile.body.site.ref + it.site.search == requestProfile.body.site.search + it.site.keywords == requestProfile.body.site.keywords + it.site.ext.data == requestProfile.body.site.ext.data + + it.device.didsha1 == requestProfile.body.device.didsha1 + it.device.didmd5 == requestProfile.body.device.didmd5 + it.device.dpidsha1 == requestProfile.body.device.dpidsha1 + it.device.ifa == requestProfile.body.device.ifa + it.device.macsha1 == requestProfile.body.device.macsha1 + it.device.macmd5 == requestProfile.body.device.macmd5 + it.device.dpidmd5 == requestProfile.body.device.dpidmd5 } + + and: "PBS shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) } def "PBS should use imp profile for request when it exist in database"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def impProfile = ProfileImp.getProfile(accountId) + def impProfile = ImpProfile.getProfile(accountId) def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { imp.first.banner = null } as BidRequest @@ -112,21 +176,15 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request imp should contain data from profile" - assert bidder.getBidderRequest(bidRequest.id).imp.first == impProfile.body + verifyAll(bidder.getBidderRequest(bidRequest.id).imp) { + it.id == [impProfile.body.id] + it.banner == [impProfile.body.banner] + } } def "PBS should use profile for request when it exist in filesystem"() { given: "Default bidRequest with request profile" - def accountId = PBSUtils.randomNumber as String - def profile = ProfileRequest.getProfile(accountId) - def bidRequest = getRequestWithProfiles(accountId, [profile]) - - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - - and: "Profile fine in PBS container" - pbsWithStoredProfiles.copyToContainer(encode(profile), "$PROFILES_PATH/${profile.fileName}") + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [requestProfile]) when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) @@ -136,27 +194,38 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request should contain data from profile" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - it.site == profile.body.site - it.device == profile.body.device + verifyAll(response.ext.debug.resolvedRequest) { + it.site.id == requestProfile.body.site.id + it.site.name == requestProfile.body.site.name + it.site.domain == requestProfile.body.site.domain + it.site.cat == requestProfile.body.site.cat + it.site.sectionCat == requestProfile.body.site.sectionCat + it.site.pageCat == requestProfile.body.site.pageCat + it.site.page == requestProfile.body.site.page + it.site.ref == requestProfile.body.site.ref + it.site.search == requestProfile.body.site.search + it.site.keywords == requestProfile.body.site.keywords + it.site.ext.data == requestProfile.body.site.ext.data + + it.device.didsha1 == requestProfile.body.device.didsha1 + it.device.didmd5 == requestProfile.body.device.didmd5 + it.device.dpidsha1 == requestProfile.body.device.dpidsha1 + it.device.ifa == requestProfile.body.device.ifa + it.device.macsha1 == requestProfile.body.device.macsha1 + it.device.macmd5 == requestProfile.body.device.macmd5 + it.device.dpidmd5 == requestProfile.body.device.dpidmd5 } + + and: "PBS shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) } def "PBS should use imp profile for request when it exist in filesystem"() { given: "Default bidRequest with request profile" - def accountId = PBSUtils.randomNumber as String - def profile = ProfileImp.getProfile(accountId) - def bidRequest = getRequestWithProfiles(accountId, [profile]).tap { + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [impProfile]).tap { imp.first.banner = null } as BidRequest - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - - and: "Profile fine in PBS container" - pbsWithStoredProfiles.copyToContainer(encode(profile), "$PROFILES_PATH/${profile.fileName}") - when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) @@ -165,22 +234,21 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request imp should contain data from profile" - assert bidder.getBidderRequest(bidRequest.id).imp.first == profile.body + verifyAll(bidder.getBidderRequest(bidRequest.id).imp) { + it.id == [impProfile.body.id] + it.banner == [impProfile.body.banner] + } } def "PBS should emit error for request when same profile exist in filesystem and database"() { given: "Default bidRequest with request profile" - def accountId = PBSUtils.randomNumber as String - def profile = ProfileRequest.getProfile(accountId) - def bidRequest = getRequestWithProfiles(accountId, [profile]) + def profile = RequestProfile.getProfile(requestProfile.accountId, requestProfile.name) + def bidRequest = getRequestWithProfiles(requestProfile.accountId, [profile]) and: "Default account" def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) accountDao.save(account) - and: "Profile fine in PBS container" - pbsWithStoredProfiles.copyToContainer(encode(profile), "$PROFILES_PATH/${profile.fileName}") - and: "Default profile in database" profileRequestDao.save(StoredProfileRequest.getProfile(profile)) @@ -199,8 +267,7 @@ class ProfileSpec extends BaseSpec { and: "Reject metric should increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() - assert metrics[REJECT_PROFILE_METRIC] == 1 - assert metrics[REJECT_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + assert metrics[REJECT_ACCOUNT_PROFILE_METRIC.formatted(requestProfile.accountId)] == 1 and: "Bidder request should contain data from original request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { @@ -212,35 +279,49 @@ class ProfileSpec extends BaseSpec { def "PBS should prioritise original request data over profile when merge strategy #mergeStrategy"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def requestProfile = ProfileRequest.getProfile(accountId).tap { + def requestProfile = RequestProfile.getProfile(accountId).tap { mergePrecedence = mergeStrategy } - BidRequest.getDefaultBidRequest().tap { - it.ext.prebid.profilesNames = [requestProfile.name] - it.site = new Site() - it.device = null - setAccountId(accountId) - } - def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { + it.site = Site.configFPDSite + it.device = Device.default + } as BidRequest and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + def account = new Account(uuid: accountId, status: ACTIVE) accountDao.save(account) and: "Default profile in database" profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) when: "PBS processes auction request" - def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest as BidRequest) then: "No errors should be emitted in the debug" assert !response.ext?.errors - assert !response.ext?.warnings +// assert !response.ext?.warnings and: "Bidder request should contain data from profile" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - it.site == requestProfile.body.site - it.device == requestProfile.body.device + verifyAll(response.ext.debug.resolvedRequest) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + it.site.ext.data == bidRequest.site.ext.data + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 } where: @@ -250,8 +331,10 @@ class ProfileSpec extends BaseSpec { def "PBS should prioritise original imp data over profile when merge strategy #mergeStrategy"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def impProfile = ProfileImp.getProfile(accountId).tap { - mergePrecedence = mergeStrategy + def impProfile = ImpProfile.getProfile(accountId).tap { + it.mergePrecedence = mergeStrategy + it.body.banner.format.first.height = PBSUtils.randomNumber + it.body.banner.format.first.weight = PBSUtils.randomNumber } def bidRequest = getRequestWithProfiles(accountId, [impProfile]) @@ -267,22 +350,27 @@ class ProfileSpec extends BaseSpec { then: "No errors should be emitted in the debug" assert !response.ext?.errors - assert !response.ext?.warnings +// assert !response.ext?.warnings and: "Bidder request imp should contain data from profile" - assert bidder.getBidderRequest(bidRequest.id).imp == [impProfile.body] + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner where: mergeStrategy << [null, UNKNOWN, REQUEST] } - def "PBS should marge earliest-specified profile profile with request merge precedence when there marge conflict"() { + def "PBS should marge latest-specified profile when there marge conflict and different merge precedence present"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def bidRequest = getRequestWithProfiles(accountId, [firstProfile, secondProfile]) + firstProfile.accountId = accountId + secondProfile.accountId = accountId + def bidRequest = getRequestWithProfiles(accountId, [firstProfile, secondProfile]).tap { + it.site = Site.configFPDSite + it.device = Device.default + } and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + def account = new Account(uuid: accountId, status: ACTIVE) accountDao.save(account) and: "Default profiles in database" @@ -300,26 +388,51 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request should contain data from profiles" - def mergedRequest = [firstProfile, secondProfile].find { it.mergePrecedence == REQUEST }.body - assert bidder.getBidderRequest(bidRequest.id).site == mergedRequest.site + def mergedRequest = [firstProfile, secondProfile].find { it.mergePrecedence == PROFILE }.body + and: "Bidder request should contain data from profile" + verifyAll(response.ext.debug.resolvedRequest) { + it.site.id == mergedRequest.site.id + it.site.name == mergedRequest.site.name + it.site.domain == mergedRequest.site.domain + it.site.cat == mergedRequest.site.cat + it.site.sectionCat == mergedRequest.site.sectionCat + it.site.pageCat == mergedRequest.site.pageCat + it.site.page == mergedRequest.site.page + it.site.ref == mergedRequest.site.ref + it.site.search == mergedRequest.site.search + it.site.keywords == mergedRequest.site.keywords + it.site.ext.data == mergedRequest.site.ext.data + + it.device.didsha1 == mergedRequest.device.didsha1 + it.device.didmd5 == mergedRequest.device.didmd5 + it.device.dpidsha1 == mergedRequest.device.dpidsha1 + it.device.ifa == mergedRequest.device.ifa + it.device.macsha1 == mergedRequest.device.macsha1 + it.device.macmd5 == mergedRequest.device.macmd5 + it.device.dpidmd5 == mergedRequest.device.dpidmd5 + } + + and: "PBS shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) where: - firstProfile | secondProfile - ProfileRequest.getProfile(accountId).tap { mergePrecedence = REQUEST } | ProfileRequest.getProfile(accountId) - ProfileRequest.getProfile(accountId) | ProfileRequest.getProfile(accountId).tap { mergePrecedence = REQUEST } - ProfileRequest.getProfile(accountId).tap { mergePrecedence = REQUEST } | ProfileRequest.getProfile(accountId).tap { mergePrecedence = REQUEST } + firstProfile | secondProfile + RequestProfile.getProfile().tap { mergePrecedence = REQUEST } | RequestProfile.getProfile() + RequestProfile.getProfile() | RequestProfile.getProfile().tap { mergePrecedence = REQUEST } } - def "PBS should marge latest-specified profile profile with request merge precedence when there marge conflict"() { + def "PBS should marge first-specified profile with request merge precedence when there marge conflict"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def firstRequestProfile = ProfileRequest.getProfile(accountId).tap { + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { body.device = Device.default - body.site = Site.defaultSite + body.site = Site.rootFPDSite + mergePrecedence = REQUEST } - def secondRequestProfile = ProfileRequest.getProfile(accountId).tap { + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { body.device = Device.default - body.site = Site.defaultSite + body.site = Site.rootFPDSite + mergePrecedence = REQUEST } def bidRequest = getRequestWithProfiles(accountId, [firstRequestProfile, secondRequestProfile]) @@ -330,10 +443,57 @@ class ProfileSpec extends BaseSpec { and: "Default profiles in database" profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) - profileImpDao.save(StoredProfileImp.getProfile(impProfile)) - and: "Flash metrics" - flushMetrics(pbsWithStoredProfiles) + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "No errors should be emitted in the debug" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(response.ext.debug.resolvedRequest) { + it.site.id == firstRequestProfile.body.site.id + it.site.name == firstRequestProfile.body.site.name + it.site.domain == firstRequestProfile.body.site.domain + it.site.cat == firstRequestProfile.body.site.cat + it.site.sectionCat == firstRequestProfile.body.site.sectionCat + it.site.pageCat == firstRequestProfile.body.site.pageCat + it.site.ref == firstRequestProfile.body.site.ref + it.site.search == firstRequestProfile.body.site.search + it.site.keywords == firstRequestProfile.body.site.keywords + it.site.ext.data == firstRequestProfile.body.site.ext.data + + it.device.didsha1 == firstRequestProfile.body.device.didsha1 + it.device.didmd5 == firstRequestProfile.body.device.didmd5 + it.device.dpidsha1 == firstRequestProfile.body.device.dpidsha1 + it.device.ifa == firstRequestProfile.body.device.ifa + it.device.macsha1 == firstRequestProfile.body.device.macsha1 + it.device.macmd5 == firstRequestProfile.body.device.macmd5 + it.device.dpidmd5 == firstRequestProfile.body.device.dpidmd5 + } + } + + def "PBS should marge latest-specified profile with profile merge precedence when there marge conflict"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { + body.device = Device.default + body.site = Site.rootFPDSite + } + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { + body.device = Device.default + body.site = Site.rootFPDSite + } + def bidRequest = getRequestWithProfiles(accountId, [firstRequestProfile, secondRequestProfile]) + + and: "Default account" + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + accountDao.save(account) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) @@ -342,25 +502,47 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.errors assert !response.ext?.warnings - and: "Bidder request should contain data from profiles" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - it.site == secondRequestProfile.body.site - it.device == secondRequestProfile.body.device + and: "Bidder request should contain data from profile" + verifyAll(response.ext.debug.resolvedRequest) { + it.site.id == secondRequestProfile.body.site.id + it.site.name == secondRequestProfile.body.site.name + it.site.domain == secondRequestProfile.body.site.domain + it.site.cat == secondRequestProfile.body.site.cat + it.site.sectionCat == secondRequestProfile.body.site.sectionCat + it.site.pageCat == secondRequestProfile.body.site.pageCat + it.site.page == secondRequestProfile.body.site.page + it.site.ref == secondRequestProfile.body.site.ref + it.site.search == secondRequestProfile.body.site.search + it.site.keywords == secondRequestProfile.body.site.keywords + it.site.ext.data == secondRequestProfile.body.site.ext.data + + it.device.didsha1 == secondRequestProfile.body.device.didsha1 + it.device.didmd5 == secondRequestProfile.body.device.didmd5 + it.device.dpidsha1 == secondRequestProfile.body.device.dpidsha1 + it.device.ifa == secondRequestProfile.body.device.ifa + it.device.macsha1 == secondRequestProfile.body.device.macsha1 + it.device.macmd5 == secondRequestProfile.body.device.macmd5 + it.device.dpidmd5 == secondRequestProfile.body.device.dpidmd5 } + + and: "PBS shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) } - def "PBS should prioritise profile for request when request is overloaded by profiles"() { + def "PBS should prioritise profile for request and emit warning when request is overloaded by profiles"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def profileSite = Site.defaultSite + def profileSite = Site.rootFPDSite def profileDevice = Device.default - def firstRequestProfile = ProfileRequest.getProfile(accountId).tap { + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { body.site = profileSite + body.device = null } - def secondRequestProfile = ProfileRequest.getProfile(accountId).tap { + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { + body.site = null body.device = profileDevice } - def impProfile = ProfileImp.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) + def impProfile = ImpProfile.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) def bidRequest = getRequestWithProfiles(accountId, [impProfile, firstRequestProfile, secondRequestProfile]) and: "Default account" @@ -380,28 +562,47 @@ class ProfileSpec extends BaseSpec { then: "PBS should emit proper warning" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [LIMIT_ERROR_MESSAGE] and: "Response should contain error" assert !response.ext?.errors - and: "PBS log should contain error" - assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) - and: "Missing metric should increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() - assert metrics[MISSING_PROFILE_METRIC] == 1 - assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 - and: "Bidder request should contain data from profiles" - def bidderRequest = bidder.getBidderRequest(bidRequest.id) + and: "Bidder request should contain data from profile" + def bidderRequest = response.ext.debug.resolvedRequest verifyAll(bidderRequest) { - it.site == profileSite - it.device == profileDevice + it.site.id == profileSite.id + it.site.name == profileSite.name + it.site.domain == profileSite.domain + it.site.cat == profileSite.cat + it.site.sectionCat == profileSite.sectionCat + it.site.pageCat == profileSite.pageCat + it.site.page == profileSite.page + it.site.ref == profileSite.ref + it.site.search == profileSite.search + it.site.keywords == profileSite.keywords + it.site.ext.data == profileSite.ext.data + + it.device.didsha1 == profileDevice.didsha1 + it.device.didmd5 == profileDevice.didmd5 + it.device.dpidsha1 == profileDevice.dpidsha1 + it.device.ifa == profileDevice.ifa + it.device.macsha1 == profileDevice.macsha1 + it.device.macmd5 == profileDevice.macmd5 + it.device.dpidmd5 == profileDevice.dpidmd5 } + and: "PBS shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) + and: "Bidder imp should contain original data from request" - assert bidderRequest.imp == bidRequest.imp + assert verifyAll(bidderRequest.imp) { + it.banner == bidRequest.imp.banner + it.video == [null] + } } def "PBS should be able override profile limit by account config and use remaining limits for each imp separately"() { @@ -409,15 +610,26 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def profileSite = Site.defaultSite def profileDevice = Device.default - def firstRequestProfile = ProfileRequest.getProfile(accountId).tap { + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { + body.device = null body.site = profileSite } - def secondRequestProfile = ProfileRequest.getProfile(accountId).tap { + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { + body.site = null body.device = profileDevice } - def firstImpProfile = ProfileImp.getProfile(accountId) - def secondImpProfile = ProfileImp.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) - def thirdImpProfile = ProfileImp.getProfile(accountId, Imp.getDefaultImpression(NATIVE)) + def firstImp = Imp.defaultImpression.tap { + it.banner.btype = [PBSUtils.randomNumber] + } + def secondImp = Imp.defaultImpression.tap { + it.banner.battr = [PBSUtils.randomNumber] + } + def thirdImp = Imp.defaultImpression.tap { + it.banner.mimes = [PBSUtils.randomString] + } + def firstImpProfile = ImpProfile.getProfile(accountId, firstImp) + def secondImpProfile = ImpProfile.getProfile(accountId, secondImp) + def thirdImpProfile = ImpProfile.getProfile(accountId, thirdImp) def bidRequest = getRequestWithProfiles(accountId, [firstImpProfile, secondImpProfile, firstRequestProfile, secondRequestProfile]).tap { imp << new Imp(ext: new ImpExt(prebid: new ImpExtPrebid(profilesNames: [secondImpProfile, thirdImpProfile].name))) } as BidRequest @@ -435,7 +647,6 @@ class ProfileSpec extends BaseSpec { profileImpDao.save(StoredProfileImp.getProfile(firstImpProfile)) profileImpDao.save(StoredProfileImp.getProfile(secondImpProfile)) profileImpDao.save(StoredProfileImp.getProfile(thirdImpProfile)) - profileImpDao.save(StoredProfileImp.getProfile(fourthImpProfile)) and: "Flash metrics" flushMetrics(pbsWithStoredProfiles) @@ -447,39 +658,73 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.errors assert !response.ext?.warnings - and: "PBS log should contain error" - assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) - and: "Missing metric shouldn't increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() assert !metrics[MISSING_PROFILE_METRIC] - assert !metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] + assert !metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] and: "Bidder request should contain data from profiles" - def bidderRequest = bidder.getBidderRequest(bidRequest.id) + def bidderRequest = response.ext.debug.resolvedRequest verifyAll(bidderRequest) { - it.site == profileSite - it.device == profileDevice + it.site.id == profileSite.id + it.site.name == profileSite.name + it.site.domain == profileSite.domain + it.site.cat == profileSite.cat + it.site.sectionCat == profileSite.sectionCat + it.site.pageCat == profileSite.pageCat + it.site.page == profileSite.page + it.site.ref == profileSite.ref + it.site.search == profileSite.search + it.site.keywords == profileSite.keywords + + it.device.didsha1 == profileDevice.didsha1 + it.device.didmd5 == profileDevice.didmd5 + it.device.dpidsha1 == profileDevice.dpidsha1 + it.device.ifa == profileDevice.ifa + it.device.macsha1 == profileDevice.macsha1 + it.device.macmd5 == profileDevice.macmd5 + it.device.dpidmd5 == profileDevice.dpidmd5 + } + + and: "PBS shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Bidder imp should contain data from specified profiles" + def firstBidderImpBanner = bidderRequest.imp.first.banner + verifyAll(firstBidderImpBanner) { + it.btype == firstImpProfile.body.banner.btype + it.battr == secondImpProfile.body.banner.battr } - and: "Bidder imps should contain data from profiles" - assert bidderRequest.imp.first.banner == firstImpProfile.body.banner - assert bidderRequest.imp.first.video == secondImpProfile.body.video - assert bidderRequest.imp.last.video == secondImpProfile.body.video - assert bidderRequest.imp.first.video == thirdImpProfile.body.video - assert bidderRequest.imp.banner == [firstImpProfile.body.banner] + and: "Ignore data from unspecified profiles" + assert !firstBidderImpBanner.mimes + + and: "Bidder imp should contain data from specified profiles" + def secondBidderImpBanner = bidderRequest.imp.last.banner + verifyAll(secondBidderImpBanner) { + it.battr == secondImpProfile.body.banner.battr + it.mimes == thirdImpProfile.body.banner.mimes + } + + and: "Ignore data from unspecified profiles" + assert !secondBidderImpBanner.btype } def "PBS should include invalid or missing profiles into limit count"() { given: "Default bidRequest with request profiles" def accountId = PBSUtils.randomNumber as String - def invalidProfileRequest = ProfileRequest.getProfile(accountId).tap { + def invalidProfileRequest = RequestProfile.getProfile(accountId).tap { body = null } - def impProfile = ProfileImp.getProfile(accountId) + def impProfile = ImpProfile.getProfile(accountId) def bidRequest = BidRequest.getDefaultBidRequest().tap { - imp.first.ext.prebid.profilesNames = [impProfile.name] + it.imp.first.tap { + it.banner.format = [Format.randomFormat] + it.ext.prebid.profilesNames = [impProfile.name] + } it.ext.prebid.profilesNames = [invalidProfileRequest.name, PBSUtils.randomString] + it.site = Site.configFPDSite + it.device = Device.default setAccountId(accountId) } @@ -498,36 +743,49 @@ class ProfileSpec extends BaseSpec { def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) then: "PBS should emit proper warning" - assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + assert response.ext?.warnings[ErrorType.PREBID]*.message.contains(LIMIT_ERROR_MESSAGE) and: "Response should contain error" assert !response.ext?.errors - and: "PBS log should contain error" - assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) - - and: "Missing metric shouldn't increments" + and: "Missing metric should increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() - assert !metrics[MISSING_PROFILE_METRIC] - assert !metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] + assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 - and: "Bidder request should contain data from profiles" - def bidderRequest = bidder.getBidderRequest(bidRequest.id) + and: "Bidder request should contain data from original request" + def bidderRequest = response.ext.debug.resolvedRequest verifyAll(bidderRequest) { - it.site == bidRequest.site - it.device == bidRequest.device + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + it.site.ext.data == bidRequest.site.ext.data + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 } - and: "Bidder imp should contain original data from request" - assert bidderRequest.imp == bidRequest.imp + and: "Bidder request imp should contain data from request" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner } + def "PBS should include data from storedBidResponses when it specified in profiles"() { given: "Default basic BidRequest with stored response" def accountId = PBSUtils.randomNumber as String def storedResponseId = PBSUtils.randomNumber - def impProfile = ProfileImp.getProfile(accountId).tap { + def impProfile = ImpProfile.getProfile(accountId).tap { it.body.ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } def bidRequest = getRequestWithProfiles(accountId, [impProfile]) @@ -559,7 +817,7 @@ class ProfileSpec extends BaseSpec { given: "Default basic BidRequest with stored response" def accountId = PBSUtils.randomNumber as String def storedAuctionId = PBSUtils.randomNumber - def impProfile = ProfileImp.getProfile(accountId).tap { + def impProfile = ImpProfile.getProfile(accountId).tap { it.body.ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedAuctionId) } def bidRequest = getRequestWithProfiles(accountId, [impProfile]) @@ -596,8 +854,9 @@ class ProfileSpec extends BaseSpec { and: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [PBSUtils.randomString] + it.imp.first.ext.prebid.profilesNames = [invalidProfileId] it.site = new Site() it.device = null setAccountId(accountId) @@ -611,18 +870,12 @@ class ProfileSpec extends BaseSpec { flushMetrics(prebidServerService) when: "PBS processes auction request" - def response = prebidServerService.sendAuctionRequest(bidRequest) - - then: "PBS should emit proper warning" - assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] - - and: "Response should contain error" - assert !response.ext?.errors + prebidServerService.sendAuctionRequest(bidRequest) - and: "PBS should fail auction" - assert !response.seatbid.bid - assert response.noBidResponse == NoBidResponse.INVALID_REQUEST + then: "PBs should throw error due to invalid profile" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == INVALID_REQEUST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) cleanup: "Stop and remove pbs container" pbsServiceFactory.removeContainer(failOnUnknownProfilesConfig) @@ -636,8 +889,9 @@ class ProfileSpec extends BaseSpec { and: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [PBSUtils.randomString] + it.imp.first.ext.prebid.profilesNames = [invalidProfileId] it.site = new Site() it.device = null setAccountId(accountId) @@ -651,33 +905,23 @@ class ProfileSpec extends BaseSpec { flushMetrics(prebidServerService) when: "PBS processes auction request" - def response = prebidServerService.sendAuctionRequest(bidRequest) + prebidServerService.sendAuctionRequest(bidRequest) - then: "PBS should emit proper warning" - assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] - - and: "Response should contain error" - assert !response.ext?.errors - - and: "PBS should fail auction" - assert !response.seatbid.bid - assert response.noBidResponse == NoBidResponse.INVALID_REQUEST + then: "PBs should throw error due to invalid profile" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == INVALID_REQEUST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) cleanup: "Stop and remove pbs container" pbsServiceFactory.removeContainer(failOnUnknownProfilesConfig) } def "PBS should prioritise fail-on-unknown-profile from account over host config"() { - given: "PBS with profiles.fail-on-unknown config" - def failOnUnknownProfilesConfig = new HashMap<>(PROFILES_CONFIG) - failOnUnknownProfilesConfig["auction.profiles.fail-on-unknown"] = "true" - def prebidServerService = pbsServiceFactory.getService(failOnUnknownProfilesConfig) - - and: "Default bidRequest with request profile" + given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [PBSUtils.randomString] + it.imp.first.ext.prebid.profilesNames = [invalidProfileId] it.site = new Site() it.device = null setAccountId(accountId) @@ -690,35 +934,20 @@ class ProfileSpec extends BaseSpec { accountDao.save(account) and: "Flash metrics" - flushMetrics(prebidServerService) + flushMetrics(pbsWithStoredProfiles) when: "PBS processes auction request" - def response = prebidServerService.sendAuctionRequest(bidRequest) - - then: "PBS should emit proper warning" - assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - and: "Response should contain error" - assert !response.ext?.errors - - and: "PBS log should contain error" - assert prebidServerService.isContainLogsByValue(MISSING_ERROR_MESSAGE) + then: "PBs should throw error due to invalid profile" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == INVALID_REQEUST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) and: "Missing metric should increments" - def metrics = prebidServerService.sendCollectedMetricsRequest() - assert metrics[MISSING_PROFILE_METRIC] == 1 + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 - and: "Bidder request should contain data from original request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - it.site == bidRequest.site - it.device == bidRequest.device - } - - cleanup: "Stop and remove pbs container" - pbsServiceFactory.removeContainer(failOnUnknownProfilesConfig) - where: profilesConfigs << [ new AccountProfilesConfigs(failOnUnknown: true), @@ -729,17 +958,20 @@ class ProfileSpec extends BaseSpec { def "PBS should ignore inner request profiles when stored request profile contain link for another profile"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def innerRequestProfile = ProfileRequest.getProfile(accountId).tap { + def innerRequestProfile = RequestProfile.getProfile(accountId).tap { it.body.app = App.defaultApp } - def requestProfile = ProfileRequest.getProfile(accountId).tap { + def requestProfile = RequestProfile.getProfile(accountId).tap { it.body.ext.prebid.profilesNames = [innerRequestProfile.name] } - def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { + site = Site.configFPDSite + device = Device.default + } as BidRequest and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + def account = new Account(uuid: accountId, status: ACTIVE) accountDao.save(account) and: "Default profiles in database" @@ -754,10 +986,27 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request should contain data from profile" - def bidderRequest = bidder.getBidderRequest(bidRequest.id) - verifyAll(bidderRequest) { - it.site == requestProfile.body.site - it.device == requestProfile.body.device + def bidderRequest = response.ext.debug.resolvedRequest + verifyAll(response.ext.debug.resolvedRequest) { + it.site.id == requestProfile.body.site.id + it.site.name == requestProfile.body.site.name + it.site.domain == requestProfile.body.site.domain + it.site.cat == requestProfile.body.site.cat + it.site.sectionCat == requestProfile.body.site.sectionCat + it.site.pageCat == requestProfile.body.site.pageCat + it.site.page == requestProfile.body.site.page + it.site.ref == requestProfile.body.site.ref + it.site.search == requestProfile.body.site.search + it.site.keywords == requestProfile.body.site.keywords + it.site.ext.data == requestProfile.body.site.ext.data + + it.device.didsha1 == requestProfile.body.device.didsha1 + it.device.didmd5 == requestProfile.body.device.didmd5 + it.device.dpidsha1 == requestProfile.body.device.dpidsha1 + it.device.ifa == requestProfile.body.device.ifa + it.device.macsha1 == requestProfile.body.device.macsha1 + it.device.macmd5 == requestProfile.body.device.macmd5 + it.device.dpidmd5 == requestProfile.body.device.dpidmd5 } and: "Bidder request shouldn't contain data from inner profile" @@ -767,8 +1016,8 @@ class ProfileSpec extends BaseSpec { def "PBS should ignore inner imp profiles when stored imp profile contain link for another profile"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def innerImpProfile = ProfileImp.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) - def impProfile = ProfileImp.getProfile(accountId).tap { + def innerImpProfile = ImpProfile.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) + def impProfile = ImpProfile.getProfile(accountId).tap { body.ext.prebid.profilesNames = [innerImpProfile.name] } def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { @@ -801,15 +1050,19 @@ class ProfileSpec extends BaseSpec { def "PBS shouldn't validate profiles and imp before margining"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def impProfile = ProfileImp.getProfile(accountId).tap { - body.banner.weight = null + def weight = PBSUtils.randomNumber + def height = PBSUtils.randomNumber + def impProfile = ImpProfile.getProfile(accountId).tap { + it.body.banner.format.first.weight = null + it.body.banner.format.first.height = height } def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { - imp.first.banner.height = null + imp.first.banner.format.first.height = null + imp.first.banner.format.first.weight = weight } as BidRequest and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) + def account = new Account(uuid: accountId, status: ACTIVE) accountDao.save(account) and: "Default profile in database" @@ -823,18 +1076,26 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request imp should contain data from profile" - assert bidder.getBidderRequest(bidRequest.id).imp.first == impProfile.body + def impBannerFormat = bidder.getBidderRequest(bidRequest.id).imp.first.banner.format + assert impBannerFormat.height == [height] + assert impBannerFormat.weight == [weight] } def "PBS shouldn't emit error or warnings when bidRequest contains multiple imps with same profile"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def impProfile = ProfileImp.getProfile(accountId, Imp.defaultImpression) + def imp = Imp.defaultImpression.tap { + it.banner.format.first.height = PBSUtils.randomNumber + it.banner.format.first.weight = PBSUtils.randomNumber + } + def impProfile = ImpProfile.getProfile(accountId, imp) def bidRequest = BidRequest.getDefaultBidRequest().tap { addImp(Imp.getDefaultImpression()) setAccountId(accountId) - imp.each { it.ext.prebid.profilesNames = [impProfile.name] } } as BidRequest + bidRequest.imp.each { + it.ext.prebid.profilesNames = [impProfile.name] + } and: "Default account" def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) @@ -851,15 +1112,19 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request imps should contain data from profile" - assert bidder.getBidderRequest(bidRequest.id).imp.first == impProfile.body - assert bidder.getBidderRequest(bidRequest.id).imp.last == impProfile.body + assert bidder.getBidderRequest(bidRequest.id).imp.first.banner == impProfile.body.banner + assert bidder.getBidderRequest(bidRequest.id).imp.last.banner == impProfile.body.banner } def "PBS should ignore imp data from request profile when imp for profile not null"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def requestProfile = ProfileRequest.getProfile(accountId, - BidRequest.defaultBidRequest, + def bidRequestProfile = BidRequest.defaultBidRequest.tap { + it.imp.first.banner.format.first.height = PBSUtils.randomNumber + it.imp.first.banner.format.first.weight = PBSUtils.randomNumber + } + def requestProfile = RequestProfile.getProfile(accountId, + bidRequestProfile, PBSUtils.randomString, mergePrecedence) def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) @@ -879,20 +1144,18 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request should contain data from profile" - assert bidder.getBidderRequest(bidRequest.id).imp == bidRequest.imp + assert response.ext.debug.resolvedRequest.imp.banner == bidRequest.imp.banner where: mergePrecedence << [REQUEST, PROFILE] } - def "PBS should emit error and metrics when profile name is invalid"() { + def "PBS should add error and metrics when imp name is invalid"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def impProfile = ProfileImp.getProfile(accountId, Imp.defaultImpression, invalidProfileName) + def impProfile = ImpProfile.getProfile(accountId, Imp.defaultImpression, invalidProfileName) def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [PBSUtils.randomString] - it.site = new Site() - it.device = null + it.imp.first.ext.prebid.profilesNames = [impProfile.name] setAccountId(accountId) } @@ -911,18 +1174,18 @@ class ProfileSpec extends BaseSpec { then: "PBS should emit proper warning" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [LIMIT_ERROR_MESSAGE] and: "Response should contain error" assert !response.ext?.errors and: "PBS log should contain error" - assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) + assert pbsWithStoredProfiles.isContainLogsByValue(LIMIT_ERROR_MESSAGE) and: "Missing metric should increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() assert metrics[MISSING_PROFILE_METRIC] == 1 - assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 and: "Bidder request should contain data from original request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { @@ -937,11 +1200,11 @@ class ProfileSpec extends BaseSpec { def "PBS should emit error and metrics when request profile called from imp level"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def requestProfile = ProfileRequest.getProfile(accountId) + def requestProfile = RequestProfile.getProfile(accountId) def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [requestProfile.recordName] - it.site = new Site() - it.device = null + it.imp.first.ext.prebid.profilesNames = [requestProfile.name] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() setAccountId(accountId) } @@ -960,34 +1223,46 @@ class ProfileSpec extends BaseSpec { then: "PBS should emit proper warning" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [REJECT_ERROR_MESSAGE] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.name)] and: "Response should contain error" assert !response.ext?.errors - and: "PBS log should contain error" - assert pbsWithStoredProfiles.isContainLogsByValue(REJECT_ERROR_MESSAGE) - - and: "Reject metric should increments" + and: "Missing metric should increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() - assert metrics[REJECT_PROFILE_METRIC] == 1 - assert metrics[REJECT_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 - and: "Bidder request should contain data from original request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - it.site == bidRequest.site - it.device == bidRequest.device + and: "Bidder request should contain data from profile" + verifyAll(response.ext.debug.resolvedRequest) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 } } def "PBS should emit error and metrics when imp profile called from request level"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def requestProfile = ProfileImp.getProfile(accountId) + def requestProfile = ImpProfile.getProfile(accountId) def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.ext.prebid.profilesNames = [requestProfile.recordName] - it.site = new Site() - it.device = null + it.ext.prebid.profilesNames = [requestProfile.name] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() setAccountId(accountId) } @@ -1006,33 +1281,44 @@ class ProfileSpec extends BaseSpec { then: "PBS should emit proper warning" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [ERROR_MESSAGE] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.name)] and: "Response should contain error" assert !response.ext?.errors - and: "PBS log should contain error" - assert pbsWithStoredProfiles.isContainLogsByValue(ERROR_MESSAGE) - - and: "Reject metric should increments" + and: "Missing metric should increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() - assert metrics[REJECT_PROFILE_METRIC] == 1 - assert metrics[REJECT_ACCOUNT_PROFILE_METRIC] == 1 + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 - and: "Bidder request should contain data from original request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - it.site == bidRequest.site - it.device == bidRequest.device + and: "Bidder request should contain data from profile" + verifyAll(response.ext.debug.resolvedRequest) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 } } - def "PBS should emit error and metrics when request profile missing"() { + def "PBS should emit error and metrics when imp profile missing"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [PBSUtils.randomString] - it.site = new Site() - it.device = null + it.imp.first.ext.prebid.profilesNames = [invalidProfileId] setAccountId(accountId) } @@ -1048,33 +1334,27 @@ class ProfileSpec extends BaseSpec { then: "PBS should emit proper warning" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId)] and: "Response should contain error" assert !response.ext?.errors - and: "PBS log should contain error" - assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) - and: "Missing metric should increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() - assert metrics[MISSING_PROFILE_METRIC] == 1 assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 - and: "Bidder request should contain data from original request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - it.site == bidRequest.site - it.device == bidRequest.device - } + and: "Bidder request imp should contain data from original imp" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner } - def "PBS should emit error and metrics when imp profile missing"() { + def "PBS should emit error and metrics when request profile missing"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.ext.prebid.profilesNames = [PBSUtils.randomString] - it.site = new Site() - it.device = null + it.ext.prebid.profilesNames = [invalidProfileId] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() setAccountId(accountId) } @@ -1088,38 +1368,47 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "PBS should emit proper warning" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [MISSING_ERROR_MESSAGE] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_REQUEST_PROFILE_MESSAGE.formatted(invalidProfileId)] and: "Response should contain error" assert !response.ext?.errors - and: "PBS log should contain error" - assert pbsWithStoredProfiles.isContainLogsByValue(MISSING_ERROR_MESSAGE) - and: "Missing metric should increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() - assert metrics[MISSING_PROFILE_METRIC] == 1 assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 - and: "Bidder request should contain data from original request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - it.site == bidRequest.site - it.device == bidRequest.device + and: "Bidder request should contain data from profile" + verifyAll(response.ext.debug.resolvedRequest) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 } } private static BidRequest getRequestWithProfiles(String accountId, List profiles) { BidRequest.getDefaultBidRequest().tap { if (profiles.type.contains(ProfileType.IMP)) { - imp = [new Imp(ext: new ImpExt(prebid: new ImpExtPrebid(profilesNames: profiles.findAll { it.type == ProfileType.IMP }*.name)))] + imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.name } imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.name it.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.REQUEST }*.name - it.site = new Site() - it.device = null setAccountId(accountId) } } diff --git a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy index 3ab9e349ac9..a5da8c2cada 100644 --- a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.xml.XmlMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL @@ -11,6 +12,8 @@ trait ObjectMapperWrapper { private static final ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(NON_NULL) .registerModule(new ZonedDateTimeModule()) + private static final YAMLMapper yaml = new YAMLMapper().setSerializationInclusion(NON_NULL) + .registerModule(new ZonedDateTimeModule()) as YAMLMapper private static final XmlMapper xmlMapper = new XmlMapper() final static String encode(Object object) { @@ -44,4 +47,20 @@ trait ObjectMapperWrapper { final static String encodeXml(Object object) { xmlMapper.writeValueAsString(object) } + + final static String encodeYaml(Object object) { + yaml.writeValueAsString(object) + } + + final static T decodeYaml(String yamlString, Class clazz) { + yaml.readValue(yamlString, clazz) + } + + final static T decodeYaml(String yamlString, TypeReference typeReference) { + yaml.readValue(yamlString, typeReference) + } + + final static T decodeYaml(InputStream inputStream, Class clazz) { + yaml.readValue(inputStream, clazz) + } } diff --git a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql index 0a17a030905..2245e033dc9 100644 --- a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql +++ b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql @@ -42,13 +42,13 @@ CREATE TABLE stored_responses storedBidResponse varchar(1024) ); -CREATE TABLE profiles_profile +CREATE TABLE profiles ( - id SERIAL PRIMARY KEY, - profileName varchar(64) UNIQUE, - mergePrecedence enum ('request','profile'), - profileType enum('request', 'imp'), - profileBody json + accountId varchar(64) NOT NULL, + profileId varchar(128) NOT NULL, + profile json, + mergePrecedence enum ('request', 'profile', 'unknown'), + type enum ('request', 'imp', 'unknown') ); -- set session wait timeout to 1 minute From 8bc71465efbf3181851c7146b10e56d2f8e13072 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Mon, 1 Sep 2025 12:32:56 +0300 Subject: [PATCH 04/13] Update functional test --- ...groovy => FileSystemAccountsConfig.groovy} | 2 +- .../functional/tests/ProfileSpec.groovy | 144 +++++++++--------- 2 files changed, 73 insertions(+), 73 deletions(-) rename src/test/groovy/org/prebid/server/functional/model/filesystem/{FilesystemAccounts.groovy => FileSystemAccountsConfig.groovy} (92%) diff --git a/src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccounts.groovy b/src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy similarity index 92% rename from src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccounts.groovy rename to src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy index 317b922c098..f01a2787d7e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccounts.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy @@ -7,7 +7,7 @@ import org.prebid.server.functional.model.config.AccountConfig @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) -class FilesystemAccounts { +class FileSystemAccountsConfig { List accounts } diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index 1cf06f9c272..e051b39a841 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -1,6 +1,5 @@ package org.prebid.server.functional.tests - import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountProfilesConfigs @@ -8,7 +7,7 @@ import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredProfileImp import org.prebid.server.functional.model.db.StoredProfileRequest import org.prebid.server.functional.model.db.StoredResponse -import org.prebid.server.functional.model.filesystem.FilesystemAccounts +import org.prebid.server.functional.model.filesystem.FileSystemAccountsConfig import org.prebid.server.functional.model.request.auction.App import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Device @@ -23,7 +22,6 @@ import org.prebid.server.functional.model.request.profile.ImpProfile import org.prebid.server.functional.model.request.profile.RequestProfile import org.prebid.server.functional.model.request.profile.ProfileType import org.prebid.server.functional.model.request.auction.Site -import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.model.response.auction.SeatBid @@ -66,14 +64,8 @@ class ProfileSpec extends BaseSpec { 'adapters.openx.enabled' : "true", 'auction.profiles.fail-on-unknown': "false", 'auction.profiles.limit' : LIMIT_HOST_PROFILE.toString(), - 'settings.database.profiles-query': "SELECT accountId, profileId, profile, mergePrecedence, type FROM profiles WHERE profileId in (%REQUEST_ID_LIST%, %IMP_ID_LIST%)".toString()] - - ProfileImpDao profileImpDao = repository.profileImpDao - ProfileRequestDao profileRequestDao = repository.profileRequestDao - private static PrebidServerContainer pbsContainer - private static PrebidServerService pbsWithStoredProfiles - private static RequestProfile requestProfile - private static ImpProfile impProfile + 'settings.database.profiles-query': "SELECT accountId, profileId, profile, mergePrecedence, type FROM profiles " + + "WHERE profileId in (%REQUEST_ID_LIST%, %IMP_ID_LIST%)".toString()] private static final String REJECT_ERROR_MESSAGE = 'replace' private static final String LIMIT_ERROR_MESSAGE = 'Profiles exceeded the limit.' @@ -82,10 +74,17 @@ class ProfileSpec extends BaseSpec { private static final String NO_REQUEST_PROFILE_MESSAGE = "No request profiles for ids [%s] were found" private static final String NO_PROFILE_MESSAGE = "No profile found for id: %s" private static final String REJECT_ACCOUNT_PROFILE_METRIC = "account.%s.profile.rejected" - private static final String MISSING_PROFILE_METRIC = 'profile.rejected' private static final String LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.limit_exceeded" private static final String MISSING_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.missing" + private static final ProfileImpDao profileImpDao = repository.profileImpDao + private static final ProfileRequestDao profileRequestDao = repository.profileRequestDao + + private static PrebidServerContainer pbsContainer + private static PrebidServerService pbsWithStoredProfiles + private static RequestProfile requestProfile + private static ImpProfile impProfile + def setupSpec() { pbsContainer = new PrebidServerContainer(FILESYSTEM_CONFIG + PROFILES_CONFIG) requestProfile = RequestProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()) @@ -96,7 +95,8 @@ class ProfileSpec extends BaseSpec { pbsContainer.withFolder(IMPS_PATH) pbsContainer.withFolder(RESPONSES_PATH) pbsContainer.withFolder(CATEGORIES_PATH) - pbsContainer.withCopyToContainer(Transferable.of(encodeYaml(new FilesystemAccounts(accounts: [new AccountConfig(id: ACCOUNT_ID_FILE_STORAGE, status: ACTIVE)]))), + def accountsConfig = new FileSystemAccountsConfig(accounts: [new AccountConfig(id: ACCOUNT_ID_FILE_STORAGE, status: ACTIVE)]) + pbsContainer.withCopyToContainer(Transferable.of(encodeYaml(accountsConfig)), SETTINGS_FILENAME) pbsContainer.start() pbsWithStoredProfiles = new PrebidServerService(pbsContainer) @@ -122,7 +122,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings @@ -158,7 +158,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def impProfile = ImpProfile.getProfile(accountId) def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { - imp.first.banner = null + it.imp.first.banner = null } as BidRequest and: "Default account" @@ -171,7 +171,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings @@ -189,7 +189,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings @@ -223,13 +223,13 @@ class ProfileSpec extends BaseSpec { def "PBS should use imp profile for request when it exist in filesystem"() { given: "Default bidRequest with request profile" def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [impProfile]).tap { - imp.first.banner = null + it.imp.first.banner = null } as BidRequest when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings @@ -240,6 +240,7 @@ class ProfileSpec extends BaseSpec { } } + // TODO in discussion def "PBS should emit error for request when same profile exist in filesystem and database"() { given: "Default bidRequest with request profile" def profile = RequestProfile.getProfile(requestProfile.accountId, requestProfile.name) @@ -280,7 +281,7 @@ class ProfileSpec extends BaseSpec { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def requestProfile = RequestProfile.getProfile(accountId).tap { - mergePrecedence = mergeStrategy + it.mergePrecedence = mergeStrategy } def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { it.site = Site.configFPDSite @@ -297,8 +298,9 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest as BidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors + // TODO in discussion // assert !response.ext?.warnings and: "Bidder request should contain data from profile" @@ -329,12 +331,11 @@ class ProfileSpec extends BaseSpec { } def "PBS should prioritise original imp data over profile when merge strategy #mergeStrategy"() { - given: "Default bidRequest with request profile" + given: "Default bidRequest with imp profile" def accountId = PBSUtils.randomNumber as String def impProfile = ImpProfile.getProfile(accountId).tap { it.mergePrecedence = mergeStrategy - it.body.banner.format.first.height = PBSUtils.randomNumber - it.body.banner.format.first.weight = PBSUtils.randomNumber + it.body.banner.format = [Format.randomFormat] } def bidRequest = getRequestWithProfiles(accountId, [impProfile]) @@ -348,8 +349,9 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors + // TODO in discussion // assert !response.ext?.warnings and: "Bidder request imp should contain data from profile" @@ -383,13 +385,12 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings and: "Bidder request should contain data from profiles" def mergedRequest = [firstProfile, secondProfile].find { it.mergePrecedence == PROFILE }.body - and: "Bidder request should contain data from profile" verifyAll(response.ext.debug.resolvedRequest) { it.site.id == mergedRequest.site.id it.site.name == mergedRequest.site.name @@ -425,14 +426,14 @@ class ProfileSpec extends BaseSpec { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def firstRequestProfile = RequestProfile.getProfile(accountId).tap { - body.device = Device.default - body.site = Site.rootFPDSite - mergePrecedence = REQUEST + it.body.device = Device.default + it.body.site = Site.rootFPDSite + it.mergePrecedence = REQUEST } def secondRequestProfile = RequestProfile.getProfile(accountId).tap { - body.device = Device.default - body.site = Site.rootFPDSite - mergePrecedence = REQUEST + it.body.device = Device.default + it.body.site = Site.rootFPDSite + it.mergePrecedence = REQUEST } def bidRequest = getRequestWithProfiles(accountId, [firstRequestProfile, secondRequestProfile]) @@ -447,7 +448,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings @@ -478,12 +479,12 @@ class ProfileSpec extends BaseSpec { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def firstRequestProfile = RequestProfile.getProfile(accountId).tap { - body.device = Device.default - body.site = Site.rootFPDSite + it.body.device = Device.default + it.body.site = Site.rootFPDSite } def secondRequestProfile = RequestProfile.getProfile(accountId).tap { - body.device = Device.default - body.site = Site.rootFPDSite + it.body.device = Device.default + it.body.site = Site.rootFPDSite } def bidRequest = getRequestWithProfiles(accountId, [firstRequestProfile, secondRequestProfile]) @@ -498,7 +499,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings @@ -530,17 +531,17 @@ class ProfileSpec extends BaseSpec { } def "PBS should prioritise profile for request and emit warning when request is overloaded by profiles"() { - given: "Default bidRequest with request profile" + given: "Default bidRequest with profiles" def accountId = PBSUtils.randomNumber as String def profileSite = Site.rootFPDSite def profileDevice = Device.default def firstRequestProfile = RequestProfile.getProfile(accountId).tap { - body.site = profileSite - body.device = null + it.body.site = profileSite + it.body.device = null } def secondRequestProfile = RequestProfile.getProfile(accountId).tap { - body.site = null - body.device = profileDevice + it.body.site = null + it.body.device = profileDevice } def impProfile = ImpProfile.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) def bidRequest = getRequestWithProfiles(accountId, [impProfile, firstRequestProfile, secondRequestProfile]) @@ -606,17 +607,17 @@ class ProfileSpec extends BaseSpec { } def "PBS should be able override profile limit by account config and use remaining limits for each imp separately"() { - given: "Default bidRequest with request profile" + given: "BidRequest with profiles" def accountId = PBSUtils.randomNumber as String def profileSite = Site.defaultSite def profileDevice = Device.default def firstRequestProfile = RequestProfile.getProfile(accountId).tap { - body.device = null - body.site = profileSite + it.body.device = null + it.body.site = profileSite } def secondRequestProfile = RequestProfile.getProfile(accountId).tap { - body.site = null - body.device = profileDevice + it.body.site = null + it.body.device = profileDevice } def firstImp = Imp.defaultImpression.tap { it.banner.btype = [PBSUtils.randomNumber] @@ -654,13 +655,12 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings and: "Missing metric shouldn't increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() - assert !metrics[MISSING_PROFILE_METRIC] assert !metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] and: "Bidder request should contain data from profiles" @@ -714,7 +714,7 @@ class ProfileSpec extends BaseSpec { given: "Default bidRequest with request profiles" def accountId = PBSUtils.randomNumber as String def invalidProfileRequest = RequestProfile.getProfile(accountId).tap { - body = null + it.body = null } def impProfile = ImpProfile.getProfile(accountId) def bidRequest = BidRequest.getDefaultBidRequest().tap { @@ -780,9 +780,9 @@ class ProfileSpec extends BaseSpec { assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner } - + // TODO in discussion def "PBS should include data from storedBidResponses when it specified in profiles"() { - given: "Default basic BidRequest with stored response" + given: "Default BidRequest with profile" def accountId = PBSUtils.randomNumber as String def storedResponseId = PBSUtils.randomNumber def impProfile = ImpProfile.getProfile(accountId).tap { @@ -813,8 +813,9 @@ class ProfileSpec extends BaseSpec { assert bidder.getRequestCount(bidRequest.id) == 0 } + // TODO in discussion def "PBS should include data from storedAuctionResponse when it specified in profiles"() { - given: "Default basic BidRequest with stored response" + given: "Default basic BidRequest with profile" def accountId = PBSUtils.randomNumber as String def storedAuctionId = PBSUtils.randomNumber def impProfile = ImpProfile.getProfile(accountId).tap { @@ -966,8 +967,8 @@ class ProfileSpec extends BaseSpec { it.body.ext.prebid.profilesNames = [innerRequestProfile.name] } def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { - site = Site.configFPDSite - device = Device.default + it.site = Site.configFPDSite + it.device = Device.default } as BidRequest and: "Default account" @@ -981,7 +982,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings @@ -1014,14 +1015,14 @@ class ProfileSpec extends BaseSpec { } def "PBS should ignore inner imp profiles when stored imp profile contain link for another profile"() { - given: "Default bidRequest with request profile" + given: "Default bidRequest with imp profile" def accountId = PBSUtils.randomNumber as String def innerImpProfile = ImpProfile.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) def impProfile = ImpProfile.getProfile(accountId).tap { - body.ext.prebid.profilesNames = [innerImpProfile.name] + it.body.ext.prebid.profilesNames = [innerImpProfile.name] } def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { - imp.first.banner = null + it.imp.first.banner = null } as BidRequest and: "Default account" @@ -1035,7 +1036,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings @@ -1047,6 +1048,7 @@ class ProfileSpec extends BaseSpec { assert bidderImp.video == impProfile.body.video } + // TODO in discussion def "PBS shouldn't validate profiles and imp before margining"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String @@ -1071,7 +1073,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings @@ -1085,8 +1087,7 @@ class ProfileSpec extends BaseSpec { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def imp = Imp.defaultImpression.tap { - it.banner.format.first.height = PBSUtils.randomNumber - it.banner.format.first.weight = PBSUtils.randomNumber + it.banner.format = [Format.randomFormat] } def impProfile = ImpProfile.getProfile(accountId, imp) def bidRequest = BidRequest.getDefaultBidRequest().tap { @@ -1107,7 +1108,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings @@ -1120,8 +1121,7 @@ class ProfileSpec extends BaseSpec { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def bidRequestProfile = BidRequest.defaultBidRequest.tap { - it.imp.first.banner.format.first.height = PBSUtils.randomNumber - it.imp.first.banner.format.first.weight = PBSUtils.randomNumber + it.imp.first.banner.format = [Format.randomFormat] } def requestProfile = RequestProfile.getProfile(accountId, bidRequestProfile, @@ -1139,7 +1139,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in the debug" + then: "No errors should be emitted in debug" assert !response.ext?.errors assert !response.ext?.warnings @@ -1150,6 +1150,7 @@ class ProfileSpec extends BaseSpec { mergePrecedence << [REQUEST, PROFILE] } + // TODO in discussion def "PBS should add error and metrics when imp name is invalid"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String @@ -1184,7 +1185,6 @@ class ProfileSpec extends BaseSpec { and: "Missing metric should increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() - assert metrics[MISSING_PROFILE_METRIC] == 1 assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 and: "Bidder request should contain data from original request" @@ -1405,9 +1405,9 @@ class ProfileSpec extends BaseSpec { private static BidRequest getRequestWithProfiles(String accountId, List profiles) { BidRequest.getDefaultBidRequest().tap { if (profiles.type.contains(ProfileType.IMP)) { - imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.name + it.imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.name } - imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.name + it.imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.name it.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.REQUEST }*.name setAccountId(accountId) } From 752c62b4e72aa970fcea160e352eec880276d71b Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Mon, 1 Sep 2025 17:41:08 +0300 Subject: [PATCH 05/13] Update functional test --- .../model/db/StoredProfileImp.groovy | 2 +- .../model/db/StoredProfileRequest.groovy | 2 +- .../model/request/profile/ImpProfile.groovy | 2 +- .../model/request/profile/Profile.groovy | 4 +- .../request/profile/RequestProfile.groovy | 2 +- .../functional/tests/ProfileSpec.groovy | 154 +++++++++--------- 6 files changed, 87 insertions(+), 79 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy index bbc91786797..19f87ac0d9c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy @@ -36,7 +36,7 @@ class StoredProfileImp { static StoredProfileImp getProfile(ImpProfile profile) { new StoredProfileImp().tap { - it.profileName = profile.name + it.profileName = profile.id it.accountId = profile.accountId it.mergePrecedence = profile.mergePrecedence it.type = profile.type diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy index 1bd3681b159..8b04143d368 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy @@ -36,7 +36,7 @@ class StoredProfileRequest { static StoredProfileRequest getProfile(RequestProfile profile) { new StoredProfileRequest().tap { - it.profileName = profile.name + it.profileName = profile.id it.accountId = profile.accountId it.mergePrecedence = profile.mergePrecedence it.type = profile.type diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy index ae88fdb01b7..bd73f5efee5 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy @@ -15,7 +15,7 @@ class ImpProfile extends Profile { ProfileMergePrecedence mergePrecedence = PROFILE) { new ImpProfile(accountId: accountId, - name: name, + id: name, type: ProfileType.IMP, mergePrecedence: mergePrecedence, body: imp) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy index 82e7ad103c7..2bb6387da77 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy @@ -8,14 +8,14 @@ abstract class Profile { @JsonIgnore String accountId @JsonIgnore - String name + String id ProfileType type @JsonProperty("mergeprecedence") ProfileMergePrecedence mergePrecedence T body String getRecordName() { - "${accountId}-${name}" + "${accountId}-${id}" } String getFileName() { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy index 74fcc404e0d..414c7bbda93 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy @@ -28,7 +28,7 @@ class RequestProfile extends Profile { ProfileMergePrecedence mergePrecedence = PROFILE) { new RequestProfile(accountId: accountId, - name: name, + id: name, type: ProfileType.REQUEST, mergePrecedence: mergePrecedence, body: request) diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index e051b39a841..4adba6581e3 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -32,6 +32,7 @@ import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.testcontainers.container.PrebidServerContainer import org.prebid.server.functional.util.PBSUtils import org.testcontainers.images.builder.Transferable +import spock.lang.PendingFeature import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.bidder.BidderName.GENERIC @@ -73,6 +74,7 @@ class ProfileSpec extends BaseSpec { private static final String NO_IMP_PROFILE_MESSAGE = "No imp profiles for ids [%s] were found" private static final String NO_REQUEST_PROFILE_MESSAGE = "No request profiles for ids [%s] were found" private static final String NO_PROFILE_MESSAGE = "No profile found for id: %s" + private static final String REJECT_ACCOUNT_PROFILE_METRIC = "account.%s.profile.rejected" private static final String LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.limit_exceeded" private static final String MISSING_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.missing" @@ -82,15 +84,25 @@ class ProfileSpec extends BaseSpec { private static PrebidServerContainer pbsContainer private static PrebidServerService pbsWithStoredProfiles - private static RequestProfile requestProfile - private static ImpProfile impProfile + private static RequestProfile fileRequestProfile + private static RequestProfile fileRequestProfileWithEmptyMerge + private static ImpProfile fileImpProfile + private static ImpProfile fileImpProfileWithEmptyMerge def setupSpec() { pbsContainer = new PrebidServerContainer(FILESYSTEM_CONFIG + PROFILES_CONFIG) - requestProfile = RequestProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()) - impProfile = ImpProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()) - pbsContainer.withCopyToContainer(Transferable.of(encode(requestProfile)), "$PROFILES_PATH/${requestProfile.fileName}") - pbsContainer.withCopyToContainer(Transferable.of(encode(impProfile)), "$PROFILES_PATH/${impProfile.fileName}") + fileRequestProfile = RequestProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()) + fileImpProfile = ImpProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()) + pbsContainer.withCopyToContainer(Transferable.of(encode(fileRequestProfile)), "$PROFILES_PATH/${fileRequestProfile.fileName}") + pbsContainer.withCopyToContainer(Transferable.of(encode(fileImpProfile)), "$PROFILES_PATH/${fileImpProfile.fileName}") + fileRequestProfileWithEmptyMerge = RequestProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()).tap { + mergePrecedence = null + } + fileImpProfileWithEmptyMerge = ImpProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()).tap { + mergePrecedence = null + } + pbsContainer.withCopyToContainer(Transferable.of(encode(fileRequestProfileWithEmptyMerge)), "$PROFILES_PATH/${fileRequestProfileWithEmptyMerge.fileName}") + pbsContainer.withCopyToContainer(Transferable.of(encode(fileImpProfileWithEmptyMerge)), "$PROFILES_PATH/${fileImpProfileWithEmptyMerge.fileName}") pbsContainer.withFolder(REQUESTS_PATH) pbsContainer.withFolder(IMPS_PATH) pbsContainer.withFolder(RESPONSES_PATH) @@ -184,7 +196,7 @@ class ProfileSpec extends BaseSpec { def "PBS should use profile for request when it exist in filesystem"() { given: "Default bidRequest with request profile" - def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [requestProfile]) + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileRequestProfile]) when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) @@ -195,25 +207,25 @@ class ProfileSpec extends BaseSpec { and: "Bidder request should contain data from profile" verifyAll(response.ext.debug.resolvedRequest) { - it.site.id == requestProfile.body.site.id - it.site.name == requestProfile.body.site.name - it.site.domain == requestProfile.body.site.domain - it.site.cat == requestProfile.body.site.cat - it.site.sectionCat == requestProfile.body.site.sectionCat - it.site.pageCat == requestProfile.body.site.pageCat - it.site.page == requestProfile.body.site.page - it.site.ref == requestProfile.body.site.ref - it.site.search == requestProfile.body.site.search - it.site.keywords == requestProfile.body.site.keywords - it.site.ext.data == requestProfile.body.site.ext.data - - it.device.didsha1 == requestProfile.body.device.didsha1 - it.device.didmd5 == requestProfile.body.device.didmd5 - it.device.dpidsha1 == requestProfile.body.device.dpidsha1 - it.device.ifa == requestProfile.body.device.ifa - it.device.macsha1 == requestProfile.body.device.macsha1 - it.device.macmd5 == requestProfile.body.device.macmd5 - it.device.dpidmd5 == requestProfile.body.device.dpidmd5 + it.site.id == fileRequestProfile.body.site.id + it.site.name == fileRequestProfile.body.site.name + it.site.domain == fileRequestProfile.body.site.domain + it.site.cat == fileRequestProfile.body.site.cat + it.site.sectionCat == fileRequestProfile.body.site.sectionCat + it.site.pageCat == fileRequestProfile.body.site.pageCat + it.site.page == fileRequestProfile.body.site.page + it.site.ref == fileRequestProfile.body.site.ref + it.site.search == fileRequestProfile.body.site.search + it.site.keywords == fileRequestProfile.body.site.keywords + it.site.ext.data == fileRequestProfile.body.site.ext.data + + it.device.didsha1 == fileRequestProfile.body.device.didsha1 + it.device.didmd5 == fileRequestProfile.body.device.didmd5 + it.device.dpidsha1 == fileRequestProfile.body.device.dpidsha1 + it.device.ifa == fileRequestProfile.body.device.ifa + it.device.macsha1 == fileRequestProfile.body.device.macsha1 + it.device.macmd5 == fileRequestProfile.body.device.macmd5 + it.device.dpidmd5 == fileRequestProfile.body.device.dpidmd5 } and: "PBS shouldn't make bidder request" @@ -222,7 +234,7 @@ class ProfileSpec extends BaseSpec { def "PBS should use imp profile for request when it exist in filesystem"() { given: "Default bidRequest with request profile" - def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [impProfile]).tap { + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileImpProfile]).tap { it.imp.first.banner = null } as BidRequest @@ -235,16 +247,16 @@ class ProfileSpec extends BaseSpec { and: "Bidder request imp should contain data from profile" verifyAll(bidder.getBidderRequest(bidRequest.id).imp) { - it.id == [impProfile.body.id] - it.banner == [impProfile.body.banner] + it.id == [fileImpProfile.body.id] + it.banner == [fileImpProfile.body.banner] } } - // TODO in discussion + @PendingFeature def "PBS should emit error for request when same profile exist in filesystem and database"() { given: "Default bidRequest with request profile" - def profile = RequestProfile.getProfile(requestProfile.accountId, requestProfile.name) - def bidRequest = getRequestWithProfiles(requestProfile.accountId, [profile]) + def profile = RequestProfile.getProfile(fileRequestProfile.accountId, fileRequestProfile.id) + def bidRequest = getRequestWithProfiles(fileRequestProfile.accountId, [profile]) and: "Default account" def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) @@ -268,7 +280,7 @@ class ProfileSpec extends BaseSpec { and: "Reject metric should increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() - assert metrics[REJECT_ACCOUNT_PROFILE_METRIC.formatted(requestProfile.accountId)] == 1 + assert metrics[REJECT_ACCOUNT_PROFILE_METRIC.formatted(fileRequestProfile.accountId)] == 1 and: "Bidder request should contain data from original request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { @@ -277,7 +289,7 @@ class ProfileSpec extends BaseSpec { } } - def "PBS should prioritise original request data over profile when merge strategy #mergeStrategy"() { + def "PBS should skip invalid request profile from database when merge strategy #mergeStrategy"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def requestProfile = RequestProfile.getProfile(accountId).tap { @@ -300,8 +312,10 @@ class ProfileSpec extends BaseSpec { then: "No errors should be emitted in debug" assert !response.ext?.errors - // TODO in discussion -// assert !response.ext?.warnings + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.id)] and: "Bidder request should contain data from profile" verifyAll(response.ext.debug.resolvedRequest) { @@ -327,10 +341,10 @@ class ProfileSpec extends BaseSpec { } where: - mergeStrategy << [null, UNKNOWN, REQUEST] + mergeStrategy << [null, UNKNOWN] } - def "PBS should prioritise original imp data over profile when merge strategy #mergeStrategy"() { + def "PBS should skip invalid imp profile from database when merge strategy #mergeStrategy"() { given: "Default bidRequest with imp profile" def accountId = PBSUtils.randomNumber as String def impProfile = ImpProfile.getProfile(accountId).tap { @@ -351,14 +365,16 @@ class ProfileSpec extends BaseSpec { then: "No errors should be emitted in debug" assert !response.ext?.errors - // TODO in discussion -// assert !response.ext?.warnings + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(impProfile.id)] and: "Bidder request imp should contain data from profile" assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner where: - mergeStrategy << [null, UNKNOWN, REQUEST] + mergeStrategy << [null, UNKNOWN] } def "PBS should marge latest-specified profile when there marge conflict and different merge precedence present"() { @@ -632,7 +648,7 @@ class ProfileSpec extends BaseSpec { def secondImpProfile = ImpProfile.getProfile(accountId, secondImp) def thirdImpProfile = ImpProfile.getProfile(accountId, thirdImp) def bidRequest = getRequestWithProfiles(accountId, [firstImpProfile, secondImpProfile, firstRequestProfile, secondRequestProfile]).tap { - imp << new Imp(ext: new ImpExt(prebid: new ImpExtPrebid(profilesNames: [secondImpProfile, thirdImpProfile].name))) + imp << new Imp(ext: new ImpExt(prebid: new ImpExtPrebid(profilesNames: [secondImpProfile, thirdImpProfile].id))) } as BidRequest and: "Default account" @@ -720,9 +736,9 @@ class ProfileSpec extends BaseSpec { def bidRequest = BidRequest.getDefaultBidRequest().tap { it.imp.first.tap { it.banner.format = [Format.randomFormat] - it.ext.prebid.profilesNames = [impProfile.name] + it.ext.prebid.profilesNames = [impProfile.id] } - it.ext.prebid.profilesNames = [invalidProfileRequest.name, PBSUtils.randomString] + it.ext.prebid.profilesNames = [invalidProfileRequest.id, PBSUtils.randomString] it.site = Site.configFPDSite it.device = Device.default setAccountId(accountId) @@ -780,12 +796,12 @@ class ProfileSpec extends BaseSpec { assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner } - // TODO in discussion def "PBS should include data from storedBidResponses when it specified in profiles"() { given: "Default BidRequest with profile" def accountId = PBSUtils.randomNumber as String def storedResponseId = PBSUtils.randomNumber def impProfile = ImpProfile.getProfile(accountId).tap { + it.body.id = null it.body.ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } def bidRequest = getRequestWithProfiles(accountId, [impProfile]) @@ -813,12 +829,12 @@ class ProfileSpec extends BaseSpec { assert bidder.getRequestCount(bidRequest.id) == 0 } - // TODO in discussion def "PBS should include data from storedAuctionResponse when it specified in profiles"() { given: "Default basic BidRequest with profile" def accountId = PBSUtils.randomNumber as String def storedAuctionId = PBSUtils.randomNumber def impProfile = ImpProfile.getProfile(accountId).tap { + it.body.id = null it.body.ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedAuctionId) } def bidRequest = getRequestWithProfiles(accountId, [impProfile]) @@ -827,7 +843,7 @@ class ProfileSpec extends BaseSpec { profileImpDao.save(StoredProfileImp.getProfile(impProfile)) and: "Stored response in DB" - def storedAuctionResponse = SeatBid.getStoredResponse(BidRequest.defaultBidRequest) + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) def storedResponse = new StoredResponse(responseId: storedAuctionId, storedAuctionResponse: storedAuctionResponse) storedResponseDao.save(storedResponse) @@ -964,7 +980,7 @@ class ProfileSpec extends BaseSpec { } def requestProfile = RequestProfile.getProfile(accountId).tap { - it.body.ext.prebid.profilesNames = [innerRequestProfile.name] + it.body.ext.prebid.profilesNames = [innerRequestProfile.id] } def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { it.site = Site.configFPDSite @@ -1019,7 +1035,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def innerImpProfile = ImpProfile.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) def impProfile = ImpProfile.getProfile(accountId).tap { - it.body.ext.prebid.profilesNames = [innerImpProfile.name] + it.body.ext.prebid.profilesNames = [innerImpProfile.id] } def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { it.imp.first.banner = null @@ -1048,7 +1064,6 @@ class ProfileSpec extends BaseSpec { assert bidderImp.video == impProfile.body.video } - // TODO in discussion def "PBS shouldn't validate profiles and imp before margining"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String @@ -1058,10 +1073,7 @@ class ProfileSpec extends BaseSpec { it.body.banner.format.first.weight = null it.body.banner.format.first.height = height } - def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { - imp.first.banner.format.first.height = null - imp.first.banner.format.first.weight = weight - } as BidRequest + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) as BidRequest and: "Default account" def account = new Account(uuid: accountId, status: ACTIVE) @@ -1071,16 +1083,12 @@ class ProfileSpec extends BaseSpec { profileImpDao.save(StoredProfileImp.getProfile(impProfile)) when: "PBS processes auction request" - def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - - then: "No errors should be emitted in debug" - assert !response.ext?.errors - assert !response.ext?.warnings + pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - and: "Bidder request imp should contain data from profile" - def impBannerFormat = bidder.getBidderRequest(bidRequest.id).imp.first.banner.format - assert impBannerFormat.height == [height] - assert impBannerFormat.weight == [weight] + then: "PBs should throw error due to invalid request" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == 'Invalid request format: request.imp[0].banner.format[0] must define a valid "h" and "w" properties' } def "PBS shouldn't emit error or warnings when bidRequest contains multiple imps with same profile"() { @@ -1095,7 +1103,7 @@ class ProfileSpec extends BaseSpec { setAccountId(accountId) } as BidRequest bidRequest.imp.each { - it.ext.prebid.profilesNames = [impProfile.name] + it.ext.prebid.profilesNames = [impProfile.id] } and: "Default account" @@ -1150,13 +1158,13 @@ class ProfileSpec extends BaseSpec { mergePrecedence << [REQUEST, PROFILE] } - // TODO in discussion + @PendingFeature def "PBS should add error and metrics when imp name is invalid"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def impProfile = ImpProfile.getProfile(accountId, Imp.defaultImpression, invalidProfileName) def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [impProfile.name] + it.imp.first.ext.prebid.profilesNames = [impProfile.id] setAccountId(accountId) } @@ -1202,7 +1210,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def requestProfile = RequestProfile.getProfile(accountId) def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [requestProfile.name] + it.imp.first.ext.prebid.profilesNames = [requestProfile.id] it.site = Site.getRootFPDSite() it.device = Device.getDefault() setAccountId(accountId) @@ -1223,7 +1231,7 @@ class ProfileSpec extends BaseSpec { then: "PBS should emit proper warning" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.name)] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.id)] and: "Response should contain error" assert !response.ext?.errors @@ -1260,7 +1268,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def requestProfile = ImpProfile.getProfile(accountId) def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.ext.prebid.profilesNames = [requestProfile.name] + it.ext.prebid.profilesNames = [requestProfile.id] it.site = Site.getRootFPDSite() it.device = Device.getDefault() setAccountId(accountId) @@ -1281,7 +1289,7 @@ class ProfileSpec extends BaseSpec { then: "PBS should emit proper warning" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.name)] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.id)] and: "Response should contain error" assert !response.ext?.errors @@ -1405,10 +1413,10 @@ class ProfileSpec extends BaseSpec { private static BidRequest getRequestWithProfiles(String accountId, List profiles) { BidRequest.getDefaultBidRequest().tap { if (profiles.type.contains(ProfileType.IMP)) { - it.imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.name + it.imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.id } - it.imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.name - it.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.REQUEST }*.name + it.imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.id + it.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.REQUEST }*.id setAccountId(accountId) } } From 115ad69f62db4b957eb7a065855b0d65a885bab8 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Tue, 2 Sep 2025 16:04:28 +0300 Subject: [PATCH 06/13] Update after review --- .../ProfileMergePrecedenceConvert.groovy | 3 +- .../typeconverter/ProfileTypeConvert.groovy | 3 +- .../FileSystemAccountsConfig.groovy | 3 - .../model/filesystem/FilesystemAccount.groovy | 32 ---------- .../model/request/auction/ImpExtPrebid.groovy | 2 +- .../model/request/auction/Prebid.groovy | 2 +- .../service/PrebidServerService.groovy | 4 -- .../container/PrebidServerContainer.groovy | 59 ------------------- .../functional/tests/ProfileSpec.groovy | 34 +++++------ .../util/ObjectMapperWrapper.groovy | 15 +---- .../server/functional/db_mysql_schema.sql | 2 +- 11 files changed, 23 insertions(+), 136 deletions(-) delete mode 100644 src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccount.groovy diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy index d81ae72ad87..2b347cd9521 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy @@ -2,9 +2,8 @@ package org.prebid.server.functional.model.db.typeconverter import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence -import org.prebid.server.functional.util.ObjectMapperWrapper -class ProfileMergePrecedenceConvert implements AttributeConverter, ObjectMapperWrapper { +class ProfileMergePrecedenceConvert implements AttributeConverter { @Override String convertToDatabaseColumn(ProfileMergePrecedence profileMergePrecedence) { diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy index a25d22a3207..5c5565385f1 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy @@ -2,9 +2,8 @@ package org.prebid.server.functional.model.db.typeconverter import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.request.profile.ProfileType -import org.prebid.server.functional.util.ObjectMapperWrapper -class ProfileTypeConvert implements AttributeConverter, ObjectMapperWrapper { +class ProfileTypeConvert implements AttributeConverter { @Override String convertToDatabaseColumn(ProfileType profileMergePrecedence) { diff --git a/src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy index f01a2787d7e..6851ece5527 100644 --- a/src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy @@ -1,12 +1,9 @@ package org.prebid.server.functional.model.filesystem -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString import org.prebid.server.functional.model.config.AccountConfig @ToString(includeNames = true, ignoreNulls = true) -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class FileSystemAccountsConfig { List accounts diff --git a/src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccount.groovy b/src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccount.groovy deleted file mode 100644 index 529e4a3472d..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/filesystem/FilesystemAccount.groovy +++ /dev/null @@ -1,32 +0,0 @@ -package org.prebid.server.functional.model.filesystem - -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming -import groovy.transform.ToString -import org.prebid.server.functional.model.AccountStatus -import org.prebid.server.functional.model.config.AccountConfig - - -import java.sql.Timestamp - - -@ToString(includeNames = true, ignoreNulls = true) -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) -class FilesystemAccount { - - Integer id - String priceGranularity - Integer bannerCacheTtl - Integer videoCacheTtl - Boolean eventsEnabled - String tcfConfig - Integer truncateTargetAttr - String defaultIntegration - String analyticsConfig - String bidValidations - AccountStatus status - AccountConfig config - Integer updatedBy - String updatedByUser - Timestamp updated -} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy index 993389b576f..a20e3ea894d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy @@ -22,7 +22,7 @@ class ImpExtPrebid { String adUnitCode PrebidOptions options @JsonProperty("profiles") - List profilesNames + List profileNames static ImpExtPrebid getDefaultImpExtPrebid() { new ImpExtPrebid().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy index 26f0800b332..3ab6e7a6dbf 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy @@ -46,7 +46,7 @@ class Prebid { @JsonProperty("alternatebiddercodes") AlternateBidderCodes alternateBidderCodes @JsonProperty("profiles") - List profilesNames + List profileNames static class Channel { 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 a6c31644caa..b9c173baa54 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -431,10 +431,6 @@ class PrebidServerService implements ObjectMapperWrapper { }) } - Boolean copyToContainer(String content, String containerPath) { - pbsContainer.copyToContainer(content, containerPath) - } - Boolean isFileExist(String path) { pbsContainer.execInContainer("test", "-f", path).getExitCode() == 0 } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy index 71bf050ecb7..e13fcae3764 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy @@ -1,24 +1,12 @@ package org.prebid.server.functional.testcontainers.container -import com.github.dockerjava.api.DockerClient -import com.github.dockerjava.api.command.InspectContainerResponse -import org.apache.commons.compress.archivers.tar.TarArchiveEntry -import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream import org.prebid.server.functional.testcontainers.Dependencies import org.prebid.server.functional.testcontainers.PbsConfig import org.prebid.server.functional.util.SystemProperties -import org.testcontainers.DockerClientFactory import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.wait.strategy.Wait import org.testcontainers.images.builder.Transferable -import org.testcontainers.utility.MountableFile -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths - -import static java.nio.charset.StandardCharsets.UTF_8 import static org.prebid.server.functional.testcontainers.PbsConfig.DEFAULT_ENV class PrebidServerContainer extends GenericContainer { @@ -108,16 +96,6 @@ class PrebidServerContainer extends GenericContainer { .replace("]", "_") } - PrebidServerContainer copyToContainer(String content, String containerPath) { - String parentDir = getParentDirectory(containerPath) - createDirectoryInContainer(parentDir) - - byte[] archive = createTarArchive(content, getFileName(containerPath)) - copyArchiveToContainer(archive, parentDir) - - return self() - } - PrebidServerContainer withFolder(String containerPath) { this.withCopyToContainer( Transferable.of(new byte[0], 010755), @@ -126,43 +104,6 @@ class PrebidServerContainer extends GenericContainer { return this } - private static String getParentDirectory(String containerPath) { - Path path = Paths.get(containerPath) - return path.getParent().toString() - } - - private static String getFileName(String containerPath) { - return Paths.get(containerPath).getFileName().toString() - } - - private void createDirectoryInContainer(String directory) { - execInContainer("mkdir", "-p", directory) - } - - private static byte[] createTarArchive(String content, String fileName) { - byte[] data = content.getBytes(UTF_8) - ByteArrayOutputStream outputStream = new ByteArrayOutputStream() - TarArchiveOutputStream tar = new TarArchiveOutputStream(outputStream) - tar.longFileMode = TarArchiveOutputStream.LONGFILE_POSIX - - TarArchiveEntry entry = new TarArchiveEntry(fileName) - entry.size = data.length - tar.putArchiveEntry(entry) - tar.write(data) - tar.closeArchiveEntry() - tar.finish() - - return outputStream.toByteArray() - } - - private void copyArchiveToContainer(byte[] archive, String destination) { - DockerClient client = DockerClientFactory.instance().client() - client.copyArchiveToContainerCmd(getContainerId()) - .withTarInputStream(new ByteArrayInputStream(archive)) - .withRemotePath(destination) - .exec() - } - // This is a workaround for cases when container is killed mid-test due to OOM void refresh() { if (!running) { diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index 4adba6581e3..a1a7e60c89e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -648,7 +648,7 @@ class ProfileSpec extends BaseSpec { def secondImpProfile = ImpProfile.getProfile(accountId, secondImp) def thirdImpProfile = ImpProfile.getProfile(accountId, thirdImp) def bidRequest = getRequestWithProfiles(accountId, [firstImpProfile, secondImpProfile, firstRequestProfile, secondRequestProfile]).tap { - imp << new Imp(ext: new ImpExt(prebid: new ImpExtPrebid(profilesNames: [secondImpProfile, thirdImpProfile].id))) + imp << new Imp(ext: new ImpExt(prebid: new ImpExtPrebid(profileNames: [secondImpProfile, thirdImpProfile].id))) } as BidRequest and: "Default account" @@ -736,9 +736,9 @@ class ProfileSpec extends BaseSpec { def bidRequest = BidRequest.getDefaultBidRequest().tap { it.imp.first.tap { it.banner.format = [Format.randomFormat] - it.ext.prebid.profilesNames = [impProfile.id] + it.ext.prebid.profileNames = [impProfile.id] } - it.ext.prebid.profilesNames = [invalidProfileRequest.id, PBSUtils.randomString] + it.ext.prebid.profileNames = [invalidProfileRequest.id, PBSUtils.randomString] it.site = Site.configFPDSite it.device = Device.default setAccountId(accountId) @@ -873,7 +873,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def invalidProfileId = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [invalidProfileId] + it.imp.first.ext.prebid.profileNames = [invalidProfileId] it.site = new Site() it.device = null setAccountId(accountId) @@ -908,7 +908,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def invalidProfileId = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [invalidProfileId] + it.imp.first.ext.prebid.profileNames = [invalidProfileId] it.site = new Site() it.device = null setAccountId(accountId) @@ -938,7 +938,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def invalidProfileId = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [invalidProfileId] + it.imp.first.ext.prebid.profileNames = [invalidProfileId] it.site = new Site() it.device = null setAccountId(accountId) @@ -980,7 +980,7 @@ class ProfileSpec extends BaseSpec { } def requestProfile = RequestProfile.getProfile(accountId).tap { - it.body.ext.prebid.profilesNames = [innerRequestProfile.id] + it.body.ext.prebid.profileNames = [innerRequestProfile.id] } def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { it.site = Site.configFPDSite @@ -1035,7 +1035,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def innerImpProfile = ImpProfile.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) def impProfile = ImpProfile.getProfile(accountId).tap { - it.body.ext.prebid.profilesNames = [innerImpProfile.id] + it.body.ext.prebid.profileNames = [innerImpProfile.id] } def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { it.imp.first.banner = null @@ -1103,7 +1103,7 @@ class ProfileSpec extends BaseSpec { setAccountId(accountId) } as BidRequest bidRequest.imp.each { - it.ext.prebid.profilesNames = [impProfile.id] + it.ext.prebid.profileNames = [impProfile.id] } and: "Default account" @@ -1164,7 +1164,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def impProfile = ImpProfile.getProfile(accountId, Imp.defaultImpression, invalidProfileName) def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [impProfile.id] + it.imp.first.ext.prebid.profileNames = [impProfile.id] setAccountId(accountId) } @@ -1210,7 +1210,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def requestProfile = RequestProfile.getProfile(accountId) def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [requestProfile.id] + it.imp.first.ext.prebid.profileNames = [requestProfile.id] it.site = Site.getRootFPDSite() it.device = Device.getDefault() setAccountId(accountId) @@ -1268,7 +1268,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def requestProfile = ImpProfile.getProfile(accountId) def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.ext.prebid.profilesNames = [requestProfile.id] + it.ext.prebid.profileNames = [requestProfile.id] it.site = Site.getRootFPDSite() it.device = Device.getDefault() setAccountId(accountId) @@ -1326,7 +1326,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def invalidProfileId = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.imp.first.ext.prebid.profilesNames = [invalidProfileId] + it.imp.first.ext.prebid.profileNames = [invalidProfileId] setAccountId(accountId) } @@ -1360,7 +1360,7 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def invalidProfileId = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest().tap { - it.ext.prebid.profilesNames = [invalidProfileId] + it.ext.prebid.profileNames = [invalidProfileId] it.site = Site.getRootFPDSite() it.device = Device.getDefault() setAccountId(accountId) @@ -1413,10 +1413,10 @@ class ProfileSpec extends BaseSpec { private static BidRequest getRequestWithProfiles(String accountId, List profiles) { BidRequest.getDefaultBidRequest().tap { if (profiles.type.contains(ProfileType.IMP)) { - it.imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.id + it.imp.first.ext.prebid.profileNames = profiles.findAll { it.type == ProfileType.IMP }*.id } - it.imp.first.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.IMP }*.id - it.ext.prebid.profilesNames = profiles.findAll { it.type == ProfileType.REQUEST }*.id + it.imp.first.ext.prebid.profileNames = profiles.findAll { it.type == ProfileType.IMP }*.id + it.ext.prebid.profileNames = profiles.findAll { it.type == ProfileType.REQUEST }*.id setAccountId(accountId) } } diff --git a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy index a5da8c2cada..c071af92e01 100644 --- a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy @@ -12,8 +12,7 @@ trait ObjectMapperWrapper { private static final ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(NON_NULL) .registerModule(new ZonedDateTimeModule()) - private static final YAMLMapper yaml = new YAMLMapper().setSerializationInclusion(NON_NULL) - .registerModule(new ZonedDateTimeModule()) as YAMLMapper + private static final YAMLMapper yaml = new YAMLMapper().setSerializationInclusion(NON_NULL) as YAMLMapper private static final XmlMapper xmlMapper = new XmlMapper() final static String encode(Object object) { @@ -51,16 +50,4 @@ trait ObjectMapperWrapper { final static String encodeYaml(Object object) { yaml.writeValueAsString(object) } - - final static T decodeYaml(String yamlString, Class clazz) { - yaml.readValue(yamlString, clazz) - } - - final static T decodeYaml(String yamlString, TypeReference typeReference) { - yaml.readValue(yamlString, typeReference) - } - - final static T decodeYaml(InputStream inputStream, Class clazz) { - yaml.readValue(inputStream, clazz) - } } diff --git a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql index 2245e033dc9..786a575fe5b 100644 --- a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql +++ b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql @@ -44,7 +44,7 @@ CREATE TABLE stored_responses CREATE TABLE profiles ( - accountId varchar(64) NOT NULL, + accountId varchar(40) NOT NULL, profileId varchar(128) NOT NULL, profile json, mergePrecedence enum ('request', 'profile', 'unknown'), From 525ec4c8749e21eff7a20573f8ce1bf615cea9b4 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Wed, 3 Sep 2025 16:18:09 +0300 Subject: [PATCH 07/13] Update after review --- .../model/request/profile/ImpProfile.groovy | 15 +- .../request/profile/RequestProfile.groovy | 18 +- .../functional/tests/ProfileSpec.groovy | 206 +++--------------- .../util/ObjectMapperWrapper.groovy | 4 +- 4 files changed, 54 insertions(+), 189 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy index bd73f5efee5..2d622167ba1 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy @@ -9,15 +9,18 @@ import static ProfileMergePrecedence.PROFILE @ToString(includeNames = true, ignoreNulls = true) class ImpProfile extends Profile { - static getProfile(String accountId, + static ImpProfile getProfile(String accountId, Imp imp = Imp.defaultImpression, String name = PBSUtils.randomString, ProfileMergePrecedence mergePrecedence = PROFILE) { - new ImpProfile(accountId: accountId, - id: name, - type: ProfileType.IMP, - mergePrecedence: mergePrecedence, - body: imp) + new ImpProfile().tap { + it.accountId = accountId + it.id = name + it.type = ProfileType.IMP + it.mergePrecedence = mergePrecedence + it.body = imp + it.accountId = accountId + } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy index 414c7bbda93..8a047383e0b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy @@ -11,10 +11,11 @@ import static ProfileMergePrecedence.PROFILE @ToString(includeNames = true, ignoreNulls = true) class RequestProfile extends Profile { - static getProfile(String accountId = PBSUtils.randomString, + static RequestProfile getProfile(String accountId = PBSUtils.randomString, String name = PBSUtils.randomString, ProfileMergePrecedence mergePrecedence = PROFILE) { BidRequest request = BidRequest.defaultBidRequest.tap { + it.id = null it.imp = null it.site = Site.configFPDSite it.device = Device.default @@ -22,15 +23,18 @@ class RequestProfile extends Profile { getProfile(accountId, request, name, mergePrecedence) } - static getProfile(String accountId, + static RequestProfile getProfile(String accountId, BidRequest request, String name = PBSUtils.randomString, ProfileMergePrecedence mergePrecedence = PROFILE) { - new RequestProfile(accountId: accountId, - id: name, - type: ProfileType.REQUEST, - mergePrecedence: mergePrecedence, - body: request) + new RequestProfile().tap { + it.accountId = accountId + it.id = name + it.type = ProfileType.REQUEST + it.mergePrecedence = mergePrecedence + it.body = request + it.accountId = accountId + } } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index a1a7e60c89e..b895bcf6af3 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -62,20 +62,17 @@ class ProfileSpec extends BaseSpec { ] private static final Map PROFILES_CONFIG = [ - 'adapters.openx.enabled' : "true", 'auction.profiles.fail-on-unknown': "false", 'auction.profiles.limit' : LIMIT_HOST_PROFILE.toString(), 'settings.database.profiles-query': "SELECT accountId, profileId, profile, mergePrecedence, type FROM profiles " + "WHERE profileId in (%REQUEST_ID_LIST%, %IMP_ID_LIST%)".toString()] - private static final String REJECT_ERROR_MESSAGE = 'replace' private static final String LIMIT_ERROR_MESSAGE = 'Profiles exceeded the limit.' - private static final String INVALID_REQEUST_PREFIX = 'Invalid request format: Error during processing profiles: ' + private static final String INVALID_REQUEST_PREFIX = 'Invalid request format: Error during processing profiles: ' private static final String NO_IMP_PROFILE_MESSAGE = "No imp profiles for ids [%s] were found" private static final String NO_REQUEST_PROFILE_MESSAGE = "No request profiles for ids [%s] were found" private static final String NO_PROFILE_MESSAGE = "No profile found for id: %s" - private static final String REJECT_ACCOUNT_PROFILE_METRIC = "account.%s.profile.rejected" private static final String LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.limit_exceeded" private static final String MISSING_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.missing" @@ -124,10 +121,6 @@ class ProfileSpec extends BaseSpec { def requestProfile = RequestProfile.getProfile(accountId) def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profile in database" profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) @@ -139,7 +132,7 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request should contain data from profile" - verifyAll(response.ext.debug.resolvedRequest) { + verifyAll(bidder.getBidderRequest(bidRequest.id)) { it.site.id == requestProfile.body.site.id it.site.name == requestProfile.body.site.name it.site.domain == requestProfile.body.site.domain @@ -160,9 +153,6 @@ class ProfileSpec extends BaseSpec { it.device.macmd5 == requestProfile.body.device.macmd5 it.device.dpidmd5 == requestProfile.body.device.dpidmd5 } - - and: "PBS shouldn't make bidder request" - assert !bidder.getBidderRequests(bidRequest.id) } def "PBS should use imp profile for request when it exist in database"() { @@ -173,10 +163,6 @@ class ProfileSpec extends BaseSpec { it.imp.first.banner = null } as BidRequest - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profile in database" profileImpDao.save(StoredProfileImp.getProfile(impProfile)) @@ -206,7 +192,7 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request should contain data from profile" - verifyAll(response.ext.debug.resolvedRequest) { + verifyAll(bidder.getBidderRequest(bidRequest.id)) { it.site.id == fileRequestProfile.body.site.id it.site.name == fileRequestProfile.body.site.name it.site.domain == fileRequestProfile.body.site.domain @@ -227,9 +213,6 @@ class ProfileSpec extends BaseSpec { it.device.macmd5 == fileRequestProfile.body.device.macmd5 it.device.dpidmd5 == fileRequestProfile.body.device.dpidmd5 } - - and: "PBS shouldn't make bidder request" - assert !bidder.getBidderRequests(bidRequest.id) } def "PBS should use imp profile for request when it exist in filesystem"() { @@ -252,44 +235,7 @@ class ProfileSpec extends BaseSpec { } } - @PendingFeature - def "PBS should emit error for request when same profile exist in filesystem and database"() { - given: "Default bidRequest with request profile" - def profile = RequestProfile.getProfile(fileRequestProfile.accountId, fileRequestProfile.id) - def bidRequest = getRequestWithProfiles(fileRequestProfile.accountId, [profile]) - - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - - and: "Default profile in database" - profileRequestDao.save(StoredProfileRequest.getProfile(profile)) - - when: "PBS processes auction request" - def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - - then: "PBS should emit proper warning" - assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [REJECT_ERROR_MESSAGE] - - and: "Response should contain error" - assert !response.ext?.errors - - and: "PBS log should contain error" - assert pbsWithStoredProfiles.isContainLogsByValue(REJECT_ERROR_MESSAGE) - - and: "Reject metric should increments" - def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() - assert metrics[REJECT_ACCOUNT_PROFILE_METRIC.formatted(fileRequestProfile.accountId)] == 1 - - and: "Bidder request should contain data from original request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - it.site == bidRequest.site - it.device == bidRequest.device - } - } - - def "PBS should skip invalid request profile from database when merge strategy #mergeStrategy"() { + def "PBS should set merge strategy to default profile without error for profile profile when merge strategy #mergeStrategy"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def requestProfile = RequestProfile.getProfile(accountId).tap { @@ -312,13 +258,10 @@ class ProfileSpec extends BaseSpec { then: "No errors should be emitted in debug" assert !response.ext?.errors - - then: "PBS should emit proper warning" - assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.id)] + assert !response.ext?.warnings and: "Bidder request should contain data from profile" - verifyAll(response.ext.debug.resolvedRequest) { + verifyAll(bidder.getBidderRequest(bidRequest.id)) { it.site.id == bidRequest.site.id it.site.name == bidRequest.site.name it.site.domain == bidRequest.site.domain @@ -344,7 +287,7 @@ class ProfileSpec extends BaseSpec { mergeStrategy << [null, UNKNOWN] } - def "PBS should skip invalid imp profile from database when merge strategy #mergeStrategy"() { + def "PBS should set merge strategy to default profile without error for imp profile when merge strategy #mergeStrategy"() { given: "Default bidRequest with imp profile" def accountId = PBSUtils.randomNumber as String def impProfile = ImpProfile.getProfile(accountId).tap { @@ -353,10 +296,6 @@ class ProfileSpec extends BaseSpec { } def bidRequest = getRequestWithProfiles(accountId, [impProfile]) - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profile in database" profileImpDao.save(StoredProfileImp.getProfile(impProfile)) @@ -365,10 +304,7 @@ class ProfileSpec extends BaseSpec { then: "No errors should be emitted in debug" assert !response.ext?.errors - - then: "PBS should emit proper warning" - assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] - assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(impProfile.id)] + assert !response.ext?.warnings and: "Bidder request imp should contain data from profile" assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner @@ -377,7 +313,7 @@ class ProfileSpec extends BaseSpec { mergeStrategy << [null, UNKNOWN] } - def "PBS should marge latest-specified profile when there marge conflict and different merge precedence present"() { + def "PBS should merge latest-specified profile when there merge conflict and different merge precedence present"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String firstProfile.accountId = accountId @@ -395,9 +331,6 @@ class ProfileSpec extends BaseSpec { profileRequestDao.save(StoredProfileRequest.getProfile(firstProfile)) profileRequestDao.save(StoredProfileRequest.getProfile(secondProfile)) - and: "Flash metrics" - flushMetrics(pbsWithStoredProfiles) - when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) @@ -407,7 +340,7 @@ class ProfileSpec extends BaseSpec { and: "Bidder request should contain data from profiles" def mergedRequest = [firstProfile, secondProfile].find { it.mergePrecedence == PROFILE }.body - verifyAll(response.ext.debug.resolvedRequest) { + verifyAll(bidder.getBidderRequest(bidRequest.id)) { it.site.id == mergedRequest.site.id it.site.name == mergedRequest.site.name it.site.domain == mergedRequest.site.domain @@ -429,16 +362,13 @@ class ProfileSpec extends BaseSpec { it.device.dpidmd5 == mergedRequest.device.dpidmd5 } - and: "PBS shouldn't make bidder request" - assert !bidder.getBidderRequests(bidRequest.id) - where: firstProfile | secondProfile RequestProfile.getProfile().tap { mergePrecedence = REQUEST } | RequestProfile.getProfile() RequestProfile.getProfile() | RequestProfile.getProfile().tap { mergePrecedence = REQUEST } } - def "PBS should marge first-specified profile with request merge precedence when there marge conflict"() { + def "PBS should merge first-specified profile with request merge precedence when there merge conflict"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def firstRequestProfile = RequestProfile.getProfile(accountId).tap { @@ -453,10 +383,6 @@ class ProfileSpec extends BaseSpec { } def bidRequest = getRequestWithProfiles(accountId, [firstRequestProfile, secondRequestProfile]) - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profiles in database" profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) @@ -469,7 +395,7 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request should contain data from profile" - verifyAll(response.ext.debug.resolvedRequest) { + verifyAll(bidder.getBidderRequest(bidRequest.id)) { it.site.id == firstRequestProfile.body.site.id it.site.name == firstRequestProfile.body.site.name it.site.domain == firstRequestProfile.body.site.domain @@ -491,7 +417,7 @@ class ProfileSpec extends BaseSpec { } } - def "PBS should marge latest-specified profile with profile merge precedence when there marge conflict"() { + def "PBS should merge latest-specified profile with profile merge precedence when there merge conflict"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def firstRequestProfile = RequestProfile.getProfile(accountId).tap { @@ -504,10 +430,6 @@ class ProfileSpec extends BaseSpec { } def bidRequest = getRequestWithProfiles(accountId, [firstRequestProfile, secondRequestProfile]) - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profiles in database" profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) @@ -520,7 +442,7 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request should contain data from profile" - verifyAll(response.ext.debug.resolvedRequest) { + verifyAll(bidder.getBidderRequest(bidRequest.id)) { it.site.id == secondRequestProfile.body.site.id it.site.name == secondRequestProfile.body.site.name it.site.domain == secondRequestProfile.body.site.domain @@ -541,9 +463,6 @@ class ProfileSpec extends BaseSpec { it.device.macmd5 == secondRequestProfile.body.device.macmd5 it.device.dpidmd5 == secondRequestProfile.body.device.dpidmd5 } - - and: "PBS shouldn't make bidder request" - assert !bidder.getBidderRequests(bidRequest.id) } def "PBS should prioritise profile for request and emit warning when request is overloaded by profiles"() { @@ -562,10 +481,6 @@ class ProfileSpec extends BaseSpec { def impProfile = ImpProfile.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) def bidRequest = getRequestWithProfiles(accountId, [impProfile, firstRequestProfile, secondRequestProfile]) - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profiles in database" profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) @@ -589,7 +504,7 @@ class ProfileSpec extends BaseSpec { assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 and: "Bidder request should contain data from profile" - def bidderRequest = response.ext.debug.resolvedRequest + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll(bidderRequest) { it.site.id == profileSite.id it.site.name == profileSite.name @@ -612,9 +527,6 @@ class ProfileSpec extends BaseSpec { it.device.dpidmd5 == profileDevice.dpidmd5 } - and: "PBS shouldn't make bidder request" - assert !bidder.getBidderRequests(bidRequest.id) - and: "Bidder imp should contain original data from request" assert verifyAll(bidderRequest.imp) { it.banner == bidRequest.imp.banner @@ -680,7 +592,7 @@ class ProfileSpec extends BaseSpec { assert !metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] and: "Bidder request should contain data from profiles" - def bidderRequest = response.ext.debug.resolvedRequest + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll(bidderRequest) { it.site.id == profileSite.id it.site.name == profileSite.name @@ -702,9 +614,6 @@ class ProfileSpec extends BaseSpec { it.device.dpidmd5 == profileDevice.dpidmd5 } - and: "PBS shouldn't make bidder request" - assert !bidder.getBidderRequests(bidRequest.id) - and: "Bidder imp should contain data from specified profiles" def firstBidderImpBanner = bidderRequest.imp.first.banner verifyAll(firstBidderImpBanner) { @@ -744,10 +653,6 @@ class ProfileSpec extends BaseSpec { setAccountId(accountId) } - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profiles in database" profileRequestDao.save(StoredProfileRequest.getProfile(invalidProfileRequest)) profileImpDao.save(StoredProfileImp.getProfile(impProfile)) @@ -769,7 +674,7 @@ class ProfileSpec extends BaseSpec { assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 and: "Bidder request should contain data from original request" - def bidderRequest = response.ext.debug.resolvedRequest + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll(bidderRequest) { it.site.id == bidRequest.site.id it.site.name == bidRequest.site.name @@ -865,9 +770,8 @@ class ProfileSpec extends BaseSpec { def "PBS should fail auction when fail-on-unknown-profile enabled and profile is missing"() { given: "PBS with profiles.fail-on-unknown config" - def failOnUnknownProfilesConfig = new HashMap<>(PROFILES_CONFIG) - failOnUnknownProfilesConfig["auction.profiles.fail-on-unknown"] = "true" - def prebidServerService = pbsServiceFactory.getService(failOnUnknownProfilesConfig) + def prebidServerService = pbsServiceFactory.getService(PROFILES_CONFIG + + ['auction.profiles.fail-on-unknown': 'true']) and: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String @@ -879,12 +783,6 @@ class ProfileSpec extends BaseSpec { setAccountId(accountId) } - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - - and: "Flash metrics" - flushMetrics(prebidServerService) when: "PBS processes auction request" prebidServerService.sendAuctionRequest(bidRequest) @@ -892,17 +790,15 @@ class ProfileSpec extends BaseSpec { then: "PBs should throw error due to invalid profile" def exception = thrown(PrebidServerException) assert exception.statusCode == 400 - assert exception.responseBody == INVALID_REQEUST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) + assert exception.responseBody == INVALID_REQUEST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) cleanup: "Stop and remove pbs container" - pbsServiceFactory.removeContainer(failOnUnknownProfilesConfig) + pbsServiceFactory.removeContainer(PROFILES_CONFIG + ['auction.profiles.fail-on-unknown': 'true']) } def "PBS should fail auction when fail-on-unknown-profile default and profile is missing"() { given: "PBS without profiles.fail-on-unknown config" - def failOnUnknownProfilesConfig = new HashMap<>(PROFILES_CONFIG) - failOnUnknownProfilesConfig.remove("auction.profiles.fail-on-unknown") - def prebidServerService = pbsServiceFactory.getService(failOnUnknownProfilesConfig) + def prebidServerService = pbsServiceFactory.getService(PROFILES_CONFIG + ['auction.profiles.fail-on-unknown': null]) and: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String @@ -914,23 +810,16 @@ class ProfileSpec extends BaseSpec { setAccountId(accountId) } - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - - and: "Flash metrics" - flushMetrics(prebidServerService) - when: "PBS processes auction request" prebidServerService.sendAuctionRequest(bidRequest) then: "PBs should throw error due to invalid profile" def exception = thrown(PrebidServerException) assert exception.statusCode == 400 - assert exception.responseBody == INVALID_REQEUST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) + assert exception.responseBody == INVALID_REQUEST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) cleanup: "Stop and remove pbs container" - pbsServiceFactory.removeContainer(failOnUnknownProfilesConfig) + pbsServiceFactory.removeContainer(PROFILES_CONFIG + ['auction.profiles.fail-on-unknown': null]) } def "PBS should prioritise fail-on-unknown-profile from account over host config"() { @@ -959,7 +848,7 @@ class ProfileSpec extends BaseSpec { then: "PBs should throw error due to invalid profile" def exception = thrown(PrebidServerException) assert exception.statusCode == 400 - assert exception.responseBody == INVALID_REQEUST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) + assert exception.responseBody == INVALID_REQUEST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) and: "Missing metric should increments" def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() @@ -1003,8 +892,8 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request should contain data from profile" - def bidderRequest = response.ext.debug.resolvedRequest - verifyAll(response.ext.debug.resolvedRequest) { + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { it.site.id == requestProfile.body.site.id it.site.name == requestProfile.body.site.name it.site.domain == requestProfile.body.site.domain @@ -1041,10 +930,6 @@ class ProfileSpec extends BaseSpec { it.imp.first.banner = null } as BidRequest - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profiles in database" profileImpDao.save(StoredProfileImp.getProfile(innerImpProfile)) profileImpDao.save(StoredProfileImp.getProfile(impProfile)) @@ -1106,10 +991,6 @@ class ProfileSpec extends BaseSpec { it.ext.prebid.profileNames = [impProfile.id] } - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profile in database" profileImpDao.save(StoredProfileImp.getProfile(impProfile)) @@ -1129,6 +1010,7 @@ class ProfileSpec extends BaseSpec { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def bidRequestProfile = BidRequest.defaultBidRequest.tap { + it.id = null it.imp.first.banner.format = [Format.randomFormat] } def requestProfile = RequestProfile.getProfile(accountId, @@ -1137,10 +1019,6 @@ class ProfileSpec extends BaseSpec { mergePrecedence) def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profile in database" profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) @@ -1152,7 +1030,7 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.warnings and: "Bidder request should contain data from profile" - assert response.ext.debug.resolvedRequest.imp.banner == bidRequest.imp.banner + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner where: mergePrecedence << [REQUEST, PROFILE] @@ -1168,10 +1046,6 @@ class ProfileSpec extends BaseSpec { setAccountId(accountId) } - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Flash metrics" flushMetrics(pbsWithStoredProfiles) @@ -1216,10 +1090,6 @@ class ProfileSpec extends BaseSpec { setAccountId(accountId) } - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profile in database" profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) @@ -1241,7 +1111,7 @@ class ProfileSpec extends BaseSpec { assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 and: "Bidder request should contain data from profile" - verifyAll(response.ext.debug.resolvedRequest) { + verifyAll(bidder.getBidderRequest(bidRequest.id)) { it.site.id == bidRequest.site.id it.site.name == bidRequest.site.name it.site.domain == bidRequest.site.domain @@ -1274,10 +1144,6 @@ class ProfileSpec extends BaseSpec { setAccountId(accountId) } - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profile in database" profileImpDao.save(StoredProfileImp.getProfile(requestProfile)) @@ -1299,7 +1165,7 @@ class ProfileSpec extends BaseSpec { assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 and: "Bidder request should contain data from profile" - verifyAll(response.ext.debug.resolvedRequest) { + verifyAll(bidder.getBidderRequest(bidRequest.id)) { it.site.id == bidRequest.site.id it.site.name == bidRequest.site.name it.site.domain == bidRequest.site.domain @@ -1330,10 +1196,6 @@ class ProfileSpec extends BaseSpec { setAccountId(accountId) } - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Flash metrics" flushMetrics(pbsWithStoredProfiles) @@ -1366,10 +1228,6 @@ class ProfileSpec extends BaseSpec { setAccountId(accountId) } - and: "Default account" - def account = new Account(uuid: bidRequest.accountId, status: ACTIVE) - accountDao.save(account) - and: "Flash metrics" flushMetrics(pbsWithStoredProfiles) @@ -1388,7 +1246,7 @@ class ProfileSpec extends BaseSpec { assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 and: "Bidder request should contain data from profile" - verifyAll(response.ext.debug.resolvedRequest) { + verifyAll(bidder.getBidderRequest(bidRequest.id)) { it.site.id == bidRequest.site.id it.site.name == bidRequest.site.name it.site.domain == bidRequest.site.domain diff --git a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy index c071af92e01..15106b6a55f 100644 --- a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy @@ -12,7 +12,7 @@ trait ObjectMapperWrapper { private static final ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(NON_NULL) .registerModule(new ZonedDateTimeModule()) - private static final YAMLMapper yaml = new YAMLMapper().setSerializationInclusion(NON_NULL) as YAMLMapper + private static final YAMLMapper yamlMapper = new YAMLMapper().setSerializationInclusion(NON_NULL) as YAMLMapper private static final XmlMapper xmlMapper = new XmlMapper() final static String encode(Object object) { @@ -48,6 +48,6 @@ trait ObjectMapperWrapper { } final static String encodeYaml(Object object) { - yaml.writeValueAsString(object) + yamlMapper.writeValueAsString(object) } } From 72a77e0e34f142bb31f078132d5206cd040634cb Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Wed, 3 Sep 2025 16:21:06 +0300 Subject: [PATCH 08/13] Update after review --- .../org/prebid/server/functional/tests/ProfileSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index b895bcf6af3..a926ba6624d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -635,7 +635,7 @@ class ProfileSpec extends BaseSpec { assert !secondBidderImpBanner.btype } - def "PBS should include invalid or missing profiles into limit count"() { + def "PBS should count invalid or missing profiles towards the limit"() { given: "Default bidRequest with request profiles" def accountId = PBSUtils.randomNumber as String def invalidProfileRequest = RequestProfile.getProfile(accountId).tap { From 7c29c756eaf8ed98f01575dc122f4245b70d74dc Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Thu, 4 Sep 2025 09:54:10 +0300 Subject: [PATCH 09/13] Update after review --- .../model/request/profile/ImpProfile.groovy | 6 +-- .../request/profile/RequestProfile.groovy | 10 ++--- .../functional/tests/ProfileSpec.groovy | 44 ++++++------------- 3 files changed, 22 insertions(+), 38 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy index 2d622167ba1..bb67216e124 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy @@ -10,9 +10,9 @@ import static ProfileMergePrecedence.PROFILE class ImpProfile extends Profile { static ImpProfile getProfile(String accountId, - Imp imp = Imp.defaultImpression, - String name = PBSUtils.randomString, - ProfileMergePrecedence mergePrecedence = PROFILE) { + Imp imp = Imp.defaultImpression, + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { new ImpProfile().tap { it.accountId = accountId diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy index 8a047383e0b..a4f22cdebe2 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy @@ -12,8 +12,8 @@ import static ProfileMergePrecedence.PROFILE class RequestProfile extends Profile { static RequestProfile getProfile(String accountId = PBSUtils.randomString, - String name = PBSUtils.randomString, - ProfileMergePrecedence mergePrecedence = PROFILE) { + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { BidRequest request = BidRequest.defaultBidRequest.tap { it.id = null it.imp = null @@ -24,9 +24,9 @@ class RequestProfile extends Profile { } static RequestProfile getProfile(String accountId, - BidRequest request, - String name = PBSUtils.randomString, - ProfileMergePrecedence mergePrecedence = PROFILE) { + BidRequest request, + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { new RequestProfile().tap { it.accountId = accountId diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index a926ba6624d..4fb318fbe83 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -127,7 +127,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -169,7 +169,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -187,7 +187,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -224,7 +224,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -246,17 +246,13 @@ class ProfileSpec extends BaseSpec { it.device = Device.default } as BidRequest - and: "Default account" - def account = new Account(uuid: accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profile in database" profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest as BidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -302,7 +298,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -323,10 +319,6 @@ class ProfileSpec extends BaseSpec { it.device = Device.default } - and: "Default account" - def account = new Account(uuid: accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profiles in database" profileRequestDao.save(StoredProfileRequest.getProfile(firstProfile)) profileRequestDao.save(StoredProfileRequest.getProfile(secondProfile)) @@ -334,7 +326,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -390,7 +382,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -437,7 +429,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -583,7 +575,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -876,10 +868,6 @@ class ProfileSpec extends BaseSpec { it.device = Device.default } as BidRequest - and: "Default account" - def account = new Account(uuid: accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profiles in database" profileRequestDao.save(StoredProfileRequest.getProfile(innerRequestProfile)) profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) @@ -887,7 +875,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -937,7 +925,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -960,10 +948,6 @@ class ProfileSpec extends BaseSpec { } def bidRequest = getRequestWithProfiles(accountId, [impProfile]) as BidRequest - and: "Default account" - def account = new Account(uuid: accountId, status: ACTIVE) - accountDao.save(account) - and: "Default profile in database" profileImpDao.save(StoredProfileImp.getProfile(impProfile)) @@ -997,7 +981,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings @@ -1025,7 +1009,7 @@ class ProfileSpec extends BaseSpec { when: "PBS processes auction request" def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) - then: "No errors should be emitted in debug" + then: "Response should not contain errors and warnings" assert !response.ext?.errors assert !response.ext?.warnings From a678bee24ea3cd38fb9e60f82510e4727f4db8b6 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Thu, 4 Sep 2025 10:20:09 +0300 Subject: [PATCH 10/13] Update after review --- .../request/profile/ProfileMergePrecedence.groovy | 12 ++++++++++-- .../server/functional/tests/ProfileSpec.groovy | 9 ++++----- .../org/prebid/server/functional/db_mysql_schema.sql | 4 ++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy index 8d6b40203d2..47eaad93ebe 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy @@ -6,11 +6,19 @@ import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) enum ProfileMergePrecedence { - REQUEST, PROFILE, UNKNOWN + EMPTY(""), + REQUEST("request"), + PROFILE("profile") + + private final String value + + ProfileMergePrecedence(String value) { + this.value = value + } @JsonValue String getValue() { - name().toLowerCase() + value } static ProfileMergePrecedence forValue(String value) { diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index 4fb318fbe83..e4d02f53561 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -36,9 +36,9 @@ import spock.lang.PendingFeature import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.EMPTY import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.PROFILE import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.REQUEST -import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.UNKNOWN import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO class ProfileSpec extends BaseSpec { @@ -280,7 +280,7 @@ class ProfileSpec extends BaseSpec { } where: - mergeStrategy << [null, UNKNOWN] + mergeStrategy << [null, EMPTY] } def "PBS should set merge strategy to default profile without error for imp profile when merge strategy #mergeStrategy"() { @@ -306,7 +306,7 @@ class ProfileSpec extends BaseSpec { assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner where: - mergeStrategy << [null, UNKNOWN] + mergeStrategy << [null, EMPTY] } def "PBS should merge latest-specified profile when there merge conflict and different merge precedence present"() { @@ -317,7 +317,7 @@ class ProfileSpec extends BaseSpec { def bidRequest = getRequestWithProfiles(accountId, [firstProfile, secondProfile]).tap { it.site = Site.configFPDSite it.device = Device.default - } + } as BidRequest and: "Default profiles in database" profileRequestDao.save(StoredProfileRequest.getProfile(firstProfile)) @@ -940,7 +940,6 @@ class ProfileSpec extends BaseSpec { def "PBS shouldn't validate profiles and imp before margining"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String - def weight = PBSUtils.randomNumber def height = PBSUtils.randomNumber def impProfile = ImpProfile.getProfile(accountId).tap { it.body.banner.format.first.weight = null diff --git a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql index 786a575fe5b..ec7cf8f9239 100644 --- a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql +++ b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql @@ -47,8 +47,8 @@ CREATE TABLE profiles accountId varchar(40) NOT NULL, profileId varchar(128) NOT NULL, profile json, - mergePrecedence enum ('request', 'profile', 'unknown'), - type enum ('request', 'imp', 'unknown') + mergePrecedence enum ('request', 'profile', ''), + type enum ('request', 'imp') ); -- set session wait timeout to 1 minute From bc0545e9f2983014a6117370dfef52415897ec64 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Thu, 4 Sep 2025 13:16:43 +0300 Subject: [PATCH 11/13] Increase test coverage --- .../model/request/profile/ImpProfile.groovy | 2 +- .../profile/ProfileMergePrecedence.groovy | 5 +- .../model/request/profile/ProfileType.groovy | 11 +- .../request/profile/RequestProfile.groovy | 2 +- .../functional/tests/ProfileSpec.groovy | 161 +++++++++++++++++- .../server/functional/db_mysql_schema.sql | 4 +- 6 files changed, 169 insertions(+), 16 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy index bb67216e124..aa63358fb24 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy @@ -9,7 +9,7 @@ import static ProfileMergePrecedence.PROFILE @ToString(includeNames = true, ignoreNulls = true) class ImpProfile extends Profile { - static ImpProfile getProfile(String accountId, + static ImpProfile getProfile(String accountId = PBSUtils.randomNumber.toString(), Imp imp = Imp.defaultImpression, String name = PBSUtils.randomString, ProfileMergePrecedence mergePrecedence = PROFILE) { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy index 47eaad93ebe..80227015989 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy @@ -8,7 +8,8 @@ enum ProfileMergePrecedence { EMPTY(""), REQUEST("request"), - PROFILE("profile") + PROFILE("profile"), + UNKNOWN("unknown") private final String value @@ -18,7 +19,7 @@ enum ProfileMergePrecedence { @JsonValue String getValue() { - value + name().toLowerCase() } static ProfileMergePrecedence forValue(String value) { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy index 044ec77c271..6254976300f 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy @@ -6,7 +6,16 @@ import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) enum ProfileType { - REQUEST, IMP + EMPTY(""), + REQUEST("request"), + IMP("imp"), + UNKNOWN("unknown") + + private final String value + + ProfileType(String value) { + this.value = value + } @JsonValue String getValue() { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy index a4f22cdebe2..62d36bddc38 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy @@ -11,7 +11,7 @@ import static ProfileMergePrecedence.PROFILE @ToString(includeNames = true, ignoreNulls = true) class RequestProfile extends Profile { - static RequestProfile getProfile(String accountId = PBSUtils.randomString, + static RequestProfile getProfile(String accountId = PBSUtils.randomNumber.toString(), String name = PBSUtils.randomString, ProfileMergePrecedence mergePrecedence = PROFILE) { BidRequest request = BidRequest.defaultBidRequest.tap { diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index e4d02f53561..1bdd2b81cc6 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -19,6 +19,7 @@ import org.prebid.server.functional.model.request.auction.StoredAuctionResponse import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.request.profile.Profile import org.prebid.server.functional.model.request.profile.ImpProfile +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence import org.prebid.server.functional.model.request.profile.RequestProfile import org.prebid.server.functional.model.request.profile.ProfileType import org.prebid.server.functional.model.request.auction.Site @@ -36,7 +37,6 @@ import spock.lang.PendingFeature import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.EMPTY import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.PROFILE import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.REQUEST import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO @@ -235,11 +235,11 @@ class ProfileSpec extends BaseSpec { } } - def "PBS should set merge strategy to default profile without error for profile profile when merge strategy #mergeStrategy"() { + def "PBS should set merge strategy to default profile without error for request profile when merge strategy is empty in database"() { given: "Default bidRequest with request profile" def accountId = PBSUtils.randomNumber as String def requestProfile = RequestProfile.getProfile(accountId).tap { - it.mergePrecedence = mergeStrategy + it.mergePrecedence = null } def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { it.site = Site.configFPDSite @@ -278,16 +278,51 @@ class ProfileSpec extends BaseSpec { it.device.macmd5 == bidRequest.device.macmd5 it.device.dpidmd5 == bidRequest.device.dpidmd5 } + } - where: - mergeStrategy << [null, EMPTY] + def "PBS should set merge strategy to default profile without error for request profile when merge strategy is empty in filesystem"() { + given: "Default bidRequest with request profile" + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileRequestProfileWithEmptyMerge]).tap { + it.site = Site.configFPDSite + it.device = Device.default + } as BidRequest + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest as BidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + it.site.ext.data == bidRequest.site.ext.data + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } } - def "PBS should set merge strategy to default profile without error for imp profile when merge strategy #mergeStrategy"() { + def "PBS should set merge strategy to default profile without error for imp profile when merge strategy is empty in database"() { given: "Default bidRequest with imp profile" def accountId = PBSUtils.randomNumber as String def impProfile = ImpProfile.getProfile(accountId).tap { - it.mergePrecedence = mergeStrategy + it.mergePrecedence = null it.body.banner.format = [Format.randomFormat] } def bidRequest = getRequestWithProfiles(accountId, [impProfile]) @@ -304,9 +339,21 @@ class ProfileSpec extends BaseSpec { and: "Bidder request imp should contain data from profile" assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + } - where: - mergeStrategy << [null, EMPTY] + def "PBS should set merge strategy to default profile without error for imp profile when merge strategy is empty in filesystem"() { + given: "Default bidRequest with imp profile" + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileImpProfileWithEmptyMerge]) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner } def "PBS should merge latest-specified profile when there merge conflict and different merge precedence present"() { @@ -1251,6 +1298,102 @@ class ProfileSpec extends BaseSpec { } } + def "PBS should emit error and metrics when imp profile have invalid data"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [invalidProfile.id] + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_IMP_PROFILE_MESSAGE.formatted(invalidProfile.id)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request imp should contain data from original imp" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + + where: + invalidProfile << [ + ImpProfile.getProfile().tap { it.type = ProfileType.EMPTY}, + ImpProfile.getProfile().tap { it.type = ProfileType.UNKNOWN}, + ImpProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.EMPTY}, + ImpProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.UNKNOWN}, + ] + } + + def "PBS should emit error and metrics when request profile have invalid data"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profileNames = [invalidProfileId] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_REQUEST_PROFILE_MESSAGE.formatted(invalidProfileId)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } + + where: + invalidProfile << [ + RequestProfile.getProfile().tap { it.type = ProfileType.EMPTY}, + RequestProfile.getProfile().tap { it.type = ProfileType.UNKNOWN}, + RequestProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.EMPTY}, + RequestProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.UNKNOWN}, + ] + } + private static BidRequest getRequestWithProfiles(String accountId, List profiles) { BidRequest.getDefaultBidRequest().tap { if (profiles.type.contains(ProfileType.IMP)) { diff --git a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql index ec7cf8f9239..0f4d026337f 100644 --- a/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql +++ b/src/test/resources/org/prebid/server/functional/db_mysql_schema.sql @@ -47,8 +47,8 @@ CREATE TABLE profiles accountId varchar(40) NOT NULL, profileId varchar(128) NOT NULL, profile json, - mergePrecedence enum ('request', 'profile', ''), - type enum ('request', 'imp') + mergePrecedence enum ('request', 'profile', '', 'unknown'), + type enum ('request', 'imp', '', 'unknown') ); -- set session wait timeout to 1 minute From 773a8592bdc1af65ca920296143c8f68ef9f9485 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Thu, 4 Sep 2025 13:27:53 +0300 Subject: [PATCH 12/13] Update after review --- .../org/prebid/server/functional/tests/ProfileSpec.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index 1bdd2b81cc6..ace8286eab2 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -250,7 +250,7 @@ class ProfileSpec extends BaseSpec { profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) when: "PBS processes auction request" - def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest as BidRequest) + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) then: "Response should not contain errors and warnings" assert !response.ext?.errors @@ -288,7 +288,7 @@ class ProfileSpec extends BaseSpec { } as BidRequest when: "PBS processes auction request" - def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest as BidRequest) + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) then: "Response should not contain errors and warnings" assert !response.ext?.errors From 0504dcf1033023b2f3024234521e09eee59f8e22 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Thu, 4 Sep 2025 14:06:50 +0300 Subject: [PATCH 13/13] Update after review --- .../functional/tests/ProfileSpec.groovy | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy index ace8286eab2..f0c39cf4d73 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -96,6 +96,10 @@ class ProfileSpec extends BaseSpec { mergePrecedence = null } fileImpProfileWithEmptyMerge = ImpProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()).tap { + body.banner.tap { + btype = [PBSUtils.randomNumber] + format = [Format.randomFormat] + } mergePrecedence = null } pbsContainer.withCopyToContainer(Transferable.of(encode(fileRequestProfileWithEmptyMerge)), "$PROFILES_PATH/${fileRequestProfileWithEmptyMerge.fileName}") @@ -243,7 +247,6 @@ class ProfileSpec extends BaseSpec { } def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { it.site = Site.configFPDSite - it.device = Device.default } as BidRequest and: "Default profile in database" @@ -256,7 +259,7 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.errors assert !response.ext?.warnings - and: "Bidder request should contain data from profile" + and: "Bidder request should contain data from original request when data is present" verifyAll(bidder.getBidderRequest(bidRequest.id)) { it.site.id == bidRequest.site.id it.site.name == bidRequest.site.name @@ -269,14 +272,17 @@ class ProfileSpec extends BaseSpec { it.site.search == bidRequest.site.search it.site.keywords == bidRequest.site.keywords it.site.ext.data == bidRequest.site.ext.data + } - it.device.didsha1 == bidRequest.device.didsha1 - it.device.didmd5 == bidRequest.device.didmd5 - it.device.dpidsha1 == bidRequest.device.dpidsha1 - it.device.ifa == bidRequest.device.ifa - it.device.macsha1 == bidRequest.device.macsha1 - it.device.macmd5 == bidRequest.device.macmd5 - it.device.dpidmd5 == bidRequest.device.dpidmd5 + and: "Bidder request should contain data from profile when data is empty" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.device.didsha1 == requestProfile.body.device.didsha1 + it.device.didmd5 == requestProfile.body.device.didmd5 + it.device.dpidsha1 == requestProfile.body.device.dpidsha1 + it.device.ifa == requestProfile.body.device.ifa + it.device.macsha1 == requestProfile.body.device.macsha1 + it.device.macmd5 == requestProfile.body.device.macmd5 + it.device.dpidmd5 == requestProfile.body.device.dpidmd5 } } @@ -284,7 +290,6 @@ class ProfileSpec extends BaseSpec { given: "Default bidRequest with request profile" def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileRequestProfileWithEmptyMerge]).tap { it.site = Site.configFPDSite - it.device = Device.default } as BidRequest when: "PBS processes auction request" @@ -294,7 +299,7 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.errors assert !response.ext?.warnings - and: "Bidder request should contain data from profile" + and: "Bidder request should contain data from original request when data is present" verifyAll(bidder.getBidderRequest(bidRequest.id)) { it.site.id == bidRequest.site.id it.site.name == bidRequest.site.name @@ -307,14 +312,17 @@ class ProfileSpec extends BaseSpec { it.site.search == bidRequest.site.search it.site.keywords == bidRequest.site.keywords it.site.ext.data == bidRequest.site.ext.data + } - it.device.didsha1 == bidRequest.device.didsha1 - it.device.didmd5 == bidRequest.device.didmd5 - it.device.dpidsha1 == bidRequest.device.dpidsha1 - it.device.ifa == bidRequest.device.ifa - it.device.macsha1 == bidRequest.device.macsha1 - it.device.macmd5 == bidRequest.device.macmd5 - it.device.dpidmd5 == bidRequest.device.dpidmd5 + and: "Bidder request should contain data from original request when data is empty" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.device.didsha1 == fileRequestProfileWithEmptyMerge.body.device.didsha1 + it.device.didmd5 == fileRequestProfileWithEmptyMerge.body.device.didmd5 + it.device.dpidsha1 == fileRequestProfileWithEmptyMerge.body.device.dpidsha1 + it.device.ifa == fileRequestProfileWithEmptyMerge.body.device.ifa + it.device.macsha1 == fileRequestProfileWithEmptyMerge.body.device.macsha1 + it.device.macmd5 == fileRequestProfileWithEmptyMerge.body.device.macmd5 + it.device.dpidmd5 == fileRequestProfileWithEmptyMerge.body.device.dpidmd5 } } @@ -323,7 +331,10 @@ class ProfileSpec extends BaseSpec { def accountId = PBSUtils.randomNumber as String def impProfile = ImpProfile.getProfile(accountId).tap { it.mergePrecedence = null - it.body.banner.format = [Format.randomFormat] + body.banner.tap { + btype = [PBSUtils.randomNumber] + format = [Format.randomFormat] + } } def bidRequest = getRequestWithProfiles(accountId, [impProfile]) @@ -337,8 +348,12 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.errors assert !response.ext?.warnings - and: "Bidder request imp should contain data from profile" - assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + and: "Bidder request imp should contain data from profile when data is present" + def bidderImpBanner = bidder.getBidderRequest(bidRequest.id).imp.banner.first + assert bidderImpBanner.format == bidRequest.imp.first.banner.format + + and: "Bidder request should contain data from profile when data is empty" + assert bidderImpBanner.btype == impProfile.body.banner.btype } def "PBS should set merge strategy to default profile without error for imp profile when merge strategy is empty in filesystem"() { @@ -352,8 +367,12 @@ class ProfileSpec extends BaseSpec { assert !response.ext?.errors assert !response.ext?.warnings - and: "Bidder request imp should contain data from profile" - assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + and: "Bidder request imp should contain data from profile when data is present" + def bidderImpBanner = bidder.getBidderRequest(bidRequest.id).imp.banner.first + assert bidderImpBanner.format == bidRequest.imp.first.banner.format + + and: "Bidder request should contain data from profile when data is empty" + assert bidderImpBanner.btype == fileImpProfileWithEmptyMerge.body.banner.btype } def "PBS should merge latest-specified profile when there merge conflict and different merge precedence present"() {