Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functional tests for account PriceGranularity config #3517

Open
wants to merge 2 commits into
base: price-granularity-defaults-fix
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import org.prebid.server.functional.model.response.auction.MediaType
@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy)
class AccountAuctionConfig {

String priceGranularity
PriceGranularityType priceGranularity
Integer bannerCacheTtl
Integer videoCacheTtl
Integer truncateTargetAttr
Expand All @@ -28,7 +28,7 @@ class AccountAuctionConfig {
PrivacySandbox privacySandbox

@JsonProperty("price_granularity")
String priceGranularitySnakeCase
PriceGranularityType priceGranularitySnakeCase
@JsonProperty("banner_cache_ttl")
Integer bannerCacheTtlSnakeCase
@JsonProperty("video_cache_ttl")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.prebid.server.functional.model.config

import com.fasterxml.jackson.annotation.JsonValue
import org.prebid.server.functional.model.request.auction.Range

enum PriceGranularityType {

LOW(2, [Range.getDefault(5, 0.5)]),
MEDIUM(2, [Range.getDefault(20, 0.1)]),
MED(2, [Range.getDefault(20, 0.1)]),
HIGH(2, [Range.getDefault(20, 0.01)]),
AUTO(2, [Range.getDefault(5, 0.05), Range.getDefault(10, 0.1), Range.getDefault(20, 0.5)]),
DENSE(2, [Range.getDefault(3, 0.01), Range.getDefault(8, 0.05), Range.getDefault(20, 0.5)]),
UNKNOWN(null, [])

final Integer precision
final List<Range> ranges

PriceGranularityType(Integer precision, List<Range> ranges) {
this.precision = precision
marki1an marked this conversation as resolved.
Show resolved Hide resolved
this.ranges = ranges
}

@JsonValue
String toLowerCase() {
return name().toLowerCase()
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
package org.prebid.server.functional.model.request.auction

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import org.prebid.server.functional.model.config.PriceGranularityType

@ToString(includeNames = true, ignoreNulls = true)
@EqualsAndHashCode
class PriceGranularity {

Integer precision
List<Range> ranges

static PriceGranularity getDefault(PriceGranularityType granularity) {
new PriceGranularity(precision: granularity.precision, ranges: granularity.ranges)
}

static PriceGranularity getDefault() {
getDefault(PriceGranularityType.MED)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package org.prebid.server.functional.model.request.auction

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

@ToString(includeNames = true, ignoreNulls = true)
@EqualsAndHashCode
class Range {

BigDecimal max
BigDecimal increment

static Range getDefault(Integer max, BigDecimal increment) {
new Range(max: max, increment: increment)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.prebid.server.functional.model.bidder.Generic
import org.prebid.server.functional.model.bidder.Openx
import org.prebid.server.functional.model.config.AccountAuctionConfig
import org.prebid.server.functional.model.config.AccountConfig
import org.prebid.server.functional.model.config.PriceGranularityType
import org.prebid.server.functional.model.db.Account
import org.prebid.server.functional.model.db.StoredRequest
import org.prebid.server.functional.model.db.StoredResponse
Expand All @@ -17,13 +18,17 @@ import org.prebid.server.functional.model.request.auction.StoredBidResponse
import org.prebid.server.functional.model.request.auction.Targeting
import org.prebid.server.functional.model.response.auction.Bid
import org.prebid.server.functional.model.response.auction.BidResponse
import org.prebid.server.functional.service.PrebidServerException
import org.prebid.server.functional.service.PrebidServerService
import org.prebid.server.functional.util.PBSUtils

import java.math.RoundingMode
import java.nio.charset.StandardCharsets

import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST
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.config.PriceGranularityType.UNKNOWN
import static org.prebid.server.functional.model.response.auction.ErrorType.TARGETING
import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer

Expand Down Expand Up @@ -1121,6 +1126,230 @@ class TargetingSpec extends BaseSpec {
assert targeting["hb_env"] == HB_ENV_AMP
}

def "PBS auction should throw error when price granularity from original request is empty"() {
given: "Default bidRequest with empty price granularity"
def bidRequest = BidRequest.defaultBidRequest.tap {
ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN))
}

and: "Account in the DB"
def account = createAccountWithPriceGranularity(bidRequest.accountId, PBSUtils.getRandomEnum(PriceGranularityType))
accountDao.save(account)

when: "PBS processes auction request"
defaultPbsService.sendAuctionRequest(bidRequest)

then: "Request should fail with an error"
def exception = thrown(PrebidServerException)
assert exception.statusCode == BAD_REQUEST.code()
assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied'
}

def "PBS auction should prioritize price granularity from original request over account config"() {
given: "Default bidRequest with price granularity"
def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity as PriceGranularityType)
def bidRequest = BidRequest.defaultBidRequest.tap {
ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity)
}

and: "Account in the DB"
def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: PBSUtils.getRandomEnum(PriceGranularityType))
def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig)
def account = new Account(uuid: bidRequest.accountId, config: accountConfig)
accountDao.save(account)

when: "PBS processes auction request"
defaultPbsService.sendAuctionRequest(bidRequest)

then: "BidderRequest should include price granularity from bidRequest"
def bidderRequest = bidder.getBidderRequest(bidRequest.id)
assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity

where:
priceGranularity << (PriceGranularityType.values() - UNKNOWN as List<PriceGranularityType>)
}

def "PBS amp should prioritize price granularity from original request over account config"() {
given: "Default AmpRequest"
def ampRequest = AmpRequest.defaultAmpRequest

and: "Default ampStoredRequest"
def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity)
def ampStoredRequest = BidRequest.defaultBidRequest.tap {
ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity)
setAccountId(ampRequest.account)
}

and: "Create and save stored request into DB"
def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
storedRequestDao.save(storedRequest)

and: "Account in the DB"
def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType))
accountDao.save(account)

when: "PBS processes auction request"
defaultPbsService.sendAmpRequest(ampRequest)

then: "BidderRequest should include price granularity from bidRequest"
def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id)
assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity

where:
priceGranularity << (PriceGranularityType.values() - UNKNOWN as List<PriceGranularityType>)
}

def "PBS auction should include price granularity from account config when original request doesn't contain price granularity"() {
given: "Default basic BidRequest"
def bidRequest = BidRequest.defaultBidRequest.tap {
ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false)
}

and: "Account in the DB"
def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity)
accountDao.save(account)

when: "PBS processes auction request"
defaultPbsService.sendAuctionRequest(bidRequest)

then: "BidderRequest should include price granularity from account config"
def bidderRequest = bidder.getBidderRequest(bidRequest.id)
assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity)

where:
priceGranularity << (PriceGranularityType.values() - UNKNOWN as List<PriceGranularityType>)
}

def "PBS auction should include price granularity from account config with different name case when original request doesn't contain price granularity"() {
given: "Default basic BidRequest"
def bidRequest = BidRequest.defaultBidRequest.tap {
ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false)
}

and: "Account in the DB"
def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity)
accountDao.save(account)

when: "PBS processes auction request"
defaultPbsService.sendAuctionRequest(bidRequest)

then: "BidderRequest should include price granularity from account config"
def bidderRequest = bidder.getBidderRequest(bidRequest.id)
assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity)

where:
priceGranularity << (PriceGranularityType.values() - UNKNOWN as List<PriceGranularityType>)
}

def "PBS auction should include price granularity from default account config when original request doesn't contain price granularity"() {
given: "Pbs with default account that include privacySandbox configuration"
def priceGranularity = PBSUtils.getRandomEnum(PriceGranularityType, [UNKNOWN])
def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity)
def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig)
def pbsService = pbsServiceFactory.getService(
["settings.default-account-config": encode(accountConfig)])

and: "Default basic BidRequest"
def bidRequest = BidRequest.defaultBidRequest.tap {
ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false)
}

when: "PBS processes auction request"
pbsService.sendAuctionRequest(bidRequest)

then: "BidderRequest should include price granularity from account config"
def bidderRequest = bidder.getBidderRequest(bidRequest.id)
assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity)
}

def "PBS auction should include include default price granularity when original request and account config doesn't contain price granularity"() {
given: "Default basic BidRequest"
def bidRequest = BidRequest.defaultBidRequest.tap {
ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false)
}

and: "Account in the DB"
def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig)
def account = new Account(uuid: bidRequest.accountId, config: accountConfig)
accountDao.save(account)

when: "PBS processes auction request"
defaultPbsService.sendAuctionRequest(bidRequest)

then: "BidderRequest should include default price granularity"
def bidderRequest = bidder.getBidderRequest(bidRequest.id)
assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.default

where:
accountAuctionConfig << [
null,
new AccountAuctionConfig(),
new AccountAuctionConfig(priceGranularity: UNKNOWN)]
}

def "PBS amp should throw error when price granularity from original request is empty"() {
given: "Default AmpRequest"
def ampRequest = AmpRequest.defaultAmpRequest

and: "Default ampStoredRequest with empty price granularity"
def ampStoredRequest = BidRequest.defaultBidRequest.tap {
ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN))
setAccountId(ampRequest.account)
}

and: "Create and save stored request into DB"
def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
storedRequestDao.save(storedRequest)


and: "Account in the DB"
def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType))
accountDao.save(account)

when: "PBS processes auction request"
defaultPbsService.sendAmpRequest(ampRequest)

then: "Request should fail with an error"
def exception = thrown(PrebidServerException)
assert exception.statusCode == BAD_REQUEST.code()
assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied'
}

def "PBS amp should include price granularity from account config when original request doesn't contain price granularity"() {
given: "Default AmpRequest"
def ampRequest = AmpRequest.defaultAmpRequest

and: "Default ampStoredRequest"
def ampStoredRequest = BidRequest.defaultBidRequest.tap {
ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false)
setAccountId(ampRequest.account)
}

and: "Account in the DB"
def account = createAccountWithPriceGranularity(ampRequest.account, priceGranularity)
accountDao.save(account)

and: "Create and save stored request into DB"
def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
storedRequestDao.save(storedRequest)

when: "PBS processes amp request"
defaultPbsService.sendAmpRequest(ampRequest)

then: "BidderRequest should include price granularity from account config"
def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id)
assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity)

where:
priceGranularity << (PriceGranularityType.values() - UNKNOWN as List<PriceGranularityType>)
}

def createAccountWithPriceGranularity(String accountId, PriceGranularityType priceGranularity) {
def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity)
def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig)
return new Account(uuid: accountId, config: accountConfig)
}

private static PrebidServerService getEnabledWinBidsPbsService() {
pbsServiceFactory.getService(["auction.cache.only-winning-bids": "true"])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import java.time.Instant
import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400
import static org.prebid.server.functional.model.Currency.EUR
import static org.prebid.server.functional.model.Currency.JPY
import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE
import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER
import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ class PBSUtils implements ObjectMapperWrapper {
getRandomDecimal(min, max).setScale(scale, HALF_UP)
}

static <T extends Enum<T>> T getRandomEnum(Class<T> anEnum) {
def values = anEnum.enumConstants
values[getRandomNumber(0, values.length - 1)]
static <T extends Enum<T>> T getRandomEnum(Class<T> anEnum, List<T> exclude = []) {
def values = anEnum.enumConstants.findAll { !exclude.contains(it) } as T[]
values[getRandomNumber(0, values.size() - 1)]
}

static String convertCase(String input, Case caseType) {
Expand Down