From 1f474541528ae7c64f7a0b95dbdb1b4f8be4029a Mon Sep 17 00:00:00 2001 From: antonbabak Date: Thu, 25 Jan 2024 11:19:45 +0100 Subject: [PATCH 1/7] Add DSA fields and validation --- .../server/auction/ExchangeService.java | 21 +- .../requestfactory/AmpRequestFactory.java | 2 +- .../requestfactory/AuctionRequestFactory.java | 3 +- .../down/BidRequestOrtb26To25Converter.java | 4 +- .../up/BidRequestOrtb25To26Converter.java | 5 +- .../server/bidder/rubicon/RubiconBidder.java | 4 +- .../bidder/yahooads/YahooAdsBidder.java | 6 +- .../proto/openrtb/ext/request/ExtRegs.java | 6 + .../proto/openrtb/ext/request/ExtRegsDsa.java | 33 ++ .../ext/request/ExtRegsDsaTransparency.java | 22 ++ .../validation/ResponseBidValidator.java | 18 +- .../creator/rule/GeoRuleCreatorTest.java | 2 +- .../infrastructure/rule/GeoRuleTest.java | 6 +- .../server/auction/ExchangeServiceTest.java | 53 +++- .../auction/gpp/AuctionGppServiceTest.java | 2 +- .../requestfactory/AmpRequestFactoryTest.java | 2 +- .../AuctionRequestFactoryTest.java | 8 +- .../BidRequestOrtb26To25ConverterTest.java | 32 +- .../up/BidRequestOrtb25To26ConverterTest.java | 42 ++- .../bidder/adnuntius/AdnuntiusBidderTest.java | 8 +- .../bidder/adtarget/AdtargetBidderTest.java | 2 +- .../adtelligent/AdtelligentBidderTest.java | 4 +- .../consumable/ConsumableBidderTest.java | 2 +- .../server/bidder/openx/OpenxBidderTest.java | 8 +- .../bidder/rubicon/RubiconBidderTest.java | 6 +- .../server/bidder/sovrn/SovrnBidderTest.java | 2 +- .../bidder/unicorn/UnicornBidderTest.java | 4 +- .../bidder/yahooads/YahooAdsBidderTest.java | 8 +- .../bidder/yieldlab/YieldlabBidderTest.java | 6 +- .../request/ProtobufRequestUtilsTest.java | 2 +- .../validation/ResponseBidValidatorTest.java | 287 ++++++++++++------ .../test-auction-generic-request.json | 22 +- .../test-auction-generic-response.json | 12 + .../test-cache-generic-request.json | 9 + .../test-generic-bid-request.json | 22 +- .../test-generic-bid-response.json | 12 + 36 files changed, 506 insertions(+), 181 deletions(-) create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 4e49587e16a..899c5268df3 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -12,6 +12,7 @@ import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Source; import com.iab.openrtb.request.SupplyChain; @@ -95,6 +96,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtDooh; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; @@ -167,6 +170,7 @@ public class ExchangeService { private static final Integer DEFAULT_MULTIBID_LIMIT_MAX = 9; private static final String EID_ALLOWED_FOR_ALL_BIDDERS = "*"; private static final BigDecimal THOUSAND = BigDecimal.valueOf(1000); + private static final Set DSA_REQUIRED = Set.of(2, 3); private final double logSamplingRate; private final int timeoutAdjustmentFactor; @@ -1517,8 +1521,12 @@ private AuctionParticipation validBidderResponse(AuctionParticipation auctionPar final String lineItemId = LineItemUtil.lineItemIdFrom(bid.getBid(), bidRequest.getImp(), mapper); maybeRecordInTxnLog(lineItemId, () -> txnLog.lineItemsReceivedFromBidder().get(bidder)); - final ValidationResult validationResult = - responseBidValidator.validate(bid, bidderResponse.getBidder(), auctionContext, aliases); + final ValidationResult validationResult = responseBidValidator.validate( + bid, + bidderResponse.getBidder(), + auctionContext, + aliases, + isDsaValidationRequired(bidRequest)); if (validationResult.hasWarnings() || validationResult.hasErrors()) { errors.add(makeValidationBidderError(bid.getBid(), validationResult)); @@ -1545,6 +1553,15 @@ private AuctionParticipation validBidderResponse(AuctionParticipation auctionPar return auctionParticipation.with(resultBidderResponse); } + private static Boolean isDsaValidationRequired(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getRegs()) + .map(Regs::getExt) + .map(ExtRegs::getDsa) + .map(ExtRegsDsa::getRequired) + .map(DSA_REQUIRED::contains) + .orElse(false); + } + private BidderError makeValidationBidderError(Bid bid, ValidationResult validationResult) { final String validationErrors = Stream.concat( validationResult.getErrors().stream().map(message -> "Error: " + message), diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index 0a91bbf8d59..0ba7b84fb75 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -289,7 +289,7 @@ private static Regs createRegs(ConsentParam consentParam, .usPrivacy(usPrivacy) .gppSid(gppSid) .gpp(gpp) - .ext(gpc != null ? ExtRegs.of(null, null, gpc) : null) + .ext(gpc != null ? ExtRegs.of(null, null, gpc, null) : null) .build() : null; } diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index 58cdb69cbf5..53b2b071d31 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -201,7 +201,8 @@ private Regs fillRegsWithValuesFromHttpRequest(Regs regs, HttpRequestContext htt .ext(ExtRegs.of( extRegs != null ? extRegs.getGdpr() : null, extRegs != null ? extRegs.getUsPrivacy() : null, - gpc)) + gpc, + extRegs != null ? extRegs.getDsa() : null)) .build(); } diff --git a/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java b/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java index 39554009bdf..e0198ac0d20 100644 --- a/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java +++ b/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java @@ -23,6 +23,7 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtSource; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -422,7 +423,8 @@ private static Regs modifyRegs(Regs regs) { final ExtRegs originalExtRegs = regs.getExt(); final String gpc = originalExtRegs != null ? originalExtRegs.getGpc() : null; - final ExtRegs extRegs = ExtRegs.of(gdpr, usPrivacy, gpc); + final ExtRegsDsa dsa = originalExtRegs != null ? originalExtRegs.getDsa() : null; + final ExtRegs extRegs = ExtRegs.of(gdpr, usPrivacy, gpc, dsa); copyProperties(originalExtRegs, extRegs); return regs.toBuilder() diff --git a/src/main/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26Converter.java b/src/main/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26Converter.java index afe3fc7ac55..56cdae70eb1 100644 --- a/src/main/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26Converter.java +++ b/src/main/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26Converter.java @@ -191,7 +191,7 @@ private static ExtRegs resolveRegsExt(ExtRegs extRegs) { return null; } - final ExtRegs modifiedExtRegs = ExtRegs.of(null, null, extRegs.getGpc()); + final ExtRegs modifiedExtRegs = ExtRegs.of(null, null, extRegs.getGpc(), extRegs.getDsa()); copyProperties(extRegs, modifiedExtRegs); return modifiedExtRegs; @@ -259,7 +259,8 @@ private static ExtUser nullIfEmpty(ExtUser ext) { } private static ExtRegs nullIfEmpty(ExtRegs ext) { - return allNull(ext.getGdpr(), ext.getUsPrivacy(), ext.getGpc()) && MapUtils.isEmpty(ext.getProperties()) + return allNull(ext.getGdpr(), ext.getUsPrivacy(), ext.getGpc(), ext.getDsa()) + && MapUtils.isEmpty(ext.getProperties()) ? null : ext; } diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index 7943b3b04b2..a8e071e42dd 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -92,6 +92,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors; import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidMultiBid; @@ -1514,7 +1515,8 @@ private static Regs makeRegs(Regs regs) { final ExtRegs originalExtRegs = regs.getExt(); final String gpc = originalExtRegs != null ? originalExtRegs.getGpc() : null; - final ExtRegs extRegs = copyProperties(originalExtRegs, ExtRegs.of(gdpr, usPrivacy, gpc)); + final ExtRegsDsa dsa = originalExtRegs != null ? originalExtRegs.getDsa() : null; + final ExtRegs extRegs = copyProperties(originalExtRegs, ExtRegs.of(gdpr, usPrivacy, gpc, dsa)); return regs.toBuilder() .gdpr(null) diff --git a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java index 0381c6f286d..9d35a90c69d 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -34,6 +34,7 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.yahooads.ExtImpYahooAds; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -184,7 +185,10 @@ private ExtRegs resolveExtRegs(Regs regs) { final String gpc = Optional.ofNullable(regs.getExt()) .map(ExtRegs::getGpc) .orElse(null); - final ExtRegs extRegs = ExtRegs.of(gdpr, usPrivacy, gpc); + final ExtRegsDsa dsa = Optional.ofNullable(regs.getExt()) + .map(ExtRegs::getDsa) + .orElse(null); + final ExtRegs extRegs = ExtRegs.of(gdpr, usPrivacy, gpc, dsa); extRegs.addProperty("gpp", TextNode.valueOf(gpp)); if (!CollectionUtils.isEmpty(gppSid)) { final ArrayNode gppArrayNode = mapper.mapper().createArrayNode(); diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegs.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegs.java index 720f5cbbbb8..ad4abdcec97 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegs.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegs.java @@ -33,4 +33,10 @@ public class ExtRegs extends FlexibleExtension { * should be set to 1 and where it is not present this property should not be present. */ String gpc; + + /** + * Allows for publishers to indicate that a transaction is subject to Digital Services Act (DSA) + * and whether they will render the required transparency information themselves + */ + ExtRegsDsa dsa; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java new file mode 100644 index 00000000000..3e2072cd053 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java @@ -0,0 +1,33 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.Value; + +import java.util.List; + +/** + * Defines the contract for bidrequest.regs.ext.dsa + */ +@Value(staticConstructor = "of") +public class ExtRegsDsa { + + /** + * Defines the contract for bidrequest.regs.ext.dsa.required + */ + Integer required; + + /** + * Defines the contract for bidrequest.regs.ext.dsa.pubrender + */ + Integer pubrender; + + /** + * Defines the contract for bidrequest.regs.ext.dsa.datatopub + */ + Integer datatopub; + + /** + * Defines the contract for bidrequest.regs.ext.dsa.transparency[] + */ + List transparency; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java new file mode 100644 index 00000000000..73d6f9d7997 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java @@ -0,0 +1,22 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.Value; + +import java.util.List; + +/** + * Defines the contract for bidrequest.regs.ext.dsa.transparency[i] + */ +@Value(staticConstructor = "of") +public class ExtRegsDsaTransparency { + + /** + * Defines the contract for bidrequest.regs.ext.dsa.transparency[i].domain + */ + String domain; + + /** + * Defines the contract for bidrequest.regs.ext.dsa.transparency[i].params[] + */ + List params; +} diff --git a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java index e619b35fb5d..52a53c08220 100644 --- a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java +++ b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Deal; @@ -60,6 +61,7 @@ public class ResponseBidValidator { private static final String PREBID_EXT = "prebid"; private static final String BIDDER_EXT = "bidder"; + private static final String DSA_EXT = "dsa"; private static final String DEALS_ONLY = "dealsonly"; @@ -90,7 +92,8 @@ public ResponseBidValidator(BidValidationEnforcement bannerMaxSizeEnforcement, public ValidationResult validate(BidderBid bidderBid, String bidder, AuctionContext auctionContext, - BidderAliases aliases) { + BidderAliases aliases, + boolean isDsaValidationEnabled) { final Bid bid = bidderBid.getBid(); final BidRequest bidRequest = auctionContext.getBidRequest(); @@ -102,13 +105,17 @@ public ValidationResult validate(BidderBid bidderBid, validateTypeSpecific(bidderBid, bidder); validateCurrency(bidderBid.getBidCurrency()); + if (isDsaValidationEnabled) { + validateDsaFor(bid); + } + final Imp correspondingImp = findCorrespondingImp(bid, bidRequest); if (bidderBid.getType() == BidType.banner) { warnings.addAll(validateBannerFields(bid, bidder, bidRequest, account, correspondingImp, aliases)); } if (dealsEnabled) { - validateDealsFor(bidderBid, auctionContext.getBidRequest(), bidder, aliases, warnings); + validateDealsFor(bidderBid, bidRequest, bidder, aliases, warnings); } warnings.addAll(validateSecureMarkup(bid, bidder, bidRequest, account, correspondingImp, aliases)); @@ -157,6 +164,13 @@ private static void validateCurrency(String currency) throws ValidationException } } + private void validateDsaFor(Bid bid) throws ValidationException { + final ObjectNode bidExt = bid.getExt(); + if (bidExt != null && (!bidExt.hasNonNull(DSA_EXT) || bidExt.get(DSA_EXT).isEmpty())) { + throw new ValidationException("Bid \"%s\" missing DSA", bid.getId()); + } + } + private Imp findCorrespondingImp(Bid bid, BidRequest bidRequest) throws ValidationException { return bidRequest.getImp().stream() .filter(imp -> Objects.equals(imp.getId(), bid.getImpid())) diff --git a/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/GeoRuleCreatorTest.java b/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/GeoRuleCreatorTest.java index c7bddd6d136..e3d4976924c 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/GeoRuleCreatorTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/GeoRuleCreatorTest.java @@ -80,7 +80,7 @@ public void fromShouldCreateExpectedRule() { private static BidRequest givenBidRequest(String country, String region, String gpc) { return BidRequest.builder() .device(Device.builder().geo(Geo.builder().country(country).region(region).build()).build()) - .regs(Regs.builder().ext(ExtRegs.of(null, null, gpc)).build()) + .regs(Regs.builder().ext(ExtRegs.of(null, null, gpc, null)).build()) .build(); } diff --git a/src/test/java/org/prebid/server/activity/infrastructure/rule/GeoRuleTest.java b/src/test/java/org/prebid/server/activity/infrastructure/rule/GeoRuleTest.java index 25637228a5f..f4ad90d22b2 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/rule/GeoRuleTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/rule/GeoRuleTest.java @@ -199,7 +199,7 @@ public void matchesShouldReturnTrueIfGpcMatched() { final ActivityInvocationPayload payload = BidRequestActivityInvocationPayload.of( null, BidRequest.builder() - .regs(Regs.builder().ext(ExtRegs.of(null, null, "2")).build()) + .regs(Regs.builder().ext(ExtRegs.of(null, null, "2", null)).build()) .build()); // when @@ -216,7 +216,7 @@ public void matchesShouldReturnFalseIfGpcNotMatched() { final ActivityInvocationPayload payload = BidRequestActivityInvocationPayload.of( null, BidRequest.builder() - .regs(Regs.builder().ext(ExtRegs.of(null, null, "1")).build()) + .regs(Regs.builder().ext(ExtRegs.of(null, null, "1", null)).build()) .build()); // when @@ -240,7 +240,7 @@ public void matchesShouldReturnExpectedResult() { ActivityInvocationPayloadImpl.of(ComponentType.BIDDER, "bidder"), BidRequest.builder() .device(Device.builder().geo(Geo.builder().country("country").region("region").build()).build()) - .regs(Regs.builder().ext(ExtRegs.of(null, null, "2")).build()) + .regs(Regs.builder().ext(ExtRegs.of(null, null, "2", null)).build()) .build()); // when diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 211985080ad..487824fd092 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -114,6 +114,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; import org.prebid.server.proto.openrtb.ext.request.ExtImpAuctionEnvironment; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; @@ -371,7 +373,8 @@ public void setUp() { given(mediaTypeProcessor.process(any(), anyString(), any(), any())) .willAnswer(invocation -> MediaTypeProcessingResult.succeeded(invocation.getArgument(0), emptyList())); - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); + given(responseBidValidator.validate(any(), any(), any(), any(), eq(false))) + .willReturn(ValidationResult.success()); given(currencyService.convertCurrency(any(), any(), any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); @@ -1790,7 +1793,7 @@ public void shouldTolerateResponseBidValidationErrors() { .auctiontimestamp(1000L) .build()))); - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.error( + given(responseBidValidator.validate(any(), any(), any(), any(), eq(false))).willReturn(ValidationResult.error( singletonList("bid validation warning"), "bid validation error")); @@ -1828,7 +1831,7 @@ public void shouldTolerateResponseBidValidationWarnings() { .auctiontimestamp(1000L) .build()))); - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success( + given(responseBidValidator.validate(any(), any(), any(), any(), eq(false))).willReturn(ValidationResult.success( singletonList("bid validation warning"))); givenBidResponseCreator(singletonList(Bid.builder().build())); @@ -1866,7 +1869,7 @@ public void shouldRejectBidIfCurrencyIsNotValid() { .auctiontimestamp(1000L) .build()))); - given(responseBidValidator.validate(any(), any(), any(), any())) + given(responseBidValidator.validate(any(), any(), any(), any(), eq(false))) .willReturn(ValidationResult.error("BidResponse currency is not valid: USDD")); final List bidderErrors = singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(), @@ -1886,6 +1889,41 @@ public void shouldRejectBidIfCurrencyIsNotValid() { .isEmpty(); } + @Test + public void shouldValidateBidResponsesDsaWhenRegsExtDsaRequiredFieldRequires() { + // given + givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( + givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build(), + "USDD")))); + + final ExtRegs extRegs = ExtRegs.of(1, "usPrivacy", "1", ExtRegsDsa.of(2, 2, 3, emptyList())); + final BidRequest bidRequest = givenBidRequest(singletonList( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), + builder -> builder + .ext(ExtRequest.of(ExtRequestPrebid.builder().auctiontimestamp(1000L).build())) + .regs(Regs.builder().ext(extRegs).build())); + + given(responseBidValidator.validate(any(), any(), any(), any(), eq(true))) + .willReturn(ValidationResult.error("Bid \"bidId1\" missing DSA")); + + final List bidderErrors = singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(), + "Bid \"bidId1\" missing DSA")); + givenBidResponseCreator(singletonMap("bidder1", bidderErrors)); + + // when + final AuctionContext result = target.holdAuction(givenRequestContext(bidRequest)).result(); + + // then + final BidResponse bidResponse = result.getBidResponse(); + final ExtBidResponse ext = bidResponse.getExt(); + assertThat(ext.getErrors()).hasSize(1) + .containsOnly(entry("bidder1", bidderErrors)); + assertThat(bidResponse.getSeatbid()) + .extracting(SeatBid::getBid) + .isEmpty(); + } + @Test public void shouldCreateRequestsFromImpsReturnedByStoredResponseProcessor() { // given @@ -4580,9 +4618,9 @@ public void shouldRecordLineItemMetricsInTransactionLog() { givenBidderBid(Bid.builder().impid("impId").dealid("dealId2").price(BigDecimal.ONE).build())))); willReturn(ValidationResult.success()).given(responseBidValidator) - .validate(argThat(bid -> bid.getBid().getDealid().equals("dealId1")), any(), any(), any()); + .validate(argThat(bid -> bid.getBid().getDealid().equals("dealId1")), any(), any(), any(), eq(false)); willReturn(ValidationResult.error("validation error")).given(responseBidValidator) - .validate(argThat(bid -> bid.getBid().getDealid().equals("dealId2")), any(), any(), any()); + .validate(argThat(bid -> bid.getBid().getDealid().equals("dealId2")), any(), any(), any(), eq(false)); final List deals = asList( Deal.builder() @@ -4748,7 +4786,8 @@ public void shouldReduceBidsHavingDealIdWithSameImpIdByBidderWithToleratingNotOb givenBidder(givenSingleSeatBid(bidderBid)); - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); + given(responseBidValidator.validate(any(), any(), any(), any(), eq(false))) + .willReturn(ValidationResult.success()); // when target.holdAuction(auctionContext); diff --git a/src/test/java/org/prebid/server/auction/gpp/AuctionGppServiceTest.java b/src/test/java/org/prebid/server/auction/gpp/AuctionGppServiceTest.java index e0a9bacbba7..8752af645d6 100644 --- a/src/test/java/org/prebid/server/auction/gpp/AuctionGppServiceTest.java +++ b/src/test/java/org/prebid/server/auction/gpp/AuctionGppServiceTest.java @@ -304,7 +304,7 @@ private static BidRequest givenLegacyBidRequest(String gpp, return BidRequest.builder() .user(User.builder().ext(ExtUser.builder().consent(consent).build()).build()) - .regs(Regs.builder().gpp(gpp).gppSid(gppSid).ext(ExtRegs.of(gdpr, usPrivacy, null)).build()) + .regs(Regs.builder().gpp(gpp).gppSid(gppSid).ext(ExtRegs.of(gdpr, usPrivacy, null, null)).build()) .build(); } diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java index 9765df01216..a81e971a019 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java @@ -1491,7 +1491,7 @@ public void shouldReturnBidRequestWithGpc() { // then assertThat(result.getRegs()).isEqualTo(Regs.builder() - .ext(ExtRegs.of(null, null, "1")) + .ext(ExtRegs.of(null, null, "1", null)) .build()); } diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index 35b7f2978ae..8507b9a1e94 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -46,6 +46,7 @@ import org.prebid.server.privacy.model.Privacy; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; @@ -255,9 +256,10 @@ public void shouldReturnFailedFutureIfRequestBodyCouldNotBeParsed() { @Test public void shouldFillBidRequestWithValuesFromHttpRequest() { // given + final ExtRegsDsa dsa = ExtRegsDsa.of(1, 2, 3, emptyList()); final BidRequest receivedBidRequest = BidRequest.builder() .regs(Regs.builder() - .ext(ExtRegs.of(0, "us_privacy", null)) + .ext(ExtRegs.of(0, "us_privacy", null, dsa)) .build()) .build(); @@ -274,8 +276,8 @@ public void shouldFillBidRequestWithValuesFromHttpRequest() { final BidRequest capturedRequest = captor.getValue(); assertThat(capturedRequest.getRegs()) .extracting(Regs::getExt) - .extracting(ExtRegs::getGdpr, ExtRegs::getUsPrivacy, ExtRegs::getGpc) - .containsExactly(0, "us_privacy", "1"); + .extracting(ExtRegs::getGdpr, ExtRegs::getUsPrivacy, ExtRegs::getGpc, ExtRegs::getDsa) + .containsExactly(0, "us_privacy", "1", dsa); } @Test diff --git a/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java b/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java index 357185dbe4c..8896a1b451f 100644 --- a/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java +++ b/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java @@ -23,10 +23,10 @@ import com.iab.openrtb.request.User; import com.iab.openrtb.request.UserAgent; import com.iab.openrtb.request.Video; -import org.junit.Before; import org.junit.Test; import org.prebid.server.VertxTest; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtSource; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -43,12 +43,7 @@ public class BidRequestOrtb26To25ConverterTest extends VertxTest { - private BidRequestOrtb26To25Converter converter; - - @Before - public void setUp() { - converter = new BidRequestOrtb26To25Converter(jacksonMapper); - } + private final BidRequestOrtb26To25Converter target = new BidRequestOrtb26To25Converter(jacksonMapper); @Test public void convertShouldMoveImpsRwdd() { @@ -60,7 +55,7 @@ public void convertShouldMoveImpsRwdd() { .ext(mapper.valueToTree(Map.of("prebid", Map.of("someField", "someValue")))))))); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -92,7 +87,7 @@ public void convertShouldMoveSourceSupplyChain() { .build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -113,6 +108,11 @@ public void convertShouldMoveSourceSupplyChain() { @Test public void convertShouldMoveRegsData() { // given + final Map dsaMap = Map.of( + "required", 1, + "pubrender", 2, + "datatopub", 3, + "transparency", emptyList()); final BidRequest bidRequest = givenBidRequest(request -> request.regs( Regs.builder() .gdpr(1) @@ -120,12 +120,13 @@ public void convertShouldMoveRegsData() { .ext(mapper.convertValue( Map.of( "someField", "someValue", - "gpc", "1"), + "gpc", "1", + "dsa", dsaMap), ExtRegs.class)) .build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -135,7 +136,8 @@ public void convertShouldMoveRegsData() { .extracting(Regs::getGdpr, Regs::getUsPrivacy) .containsOnlyNulls(); - final ExtRegs expectedRegsExt = ExtRegs.of(1, "usPrivacy", "1"); + final ExtRegsDsa dsa = ExtRegsDsa.of(1, 2, 3, emptyList()); + final ExtRegs expectedRegsExt = ExtRegs.of(1, "usPrivacy", "1", dsa); expectedRegsExt.addProperty("someField", TextNode.valueOf("someValue")); assertThat(regs) .extracting(Regs::getExt) @@ -153,7 +155,7 @@ public void convertShouldRemoveRegsGppData() { .build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -173,7 +175,7 @@ public void convertShouldMoveUserData() { .build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -259,7 +261,7 @@ public void convertShouldRemoveFieldsThatAreNotInOrtb25() { .cattax(1)); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result).satisfies(request -> { diff --git a/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java b/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java index 37cff395ee4..ef96ea8ba2b 100644 --- a/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java +++ b/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java @@ -9,7 +9,6 @@ import com.iab.openrtb.request.Source; import com.iab.openrtb.request.SupplyChain; import com.iab.openrtb.request.User; -import org.junit.Before; import org.junit.Test; import org.prebid.server.VertxTest; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; @@ -27,12 +26,7 @@ public class BidRequestOrtb25To26ConverterTest extends VertxTest { - private BidRequestOrtb25To26Converter converter; - - @Before - public void setUp() { - converter = new BidRequestOrtb25To26Converter(); - } + private final BidRequestOrtb25To26Converter target = new BidRequestOrtb25To26Converter(); @Test public void convertShouldMoveImpsRwddIfNeeded() { @@ -43,7 +37,7 @@ public void convertShouldMoveImpsRwddIfNeeded() { givenImp(imp -> imp.ext(extImp))))); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -68,7 +62,7 @@ public void convertShouldMoveSourceExtSupplyChainToSourceSupplyChainIfNotPresent Source.builder().ext(ExtSource.of(supplyChain)).build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -92,7 +86,7 @@ public void convertShouldNotChangeSourceSupplyChainIfPresent() { Source.builder().schain(supplyChain).ext(ExtSource.of(SupplyChain.of(1, null, null, null))).build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -112,10 +106,10 @@ public void convertShouldMoveRegsExtGdprToRegsGdprIfNotPresent() { // given final Integer gdpr = 1; final BidRequest bidRequest = givenBidRequest(request -> request.regs( - Regs.builder().ext(ExtRegs.of(gdpr, null, null)).build())); + Regs.builder().ext(ExtRegs.of(gdpr, null, null, null)).build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -135,10 +129,10 @@ public void convertShouldNotChangeRegsGdprIfPresent() { // given final Integer gdpr = 1; final BidRequest bidRequest = givenBidRequest(request -> request.regs( - Regs.builder().gdpr(gdpr).ext(ExtRegs.of(0, null, null)).build())); + Regs.builder().gdpr(gdpr).ext(ExtRegs.of(0, null, null, null)).build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -158,10 +152,10 @@ public void convertShouldMoveRegsExtUsPrivacyToRegsUsPrivacyIfNotPresent() { // given final String usPrivacy = "privacy"; final BidRequest bidRequest = givenBidRequest(request -> request.regs( - Regs.builder().ext(ExtRegs.of(null, usPrivacy, "1")).build())); + Regs.builder().ext(ExtRegs.of(null, usPrivacy, "1", null)).build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -172,7 +166,7 @@ public void convertShouldMoveRegsExtUsPrivacyToRegsUsPrivacyIfNotPresent() { .isSameAs(usPrivacy); assertThat(regs) .extracting(Regs::getExt) - .isEqualTo(ExtRegs.of(null, null, "1")); + .isEqualTo(ExtRegs.of(null, null, "1", null)); }); } @@ -181,10 +175,10 @@ public void convertShouldNotChangeRegsUsPrivacyIfPresent() { // given final String usPrivacy = "privacy"; final BidRequest bidRequest = givenBidRequest(request -> request.regs( - Regs.builder().usPrivacy(usPrivacy).ext(ExtRegs.of(null, "anotherPrivacy", null)).build())); + Regs.builder().usPrivacy(usPrivacy).ext(ExtRegs.of(null, "anotherPrivacy", null, null)).build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -207,7 +201,7 @@ public void convertShouldMoveUserExtConsentToUserConsentIfNotPresent() { User.builder().ext(ExtUser.builder().consent(consent).build()).build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -230,7 +224,7 @@ public void convertShouldNotChangeUserConsentIfPresent() { User.builder().consent(consent).ext(ExtUser.builder().consent("anotherConsent").build()).build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -253,7 +247,7 @@ public void convertShouldMoveUserExtEidsToUserEidsIfNotPresent() { User.builder().ext(ExtUser.builder().eids(eids).build()).build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -276,7 +270,7 @@ public void convertShouldNotChangeUserEidsIfPresent() { User.builder().eids(eids).ext(ExtUser.builder().eids(emptyList()).build()).build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) @@ -298,7 +292,7 @@ public void convertShouldPass25EmptyEidArray() { User.builder().ext(ExtUser.builder().eids(emptyList()).build()).build())); // when - final BidRequest result = converter.convert(bidRequest); + final BidRequest result = target.convert(bidRequest); // then assertThat(result) diff --git a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java index 6fba683eb4d..de75e0182f1 100644 --- a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java @@ -292,7 +292,7 @@ public void makeHttpRequestsShouldReturnRequestsWithContextIfSitePageIsPresent() public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfGdprAndConsentAreAbsent() { // given final BidRequest bidRequest = givenBidRequest(request -> request - .regs(Regs.builder().ext(ExtRegs.of(null, null, null)).build()) + .regs(Regs.builder().ext(ExtRegs.of(null, null, null, null)).build()) .user(User.builder().ext(ExtUser.builder().consent(null).build()).build()), givenImp(identity()), givenImp(ExtImpAdnuntius.builder().network("network").build(), identity())); @@ -311,7 +311,7 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfGdprAndConsentAr public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfGdprIsAbsent() { // given final BidRequest bidRequest = givenBidRequest(request -> request - .regs(Regs.builder().ext(ExtRegs.of(null, null, null)).build()) + .regs(Regs.builder().ext(ExtRegs.of(null, null, null, null)).build()) .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()), givenImp(identity()), givenImp(ExtImpAdnuntius.builder().network("network").build(), identity())); @@ -330,7 +330,7 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfGdprIsAbsent() { public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfConsentIsAbsent() { // given final BidRequest bidRequest = givenBidRequest(request -> request - .regs(Regs.builder().ext(ExtRegs.of(1, null, null)).build()) + .regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()) .user(User.builder().ext(ExtUser.builder().consent(null).build()).build()), givenImp(identity()), givenImp(ExtImpAdnuntius.builder().network("network").build(), identity())); @@ -351,7 +351,7 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUri() { final Integer gdpr = 1; final String consent = "con sent"; final BidRequest bidRequest = givenBidRequest(request -> request - .regs(Regs.builder().ext(ExtRegs.of(gdpr, null, null)).build()) + .regs(Regs.builder().ext(ExtRegs.of(gdpr, null, null, null)).build()) .user(User.builder().ext(ExtUser.builder().consent(consent).build()).build()), givenImp(identity()), givenImp(ExtImpAdnuntius.builder().network("network").build(), identity())); diff --git a/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java b/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java index 0a79e03f43c..758caf4cb1b 100644 --- a/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java @@ -396,7 +396,7 @@ private static BidRequest givenBidRequest( .user(User.builder() .ext(ExtUser.builder().consent("consent").build()) .build()) - .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null)).build()) + .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build()) .build(); } diff --git a/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java b/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java index faff0d168d7..c279492e5ed 100644 --- a/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java @@ -55,7 +55,7 @@ public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMeth .user(User.builder() .ext(ExtUser.builder().consent("consent").build()) .build()) - .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null)).build()) + .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build()) .build(); // when @@ -82,7 +82,7 @@ public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMeth .user(User.builder() .ext(ExtUser.builder().consent("consent").build()) .build()) - .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null)).build()) + .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build()) .build())); } diff --git a/src/test/java/org/prebid/server/bidder/consumable/ConsumableBidderTest.java b/src/test/java/org/prebid/server/bidder/consumable/ConsumableBidderTest.java index 4c176cda9ed..01e87b12640 100644 --- a/src/test/java/org/prebid/server/bidder/consumable/ConsumableBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/consumable/ConsumableBidderTest.java @@ -365,7 +365,7 @@ private static BidRequest givenBidRequest(UnaryOperator builder.regs(Regs.builder() .gdpr(50) .usPrivacy("us") - .ext(ExtRegs.of(null, null, "1")) + .ext(ExtRegs.of(null, null, "1", dsa)) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2129,7 +2131,7 @@ public void makeHttpRequestsShouldFillRegsIfRegsAndGdprArePresent() { assertThat(result.getValue()).hasSize(1).doesNotContainNull() .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .extracting(BidRequest::getRegs).doesNotContainNull() - .containsOnly(Regs.builder().ext(ExtRegs.of(50, "us", "1")).build()); + .containsOnly(Regs.builder().ext(ExtRegs.of(50, "us", "1", dsa)).build()); } @Test diff --git a/src/test/java/org/prebid/server/bidder/sovrn/SovrnBidderTest.java b/src/test/java/org/prebid/server/bidder/sovrn/SovrnBidderTest.java index 0587c52cf00..a788d9c3283 100644 --- a/src/test/java/org/prebid/server/bidder/sovrn/SovrnBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/sovrn/SovrnBidderTest.java @@ -527,7 +527,7 @@ private static BidRequest givenBidRequest( return bidRequestCustomizer.apply(BidRequest.builder() .imp(singletonList(givenImp(impCustomizer))) .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()) - .regs(Regs.builder().ext(ExtRegs.of(1, null, null)).build())) + .regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build())) .build(); } diff --git a/src/test/java/org/prebid/server/bidder/unicorn/UnicornBidderTest.java b/src/test/java/org/prebid/server/bidder/unicorn/UnicornBidderTest.java index 2410ad4d3fe..6631ceb5a12 100644 --- a/src/test/java/org/prebid/server/bidder/unicorn/UnicornBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/unicorn/UnicornBidderTest.java @@ -107,7 +107,7 @@ public void makeHttpRequestsShouldReturnErrorIfCoppaIsOne() { public void makeHttpRequestsShouldReturnErrorIfGdprIsOne() { // given final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder - .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null)).build()), identity()); + .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build()), identity()); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -119,7 +119,7 @@ public void makeHttpRequestsShouldReturnErrorIfGdprIsOne() { public void makeHttpRequestsShouldReturnErrorIfUsPrivacyIsPresent() { // given final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder - .regs(Regs.builder().coppa(0).ext(ExtRegs.of(0, "privacy", null)).build()), identity()); + .regs(Regs.builder().coppa(0).ext(ExtRegs.of(0, "privacy", null, null)).build()), identity()); // when final Result>> result = target.makeHttpRequests(bidRequest); diff --git a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java index 562528da74c..e2041372583 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -30,6 +30,7 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.yahooads.ExtImpYahooAds; import java.util.List; @@ -420,13 +421,14 @@ public void makeBidsShouldSkipNotSupportedImpAndReturnVideoBidWhenVideoPresent() @Test public void makeBidsShouldRemoveTheOpenRTB26Regs() { // given + final ExtRegsDsa dsa = ExtRegsDsa.of(2, 2, 3, emptyList()); final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .gdpr(1) .usPrivacy("1YNN") .gpp("gppconsent") .gppSid(List.of(6)) - .ext(ExtRegs.of(null, null, "1")) + .ext(ExtRegs.of(null, null, "1", dsa)) .build()).device(Device.builder().ua("UA").build())); // when @@ -443,6 +445,7 @@ public void makeBidsShouldRemoveTheOpenRTB26Regs() { assertThat(regs.getExt().getGdpr()).isEqualTo(1); assertThat(regs.getExt().getUsPrivacy()).isEqualTo("1YNN"); assertThat(regs.getExt().getGpc()).isEqualTo("1"); + assertThat(regs.getExt().getDsa()).isEqualTo(dsa); assertThat(regs.getExt().getProperty("gpp").asText()).isEqualTo("gppconsent"); assertThat(regs.getExt().getProperty("gpp_sid").get(0).asText()).isEqualTo("6"); } @@ -453,7 +456,7 @@ public void makeBidsShouldOverwriteRegsExtValues() { final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .gdpr(1) - .ext(ExtRegs.of(0, "1YNN", null)) + .ext(ExtRegs.of(0, "1YNN", null, null)) .build()).device(Device.builder().ua("UA").build())); // when @@ -466,6 +469,7 @@ public void makeBidsShouldOverwriteRegsExtValues() { assertThat(regs.getUsPrivacy()).isNull(); assertThat(regs.getExt().getGdpr()).isEqualTo(1); assertThat(regs.getExt().getUsPrivacy()).isEqualTo("1YNN"); + assertThat(regs.getExt().getDsa()).isNull(); } private static BidRequest givenBidRequest( diff --git a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java index 4fb92faeef6..969f4b3605d 100644 --- a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java @@ -105,7 +105,7 @@ public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() { .build()))) .build())) .device(Device.builder().ip("ip").ua("Agent").language("fr").devicetype(1).build()) - .regs(Regs.builder().coppa(1).ext(ExtRegs.of(1, "usPrivacy", null)).build()) + .regs(Regs.builder().coppa(1).ext(ExtRegs.of(1, "usPrivacy", null, null)).build()) .user(User.builder().buyeruid("buyeruid").ext(ExtUser.builder().consent("consent").build()).build()) .site(Site.builder().page("http://www.example.com").build()) .build(); @@ -171,7 +171,7 @@ public void constructExtImpShouldWorkWithDuplicateKeysTargeting() { final BidRequest bidRequest = BidRequest.builder() .imp(imps) .device(Device.builder().ip("ip").ua("Agent").language("fr").devicetype(1).build()) - .regs(Regs.builder().coppa(1).ext(ExtRegs.of(1, "usPrivacy", null)).build()) + .regs(Regs.builder().coppa(1).ext(ExtRegs.of(1, "usPrivacy", null, null)).build()) .user(User.builder().buyeruid("buyeruid").ext(ExtUser.builder().consent("consent").build()).build()) .site(Site.builder().page("http://www.example.com").build()) .build(); @@ -220,7 +220,7 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio .build()))) .build())) .device(Device.builder().ip("ip").ua("Agent").language("fr").devicetype(1).build()) - .regs(Regs.builder().coppa(1).ext(ExtRegs.of(1, "usPrivacy", null)).build()) + .regs(Regs.builder().coppa(1).ext(ExtRegs.of(1, "usPrivacy", null, null)).build()) .user(User.builder().buyeruid("buyeruid").ext(ExtUser.builder().consent("consent").build()).build()) .site(Site.builder().page("http://www.example.com").build()) .build(); diff --git a/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java b/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java index b25a4e7da81..5d51a974e75 100644 --- a/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java +++ b/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java @@ -883,7 +883,7 @@ private static OpenRtb.BidRequest givenProtobufBidRequest() { } private static Regs givenRegs() { - final ExtRegs extRegs = ExtRegs.of(0, "", null); + final ExtRegs extRegs = ExtRegs.of(0, "", null, null); extRegs.addProperty("field", TextNode.valueOf("fieldValue")); return Regs.builder() diff --git a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java index 416e356bdac..dc2c20ba4b0 100644 --- a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java @@ -55,14 +55,14 @@ public class ResponseBidValidatorTest extends VertxTest { @Mock private Metrics metrics; - private ResponseBidValidator responseBidValidator; - @Mock private BidderAliases bidderAliases; + private ResponseBidValidator target; + @Before public void setUp() { - responseBidValidator = new ResponseBidValidator(enforce, enforce, metrics, jacksonMapper, true, 0.01); + target = new ResponseBidValidator(enforce, enforce, metrics, jacksonMapper, true, 0.01); given(bidderAliases.resolveBidder(anyString())).willReturn(BIDDER_NAME); } @@ -70,11 +70,12 @@ public void setUp() { @Test public void validateShouldFailedIfBidderBidCurrencyIsIncorrect() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(BidType.banner, "invalid", identity()), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then assertThat(result.getErrors()).containsOnly("BidResponse currency \"invalid\" is not valid"); @@ -83,8 +84,8 @@ public void validateShouldFailedIfBidderBidCurrencyIsIncorrect() { @Test public void validateShouldFailIfMissingBid() { // when - final ValidationResult result = responseBidValidator.validate( - BidderBid.of(null, null, "USD"), BIDDER_NAME, givenAuctionContext(), bidderAliases); + final ValidationResult result = target.validate( + BidderBid.of(null, null, "USD"), BIDDER_NAME, givenAuctionContext(), bidderAliases, false); // then assertThat(result.getErrors()).containsOnly("Empty bid object submitted"); @@ -93,8 +94,8 @@ public void validateShouldFailIfMissingBid() { @Test public void validateShouldFailIfBidHasNoId() { // when - final ValidationResult result = responseBidValidator.validate( - givenBid(builder -> builder.id(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + final ValidationResult result = target.validate( + givenBid(builder -> builder.id(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases, false); // then assertThat(result.getErrors()).containsOnly("Bid missing required field 'id'"); @@ -103,8 +104,8 @@ public void validateShouldFailIfBidHasNoId() { @Test public void validateShouldFailIfBidHasNoImpId() { // when - final ValidationResult result = responseBidValidator.validate( - givenBid(builder -> builder.impid(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + final ValidationResult result = target.validate( + givenBid(builder -> builder.impid(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases, false); // then assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing required field 'impid'"); @@ -113,11 +114,12 @@ public void validateShouldFailIfBidHasNoImpId() { @Test public void validateShouldSuccessForDealZeroPriceBid() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenVideoBid(builder -> builder.price(BigDecimal.valueOf(0)).dealid("dealId")), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then assertThat(result.hasErrors()).isFalse(); @@ -126,8 +128,8 @@ public void validateShouldSuccessForDealZeroPriceBid() { @Test public void validateShouldFailIfBidHasNoCrid() { // when - final ValidationResult result = responseBidValidator.validate( - givenBid(builder -> builder.crid(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + final ValidationResult result = target.validate( + givenBid(builder -> builder.crid(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases, false); // then assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing creative ID"); @@ -136,8 +138,8 @@ public void validateShouldFailIfBidHasNoCrid() { @Test public void validateShouldFailIfBannerBidHasNoWidthAndHeight() { // when - final ValidationResult result = responseBidValidator.validate( - givenBid(builder -> builder.w(null).h(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + final ValidationResult result = target.validate( + givenBid(builder -> builder.w(null).h(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases, false); // then assertThat(result.getErrors()) @@ -150,8 +152,8 @@ public void validateShouldFailIfBannerBidHasNoWidthAndHeight() { @Test public void validateShouldFailIfBannerBidWidthIsGreaterThanImposedByImp() { // when - final ValidationResult result = responseBidValidator.validate( - givenBid(builder -> builder.w(150).h(150)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + final ValidationResult result = target.validate( + givenBid(builder -> builder.w(150).h(150)), BIDDER_NAME, givenAuctionContext(), bidderAliases, false); // then assertThat(result.getErrors()) @@ -164,11 +166,12 @@ public void validateShouldFailIfBannerBidWidthIsGreaterThanImposedByImp() { @Test public void validateShouldFailIfBannerBidHeightIsGreaterThanImposedByImp() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(builder -> builder.w(50).h(250)), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then assertThat(result.getErrors()) @@ -181,11 +184,12 @@ public void validateShouldFailIfBannerBidHeightIsGreaterThanImposedByImp() { @Test public void validateShouldReturnSuccessIfNonBannerBidHasAnySize() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(BidType.video, builder -> builder.w(3).h(3)), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then assertThat(result.hasErrors()).isFalse(); @@ -198,11 +202,12 @@ public void validateShouldTolerateMissingImpExtBidderNode() { .ext(mapper.createObjectNode() .set("prebid", mapper.createObjectNode()))); - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(BidType.video, builder -> builder.w(3).h(3)), BIDDER_NAME, givenAuctionContext(bidRequest), - bidderAliases); + bidderAliases, + false); // then assertThat(result.hasErrors()).isFalse(); @@ -211,14 +216,15 @@ public void validateShouldTolerateMissingImpExtBidderNode() { @Test public void validateShouldReturnSuccessIfBannerBidHasInvalidSizeButAccountDoesNotEnforceValidation() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(builder -> builder.w(150).h(150)), BIDDER_NAME, givenAuctionContext( givenAccount(builder -> builder.auction(AccountAuctionConfig.builder() .bidValidations(AccountBidValidationConfig.of(skip)) .build()))), - bidderAliases); + bidderAliases, + false); // then assertThat(result.hasErrors()).isFalse(); @@ -227,11 +233,12 @@ public void validateShouldReturnSuccessIfBannerBidHasInvalidSizeButAccountDoesNo @Test public void validateShouldFailIfBidHasNoCorrespondingImp() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(builder -> builder.impid("nonExistentsImpid")), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then assertThat(result.getErrors()) @@ -241,11 +248,12 @@ public void validateShouldFailIfBidHasNoCorrespondingImp() { @Test public void validateShouldFailIfBidHasInsecureMarkerInCreativeInSecureContext() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(builder -> builder.adm("http://site.com/creative.jpg")), BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), - bidderAliases); + bidderAliases, + false); // then assertThat(result.getErrors()) @@ -258,11 +266,12 @@ public void validateShouldFailIfBidHasInsecureMarkerInCreativeInSecureContext() @Test public void validateShouldFailIfBidHasInsecureEncodedMarkerInCreativeInSecureContext() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(builder -> builder.adm("http%3A//site.com/creative.jpg")), BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), - bidderAliases); + bidderAliases, + false); // then assertThat(result.getErrors()) @@ -275,11 +284,12 @@ public void validateShouldFailIfBidHasInsecureEncodedMarkerInCreativeInSecureCon @Test public void validateShouldFailIfBidHasNoSecureMarkersInCreativeInSecureContext() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(builder -> builder.adm("//site.com/creative.jpg")), BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), - bidderAliases); + bidderAliases, + false); // then assertThat(result.getErrors()) @@ -292,11 +302,12 @@ public void validateShouldFailIfBidHasNoSecureMarkersInCreativeInSecureContext() @Test public void validateShouldReturnSuccessIfBidHasInsecureCreativeInInsecureContext() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(builder -> builder.adm("http://site.com/creative.jpg")), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then assertThat(result.hasErrors()).isFalse(); @@ -305,11 +316,12 @@ public void validateShouldReturnSuccessIfBidHasInsecureCreativeInInsecureContext @Test public void validateShouldFailedIfVideoBidHasNoNurlAndAdm() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(BidType.video, builder -> builder.adm(null).nurl(null)), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then assertThat(result.getErrors()) @@ -320,11 +332,12 @@ public void validateShouldFailedIfVideoBidHasNoNurlAndAdm() { @Test public void validateShouldReturnSuccessfulResultForValidVideoBidWithNurl() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(BidType.video, builder -> builder.adm(null)), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then assertThat(result.hasErrors()).isFalse(); @@ -333,11 +346,13 @@ public void validateShouldReturnSuccessfulResultForValidVideoBidWithNurl() { @Test public void validateShouldReturnSuccessfulResultForValidVideoBidWithAdm() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(BidType.video, builder -> builder.nurl(null)), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + + false); // then assertThat(result.hasErrors()).isFalse(); @@ -346,11 +361,12 @@ public void validateShouldReturnSuccessfulResultForValidVideoBidWithAdm() { @Test public void validateShouldReturnSuccessfulResultForValidBid() { // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(identity()), BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), - bidderAliases); + bidderAliases, + false); // then assertThat(result.hasErrors()).isFalse(); @@ -359,14 +375,15 @@ public void validateShouldReturnSuccessfulResultForValidBid() { @Test public void validateShouldReturnSuccessIfBannerSizeValidationNotEnabled() { // given - responseBidValidator = new ResponseBidValidator(skip, enforce, metrics, jacksonMapper, true, 0.01); + target = new ResponseBidValidator(skip, enforce, metrics, jacksonMapper, true, 0.01); // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(identity()), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then assertThat(result.hasErrors()).isFalse(); @@ -375,14 +392,15 @@ public void validateShouldReturnSuccessIfBannerSizeValidationNotEnabled() { @Test public void validateShouldReturnSuccessWithWarningIfBannerSizeEnforcementIsWarn() { // given - responseBidValidator = new ResponseBidValidator(warn, enforce, metrics, jacksonMapper, true, 0.01); + target = new ResponseBidValidator(warn, enforce, metrics, jacksonMapper, true, 0.01); // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(builder -> builder.w(null).h(null)), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then assertThat(result.hasErrors()).isFalse(); @@ -396,14 +414,15 @@ public void validateShouldReturnSuccessWithWarningIfBannerSizeEnforcementIsWarn( @Test public void validateShouldReturnSuccessIfSecureMarkupValidationNotEnabled() { // given - responseBidValidator = new ResponseBidValidator(enforce, skip, metrics, jacksonMapper, true, 0.01); + target = new ResponseBidValidator(enforce, skip, metrics, jacksonMapper, true, 0.01); // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(builder -> builder.adm("http://site.com/creative.jpg")), BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), - bidderAliases); + bidderAliases, + false); // then assertThat(result.hasErrors()).isFalse(); @@ -412,14 +431,15 @@ public void validateShouldReturnSuccessIfSecureMarkupValidationNotEnabled() { @Test public void validateShouldReturnSuccessWithWarningIfSecureMarkupEnforcementIsWarn() { // given - responseBidValidator = new ResponseBidValidator(enforce, warn, metrics, jacksonMapper, true, 0.01); + target = new ResponseBidValidator(enforce, warn, metrics, jacksonMapper, true, 0.01); // when - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(builder -> builder.adm("http://site.com/creative.jpg")), BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), - bidderAliases); + bidderAliases, + false); // then assertThat(result.hasErrors()).isFalse(); @@ -433,11 +453,12 @@ public void validateShouldReturnSuccessWithWarningIfSecureMarkupEnforcementIsWar @Test public void validateShouldIncrementSizeValidationErrMetrics() { // when - responseBidValidator.validate( + target.validate( givenBid(builder -> builder.w(150).h(200)), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err); @@ -446,14 +467,15 @@ public void validateShouldIncrementSizeValidationErrMetrics() { @Test public void validateShouldIncrementSizeValidationWarnMetrics() { // given - responseBidValidator = new ResponseBidValidator(warn, warn, metrics, jacksonMapper, true, 0.01); + target = new ResponseBidValidator(warn, warn, metrics, jacksonMapper, true, 0.01); // when - responseBidValidator.validate( + target.validate( givenBid(builder -> builder.w(150).h(200)), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); // then verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn); @@ -462,11 +484,12 @@ public void validateShouldIncrementSizeValidationWarnMetrics() { @Test public void validateShouldIncrementSecureValidationErrMetrics() { // when - responseBidValidator.validate( + target.validate( givenBid(builder -> builder.adm("http://site.com/creative.jpg")), BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), - bidderAliases); + bidderAliases, + false); // then verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err); @@ -475,14 +498,15 @@ public void validateShouldIncrementSecureValidationErrMetrics() { @Test public void validateShouldIncrementSecureValidationWarnMetrics() { // given - responseBidValidator = new ResponseBidValidator(warn, warn, metrics, jacksonMapper, true, 0.01); + target = new ResponseBidValidator(warn, warn, metrics, jacksonMapper, true, 0.01); // when - responseBidValidator.validate( + target.validate( givenBid(builder -> builder.adm("http://site.com/creative.jpg")), BIDDER_NAME, givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), - bidderAliases); + bidderAliases, + false); // then verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn); @@ -490,22 +514,24 @@ public void validateShouldIncrementSecureValidationWarnMetrics() { @Test public void validateShouldReturnSuccessfulResultForValidNonDealBid() { - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenVideoBid(identity()), BIDDER_NAME, givenAuctionContext(), - bidderAliases); + bidderAliases, + false); assertThat(result.hasErrors()).isFalse(); } @Test public void validateShouldFailIfBidHasNoDealid() { - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenVideoBid(identity()), BIDDER_NAME, givenAuctionContext(givenRequest(identity())), - bidderAliases); + bidderAliases, + false); assertThat(result.getErrors()).hasSize(1) .containsOnly("Bid \"bidId1\" missing required field 'dealid'"); @@ -513,11 +539,12 @@ public void validateShouldFailIfBidHasNoDealid() { @Test public void validateShouldSuccessIfBidHasDealidAndImpHasNoDeals() { - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenVideoBid(bid -> bid.dealid("dealId1")), BIDDER_NAME, givenAuctionContext(givenRequest(identity())), - bidderAliases); + bidderAliases, + false); assertThat(result.getErrors()).isEmpty(); assertThat(result.getWarnings()).isEmpty(); @@ -527,7 +554,7 @@ public void validateShouldSuccessIfBidHasDealidAndImpHasNoDeals() { public void validateShouldWarnIfBidHasDealidMissingInImp() { given(bidderAliases.isSame(eq(BIDDER_NAME), eq(BIDDER_NAME))).willReturn(true); - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenVideoBid(bid -> bid.dealid("dealId1")), BIDDER_NAME, givenAuctionContext(givenRequest(imp -> imp.pmp(pmp(asList( @@ -543,7 +570,8 @@ public void validateShouldWarnIfBidHasDealidMissingInImp() { .id("dealId4") .ext(mapper.valueToTree(ExtDeal.of( ExtDealLine.of(null, null, null, "anotherBidder")))))))))), - bidderAliases); + bidderAliases, + false); assertThat(result.getWarnings()).hasSize(1) .containsOnly("WARNING: Bid \"bidId1\" has 'dealid' not present in corresponding imp in request." @@ -552,14 +580,15 @@ public void validateShouldWarnIfBidHasDealidMissingInImp() { @Test public void validateShouldFailIfBidIsBannerAndImpHasNoBanner() { - responseBidValidator = new ResponseBidValidator(skip, enforce, metrics, jacksonMapper, true, 0.01); + target = new ResponseBidValidator(skip, enforce, metrics, jacksonMapper, true, 0.01); - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(bid -> bid.dealid("dealId1")), BIDDER_NAME, givenAuctionContext(givenRequest(imp -> imp .pmp(pmp(singletonList(deal(builder -> builder.id("dealId1"))))))), - bidderAliases); + bidderAliases, + false); assertThat(result.getErrors()).hasSize(1) .containsOnly("Bid \"bidId1\" has banner media type but corresponding imp in request is missing " @@ -568,7 +597,7 @@ public void validateShouldFailIfBidIsBannerAndImpHasNoBanner() { @Test public void validateShouldFailIfBidIsBannerAndSizeHasNoMatchInBannerFormats() { - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(bid -> bid.dealid("dealId1").w(300).h(400)), BIDDER_NAME, givenAuctionContext(givenRequest(imp -> imp @@ -576,7 +605,8 @@ public void validateShouldFailIfBidIsBannerAndSizeHasNoMatchInBannerFormats() { .banner(Banner.builder() .format(singletonList(Format.builder().w(400).h(500).build())) .build()))), - bidderAliases); + bidderAliases, + false); assertThat(result.getErrors()).hasSize(1) .containsOnly("Bid \"bidId1\" has 'w' and 'h' not supported by corresponding imp in request. Bid " @@ -585,7 +615,7 @@ public void validateShouldFailIfBidIsBannerAndSizeHasNoMatchInBannerFormats() { @Test public void validateShouldFailIfBidIsBannerAndSizeHasNoMatchInLineItem() { - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(bid -> bid.dealid("dealId1").w(300).h(400)), BIDDER_NAME, givenAuctionContext(givenRequest(imp -> imp @@ -596,7 +626,8 @@ public void validateShouldFailIfBidIsBannerAndSizeHasNoMatchInLineItem() { .banner(Banner.builder() .format(singletonList(Format.builder().w(300).h(400).build())) .build()))), - bidderAliases); + bidderAliases, + false); assertThat(result.getErrors()).hasSize(1) .containsOnly("Bid \"bidId1\" has 'w' and 'h' not matched to Line Item. Bid dimensions: '300x400', " @@ -605,7 +636,7 @@ public void validateShouldFailIfBidIsBannerAndSizeHasNoMatchInLineItem() { @Test public void validateShouldFailIfBidIsBannerAndMatchingLineItemDoesNotHaveSizes() { - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(bid -> bid.dealid("dealId1").w(300).h(400)), BIDDER_NAME, givenAuctionContext(givenRequest(imp -> imp @@ -616,7 +647,8 @@ public void validateShouldFailIfBidIsBannerAndMatchingLineItemDoesNotHaveSizes() .banner(Banner.builder() .format(singletonList(Format.builder().w(300).h(400).build())) .build()))), - bidderAliases); + bidderAliases, + false); assertThat(result.getErrors()).hasSize(1) .containsOnly("Line item sizes were not found for bidId bidId1 and dealId dealId1"); @@ -624,7 +656,7 @@ public void validateShouldFailIfBidIsBannerAndMatchingLineItemDoesNotHaveSizes() @Test public void validateShouldSuccessIfBidIsBannerAndSizeHasNoMatchInLineItemForNonPgDeal() { - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(bid -> bid.dealid("dealId1").w(300).h(400)), BIDDER_NAME, givenAuctionContext(givenRequest(imp -> imp @@ -635,7 +667,8 @@ public void validateShouldSuccessIfBidIsBannerAndSizeHasNoMatchInLineItemForNonP .banner(Banner.builder() .format(singletonList(Format.builder().w(300).h(400).build())) .build()))), - bidderAliases); + bidderAliases, + false); assertThat(result.getErrors()).isEmpty(); assertThat(result.getWarnings()).isEmpty(); @@ -643,19 +676,20 @@ public void validateShouldSuccessIfBidIsBannerAndSizeHasNoMatchInLineItemForNonP @Test public void validateShouldReturnSuccessfulResultForValidDealNonBannerBid() { - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenVideoBid(bid -> bid.dealid("dealId1")), BIDDER_NAME, givenAuctionContext(givenRequest(imp -> imp.pmp(pmp(singletonList( deal(builder -> builder.id("dealId1"))))))), - bidderAliases); + bidderAliases, + false); assertThat(result.hasErrors()).isFalse(); } @Test public void validateShouldReturnSuccessfulResultForValidDealBannerBid() { - final ValidationResult result = responseBidValidator.validate( + final ValidationResult result = target.validate( givenBid(bid -> bid.dealid("dealId1").w(300).h(400)), BIDDER_NAME, givenAuctionContext(givenRequest(imp -> imp @@ -666,8 +700,81 @@ public void validateShouldReturnSuccessfulResultForValidDealBannerBid() { .banner(Banner.builder() .format(singletonList(Format.builder().w(300).h(400).build())) .build()))), - bidderAliases); + bidderAliases, + false); + + assertThat(result.hasErrors()).isFalse(); + } + @Test + public void validateShouldReturnSuccessfulResultWhenDsaValidationIsRequiredAndBidIsValid() { + // given + final ObjectNode dsaNode = mapper.createObjectNode() + .put("behalf", "Advertiser") + .put("paid", "Advertiser") + .put("adrender", 1); + final ObjectNode ext = mapper.createObjectNode().set("dsa", dsaNode); + + // when + final ValidationResult result = target.validate( + givenBid(builder -> builder.ext(ext)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases, + true); + + // then + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldFailWhenDsaValidationIsRequiredAndBidExtHasEmptyDsa() { + // given + final ObjectNode ext = mapper.createObjectNode().set("dsa", mapper.createObjectNode()); + + // when + final ValidationResult result = target.validate( + givenBid(builder -> builder.ext(ext)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases, + true); + + // then + assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing DSA"); + } + + @Test + public void validateShouldFailWhenDsaValidationIsRequiredAndBidExtNotHaveDsa() { + // given + final ObjectNode ext = mapper.createObjectNode(); + + // when + final ValidationResult result = target.validate( + givenBid(builder -> builder.ext(ext)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases, + true); + + // then + assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing DSA"); + } + + @Test + public void validateShouldReturnSuccessfulResultWhenDsaValidationIsNotRequiredAndBidExtHasEmptyDsa() { + // given + final ObjectNode ext = mapper.createObjectNode(); + + // when + final ValidationResult result = target.validate( + givenBid(builder -> builder.ext(ext)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases, + false); + + // then assertThat(result.hasErrors()).isFalse(); } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json index a385a8a1275..8fab064c3a8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json @@ -72,7 +72,27 @@ 7 ], "ext": { - "gdpr": 0 + "gdpr": 0, + "dsa": { + "required": 1, + "pubrender": 0, + "datatopub": 2, + "transparency": [ + { + "domain": "platform1domain.com", + "params": [ + 1 + ] + }, + { + "domain": "SSP2domain.com", + "params": [ + 1, + 2 + ] + } + ] + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json index 7860b71387c..ce256957edc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json @@ -14,6 +14,18 @@ "w": 300, "h": 250, "ext": { + "dsa": { + "behalf": "Advertiser", + "paid": "Advertiser", + "transparency": { + "domain": "dsp1domain.com", + "params": [ + 1, + 2 + ] + }, + "adrender": 1 + }, "prebid": { "type": "video", "targeting": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json index 5cae795f22a..82f0c913be0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json @@ -13,6 +13,15 @@ "w": 300, "h": 250, "ext": { + "dsa" : { + "behalf" : "Advertiser", + "paid" : "Advertiser", + "transparency" : { + "domain" : "dsp1domain.com", + "params" : [ 1, 2 ] + }, + "adrender" : 1 + }, "prebid": { "type": "video", "meta": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json index 4c75cbcfa5c..cd6df8e9e85 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json @@ -39,7 +39,27 @@ }, "regs" : { "ext" : { - "gdpr" : 0 + "gdpr" : 0, + "dsa": { + "required": 1, + "pubrender": 0, + "datatopub": 2, + "transparency": [ + { + "domain": "platform1domain.com", + "params": [ + 1 + ] + }, + { + "domain": "SSP2domain.com", + "params": [ + 1, + 2 + ] + } + ] + } } }, "ext" : { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-response.json index cd18ad69edc..184a787e1b0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-response.json @@ -14,6 +14,18 @@ "h": 250, "w": 300, "ext": { + "dsa": { + "behalf": "Advertiser", + "paid": "Advertiser", + "transparency": { + "domain": "dsp1domain.com", + "params": [ + 1, + 2 + ] + }, + "adrender": 1 + }, "prebid": { "meta": { "rendererName": "SamplePluginRenderer", From 062a7b2fee5ebfe7e5ed69b4c5f8a9523a908526 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Tue, 30 Jan 2024 11:18:39 +0100 Subject: [PATCH 2/7] Fix validation --- .../java/org/prebid/server/validation/ResponseBidValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java index 52a53c08220..293beaf22eb 100644 --- a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java +++ b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java @@ -166,7 +166,7 @@ private static void validateCurrency(String currency) throws ValidationException private void validateDsaFor(Bid bid) throws ValidationException { final ObjectNode bidExt = bid.getExt(); - if (bidExt != null && (!bidExt.hasNonNull(DSA_EXT) || bidExt.get(DSA_EXT).isEmpty())) { + if (bidExt == null || !bidExt.hasNonNull(DSA_EXT) || bidExt.get(DSA_EXT).isEmpty()) { throw new ValidationException("Bid \"%s\" missing DSA", bid.getId()); } } From bcc291c3e8aba4fcc67bc4eb8fa5392e3e7c4b37 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 5 Feb 2024 10:49:43 +0100 Subject: [PATCH 3/7] Tests: Cover DSA fields and validation (#2939) --- .../request/auction/DsaTransparency.groovy | 16 + .../model/request/auction/RegsDsa.groovy | 25 ++ .../model/request/auction/RegsExt.groovy | 2 + .../auction/ReqsDsaRequiredType.groovy | 15 + .../model/response/auction/BidExt.groovy | 4 + .../model/response/auction/BidExtDsa.groovy | 23 ++ .../server/functional/tests/DsaSpec.groovy | 293 ++++++++++++++++++ 7 files changed, 378 insertions(+) create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/ReqsDsaRequiredType.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/response/auction/BidExtDsa.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy new file mode 100644 index 00000000000..0b49be88fe4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy @@ -0,0 +1,16 @@ +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 DsaTransparency { + + String domain + List params + + static DsaTransparency getDefaultRegsDsaTransparency() { + new DsaTransparency(domain: PBSUtils.randomString) + } +} + diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy new file mode 100644 index 00000000000..91424d7be32 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy @@ -0,0 +1,25 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@ToString(includeNames = true, ignoreNulls = true) +class RegsDsa { + + Integer required + Integer pubRender + Integer dataToPub + List transparency + + static RegsDsa getDefaultRegsDsa(ReqsDsaRequiredType required) { + new RegsDsa( + required: required.value, + pubRender: PBSUtils.getRandomNumber(0, 2), + dataToPub: PBSUtils.getRandomNumber(0, 2), + transparency: [DsaTransparency.defaultRegsDsaTransparency] + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy index 9fb33921fee..0ec8962c778 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy @@ -11,4 +11,6 @@ class RegsExt { Integer gdpr String usPrivacy String gpc + RegsDsa dsa + } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ReqsDsaRequiredType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ReqsDsaRequiredType.groovy new file mode 100644 index 00000000000..2cb07b350d4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ReqsDsaRequiredType.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum ReqsDsaRequiredType { + + NOT_REQUIRED(0), SUPPORTED(1), REQUIRED(2), REQUIRED_PUBLISHER_ONLINE_PLATFORM(3) + + @JsonValue + final Integer value + + ReqsDsaRequiredType(Integer value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy index d2e1c1e71ac..769f6ff88ff 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy @@ -1,10 +1,14 @@ package org.prebid.server.functional.model.response.auction +import groovy.transform.ToString import org.prebid.server.functional.model.Currency +@ToString(includeNames = true, ignoreNulls = true) class BidExt { Prebid prebid BigDecimal origbidcpm Currency origbidcur + BidExtDsa dsa + } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExtDsa.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExtDsa.groovy new file mode 100644 index 00000000000..4712c59e76e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExtDsa.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.response.auction + +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.DsaTransparency +import org.prebid.server.functional.util.PBSUtils + +@ToString(includeNames = true, ignoreNulls = true) +class BidExtDsa { + + String behalf + String paid + List transparency + Integer adrender + + static BidExtDsa getDefaultBidExtDsa() { + new BidExtDsa( + behalf: PBSUtils.randomString, + paid: PBSUtils.randomString, + adrender: PBSUtils.getRandomNumber(0, 2), + transparency: [DsaTransparency.defaultRegsDsaTransparency] + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy new file mode 100644 index 00000000000..6706fbfce54 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy @@ -0,0 +1,293 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.request.amp.AmpRequest +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.RegsDsa +import org.prebid.server.functional.model.request.auction.ReqsDsaRequiredType +import org.prebid.server.functional.model.response.auction.BidExt +import org.prebid.server.functional.model.response.auction.BidExtDsa +import org.prebid.server.functional.model.response.auction.BidResponse + +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC + +class DsaSpec extends BaseSpec { + + def "AMP request should send DSA to bidder and succeed when DSA is not required"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) + + and: "Stored default bid request with DSA" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + setAccountId(ampRequest.account) + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Default bidder response with DSA" + def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest).tap { + seatbid[0].bid[0].ext = bidExt + } + + and: "Set bidder response" + bidder.setResponse(ampStoredRequest.id, bidResponse) + + when: "PBS processes amp request" + def response = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain DSA" + def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll { + bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub + bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender + bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain + bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + } + + and: "Bidder response should not contain DSA" + def bidderResponse = decode(response.ext.debug.httpcalls.get(BidderName.GENERIC.value)[0].responseBody, BidResponse) + assert !bidderResponse.seatbid[0].bid[0].ext?.dsa + + and: "PBS should not log warning" + assert !response.ext.warnings + assert !response.ext.errors + + where: + dsaRequired | bidExt + ReqsDsaRequiredType.NOT_REQUIRED | null + ReqsDsaRequiredType.SUPPORTED | new BidExt(dsa: null) + } + + def "AMP request should send DSA to bidder and always succeed when bidder returns DSA"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) + + and: "Stored default bid request with DSA" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + setAccountId(ampRequest.account) + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Default bidder response with DSA" + def bidDsa = BidExtDsa.getDefaultBidExtDsa() + def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest).tap { + seatbid[0].bid[0].ext = new BidExt(dsa: bidDsa) + } + + and: "Set bidder response" + bidder.setResponse(ampStoredRequest.id, bidResponse) + + when: "PBS processes amp request" + def response = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain DSA" + def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll { + bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub + bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender + bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain + bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + } + + and: "Bidder response should contain DSA" + def bidderResponse = decode(response.ext.debug.httpcalls.get(BidderName.GENERIC.value)[0].responseBody, BidResponse) + def actualDsa = bidderResponse.seatbid[0].bid[0].ext.dsa + verifyAll { + actualDsa.transparency[0].domain == bidDsa.transparency[0].domain + actualDsa.transparency[0].params == bidDsa.transparency[0].params + actualDsa.adrender == bidDsa.adrender + actualDsa.behalf == bidDsa.behalf + actualDsa.paid == bidDsa.paid + } + + and: "PBS should not log warning" + assert !response.ext.warnings + assert !response.ext.errors + + where: + dsaRequired << ReqsDsaRequiredType.values() + } + + def "AMP request should send DSA to bidder and fail on response when DSA is required and bidder does not return DSA"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) + + and: "Stored default bid request with DSA" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + setAccountId(ampRequest.account) + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Default bidder response with DSA" + def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest).tap { + seatbid[0].bid[0].ext = bidExt + } + + and: "Set bidder response" + bidder.setResponse(ampStoredRequest.id, bidResponse) + + when: "PBS processes amp request" + def response = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain DSA" + def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll { + bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub + bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender + bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain + bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + } + + and: "Bidder response should not contain DSA" + def bidderResponse = decode(response.ext.debug.httpcalls.get(BidderName.GENERIC.value)[0].responseBody, BidResponse) + assert !bidderResponse.seatbid[0].bid[0].ext?.dsa + + and: "Response should contain error" + def expectedBidId = bidResponse.seatbid[0].bid[0].id + assert response.ext?.errors[GENERIC]*.code == [5] + assert response.ext?.errors[GENERIC]*.message == ["BidId `$expectedBidId` validation messages: Error: Bid \"$expectedBidId\" missing DSA"] + + where: + dsaRequired | bidExt + ReqsDsaRequiredType.REQUIRED | new BidExt(dsa: null) + ReqsDsaRequiredType.REQUIRED_PUBLISHER_ONLINE_PLATFORM | null + } + + def "Auction request should send DSA to bidder and succeeds when DSA is not required and bidder does not return DSA"() { + given: "Default bid request with DSA" + def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) + def bidRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + } + + and: "Default bidder response with DSA" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].ext = new BidExt(dsa: null) + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain DSA" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + verifyAll { + bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub + bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender + bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain + bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + } + + and: "DSA is not returned" + assert !response.seatbid[0].bid[0].ext.dsa + + where: + dsaRequired << [ReqsDsaRequiredType.NOT_REQUIRED, ReqsDsaRequiredType.SUPPORTED] + } + + def "Auction request should send DSA to bidder and always succeed when bidder returns DSA"() { + given: "Default bid request with DSA" + def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) + def bidRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + } + + and: "Default bidder response with DSA" + def bidDsa = BidExtDsa.getDefaultBidExtDsa() + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].ext = new BidExt(dsa: bidDsa) + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain DSA" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + verifyAll { + bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub + bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender + bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain + bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + } + + and: "DSA is not returned" + def actualDsa = response.seatbid[0].bid[0].ext.dsa + verifyAll { + actualDsa.transparency[0].domain == bidDsa.transparency[0].domain + actualDsa.transparency[0].params == bidDsa.transparency[0].params + actualDsa.adrender == bidDsa.adrender + actualDsa.behalf == bidDsa.behalf + actualDsa.paid == bidDsa.paid + } + + where: + dsaRequired << ReqsDsaRequiredType.values() + } + + def "Auction request should send DSA to bidder and fail on response when DSA is required and bidder does not return DSA"() { + given: "Default bid request with DSA" + def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) + def bidRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + } + + and: "Default bidder response with DSA" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].ext = bidExt + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain DSA" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + verifyAll { + bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub + bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender + bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain + bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + } + + and: "Response should contain error" + def expectedBidId = bidResponse.seatbid[0].bid[0].id + verifyAll { + response.seatbid.isEmpty() + response.ext?.errors[GENERIC]*.code == [5] + response.ext?.errors[GENERIC]*.message == ["BidId `$expectedBidId` validation messages: Error: Bid \"$expectedBidId\" missing DSA"] + } + + where: + dsaRequired | bidExt + ReqsDsaRequiredType.REQUIRED | new BidExt(dsa: null) + ReqsDsaRequiredType.REQUIRED_PUBLISHER_ONLINE_PLATFORM | null + } +} From 3b43f0ac7f7f27c58b1944ad2ce152ea2e77726e Mon Sep 17 00:00:00 2001 From: antonbabak Date: Tue, 6 Feb 2024 15:48:44 +0100 Subject: [PATCH 4/7] Fix comments --- src/main/java/org/prebid/server/auction/ExchangeService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 899c5268df3..2dd0e6f4d09 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -1553,7 +1553,7 @@ private AuctionParticipation validBidderResponse(AuctionParticipation auctionPar return auctionParticipation.with(resultBidderResponse); } - private static Boolean isDsaValidationRequired(BidRequest bidRequest) { + private static boolean isDsaValidationRequired(BidRequest bidRequest) { return Optional.ofNullable(bidRequest.getRegs()) .map(Regs::getExt) .map(ExtRegs::getDsa) From 6e3a0ecc8473d77bed59232868e5997679450266 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Thu, 8 Feb 2024 11:00:55 +0100 Subject: [PATCH 5/7] Rename DSA fields according the specs --- .../server/auction/ExchangeService.java | 2 +- .../proto/openrtb/ext/request/ExtRegsDsa.java | 4 +-- .../ext/request/ExtRegsDsaTransparency.java | 4 +-- .../request/auction/DsaTransparency.groovy | 2 +- .../model/request/auction/RegsDsa.groovy | 4 +-- .../server/functional/tests/DsaSpec.groovy | 28 +++++++++---------- .../BidRequestOrtb26To25ConverterTest.java | 2 +- .../test-auction-generic-request.json | 6 ++-- .../test-auction-generic-response.json | 2 +- .../test-cache-generic-request.json | 2 +- .../test-generic-bid-request.json | 6 ++-- .../test-generic-bid-response.json | 2 +- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 2dd0e6f4d09..f7c30b8cc2c 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -1557,7 +1557,7 @@ private static boolean isDsaValidationRequired(BidRequest bidRequest) { return Optional.ofNullable(bidRequest.getRegs()) .map(Regs::getExt) .map(ExtRegs::getDsa) - .map(ExtRegsDsa::getRequired) + .map(ExtRegsDsa::getDsarequired) .map(DSA_REQUIRED::contains) .orElse(false); } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java index 3e2072cd053..5772a46a255 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java @@ -11,9 +11,9 @@ public class ExtRegsDsa { /** - * Defines the contract for bidrequest.regs.ext.dsa.required + * Defines the contract for bidrequest.regs.ext.dsa.dsarequired */ - Integer required; + Integer dsarequired; /** * Defines the contract for bidrequest.regs.ext.dsa.pubrender diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java index 73d6f9d7997..5ace29f9280 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java @@ -16,7 +16,7 @@ public class ExtRegsDsaTransparency { String domain; /** - * Defines the contract for bidrequest.regs.ext.dsa.transparency[i].params[] + * Defines the contract for bidrequest.regs.ext.dsa.transparency[i].dsaparams[] */ - List params; + List dsaparams; } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy index 0b49be88fe4..2942a205a42 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy @@ -7,7 +7,7 @@ import org.prebid.server.functional.util.PBSUtils class DsaTransparency { String domain - List params + List dsaparams static DsaTransparency getDefaultRegsDsaTransparency() { new DsaTransparency(domain: PBSUtils.randomString) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy index 91424d7be32..ea413276f4e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy @@ -9,14 +9,14 @@ import org.prebid.server.functional.util.PBSUtils @ToString(includeNames = true, ignoreNulls = true) class RegsDsa { - Integer required + Integer dsarequired Integer pubRender Integer dataToPub List transparency static RegsDsa getDefaultRegsDsa(ReqsDsaRequiredType required) { new RegsDsa( - required: required.value, + dsarequired: required.value, pubRender: PBSUtils.getRandomNumber(0, 2), dataToPub: PBSUtils.getRandomNumber(0, 2), transparency: [DsaTransparency.defaultRegsDsaTransparency] diff --git a/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy index 6706fbfce54..620c431ab0e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy @@ -43,11 +43,11 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams } and: "Bidder response should not contain DSA" @@ -94,11 +94,11 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams } and: "Bidder response should contain DSA" @@ -106,7 +106,7 @@ class DsaSpec extends BaseSpec { def actualDsa = bidderResponse.seatbid[0].bid[0].ext.dsa verifyAll { actualDsa.transparency[0].domain == bidDsa.transparency[0].domain - actualDsa.transparency[0].params == bidDsa.transparency[0].params + actualDsa.transparency[0].dsaparams == bidDsa.transparency[0].dsaparams actualDsa.adrender == bidDsa.adrender actualDsa.behalf == bidDsa.behalf actualDsa.paid == bidDsa.paid @@ -149,11 +149,11 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams } and: "Bidder response should not contain DSA" @@ -192,11 +192,11 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(bidRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams } and: "DSA is not returned" @@ -228,18 +228,18 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(bidRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams } and: "DSA is not returned" def actualDsa = response.seatbid[0].bid[0].ext.dsa verifyAll { actualDsa.transparency[0].domain == bidDsa.transparency[0].domain - actualDsa.transparency[0].params == bidDsa.transparency[0].params + actualDsa.transparency[0].dsaparams == bidDsa.transparency[0].dsaparams actualDsa.adrender == bidDsa.adrender actualDsa.behalf == bidDsa.behalf actualDsa.paid == bidDsa.paid @@ -270,11 +270,11 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(bidRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.required == dsa.required + bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].params == dsa.transparency[0].params + bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams } and: "Response should contain error" diff --git a/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java b/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java index 8896a1b451f..4a53da795c9 100644 --- a/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java +++ b/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java @@ -109,7 +109,7 @@ public void convertShouldMoveSourceSupplyChain() { public void convertShouldMoveRegsData() { // given final Map dsaMap = Map.of( - "required", 1, + "dsarequired", 1, "pubrender", 2, "datatopub", 3, "transparency", emptyList()); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json index 8fab064c3a8..dfa745e2d1d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json @@ -74,19 +74,19 @@ "ext": { "gdpr": 0, "dsa": { - "required": 1, + "dsarequired": 1, "pubrender": 0, "datatopub": 2, "transparency": [ { "domain": "platform1domain.com", - "params": [ + "dsaparams": [ 1 ] }, { "domain": "SSP2domain.com", - "params": [ + "dsaparams": [ 1, 2 ] diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json index ce256957edc..552408995c8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json @@ -19,7 +19,7 @@ "paid": "Advertiser", "transparency": { "domain": "dsp1domain.com", - "params": [ + "dsaparams": [ 1, 2 ] diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json index 82f0c913be0..1b5c1802325 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json @@ -18,7 +18,7 @@ "paid" : "Advertiser", "transparency" : { "domain" : "dsp1domain.com", - "params" : [ 1, 2 ] + "dsaparams" : [ 1, 2 ] }, "adrender" : 1 }, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json index cd6df8e9e85..b4895753bb0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json @@ -41,19 +41,19 @@ "ext" : { "gdpr" : 0, "dsa": { - "required": 1, + "dsarequired": 1, "pubrender": 0, "datatopub": 2, "transparency": [ { "domain": "platform1domain.com", - "params": [ + "dsaparams": [ 1 ] }, { "domain": "SSP2domain.com", - "params": [ + "dsaparams": [ 1, 2 ] diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-response.json index 184a787e1b0..58f17fa4591 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-response.json @@ -19,7 +19,7 @@ "paid": "Advertiser", "transparency": { "domain": "dsp1domain.com", - "params": [ + "dsaparams": [ 1, 2 ] From 774166d8bb8c4c94a994ef2b99dc195e48790c24 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Thu, 8 Feb 2024 15:24:42 +0100 Subject: [PATCH 6/7] DSA fields in camelcase --- .../server/auction/ExchangeService.java | 2 +- .../proto/openrtb/ext/request/ExtRegsDsa.java | 10 +++++-- .../ext/request/ExtRegsDsaTransparency.java | 4 ++- .../request/auction/DsaTransparency.groovy | 5 +++- .../model/request/auction/RegsDsa.groovy | 4 +-- .../server/functional/tests/DsaSpec.groovy | 28 +++++++++---------- 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index f7c30b8cc2c..0385f5c1028 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -1557,7 +1557,7 @@ private static boolean isDsaValidationRequired(BidRequest bidRequest) { return Optional.ofNullable(bidRequest.getRegs()) .map(Regs::getExt) .map(ExtRegs::getDsa) - .map(ExtRegsDsa::getDsarequired) + .map(ExtRegsDsa::getDsaRequired) .map(DSA_REQUIRED::contains) .orElse(false); } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java index 5772a46a255..3dc3211097f 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java @@ -1,5 +1,6 @@ package org.prebid.server.proto.openrtb.ext.request; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; import java.util.List; @@ -13,17 +14,20 @@ public class ExtRegsDsa { /** * Defines the contract for bidrequest.regs.ext.dsa.dsarequired */ - Integer dsarequired; + @JsonProperty("dsarequired") + Integer dsaRequired; /** * Defines the contract for bidrequest.regs.ext.dsa.pubrender */ - Integer pubrender; + @JsonProperty("pubrender") + Integer pubRender; /** * Defines the contract for bidrequest.regs.ext.dsa.datatopub */ - Integer datatopub; + @JsonProperty("datatopub") + Integer dataToPub; /** * Defines the contract for bidrequest.regs.ext.dsa.transparency[] diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java index 5ace29f9280..6542326bd5c 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java @@ -1,5 +1,6 @@ package org.prebid.server.proto.openrtb.ext.request; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; import java.util.List; @@ -18,5 +19,6 @@ public class ExtRegsDsaTransparency { /** * Defines the contract for bidrequest.regs.ext.dsa.transparency[i].dsaparams[] */ - List dsaparams; + @JsonProperty("dsaparams") + List dsaParams; } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy index 2942a205a42..c396f99eeba 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy @@ -1,13 +1,16 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @ToString(includeNames = true, ignoreNulls = true) class DsaTransparency { String domain - List dsaparams + List dsaParams static DsaTransparency getDefaultRegsDsaTransparency() { new DsaTransparency(domain: PBSUtils.randomString) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy index ea413276f4e..98db2f6469c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy @@ -9,14 +9,14 @@ import org.prebid.server.functional.util.PBSUtils @ToString(includeNames = true, ignoreNulls = true) class RegsDsa { - Integer dsarequired + Integer dsaRequired Integer pubRender Integer dataToPub List transparency static RegsDsa getDefaultRegsDsa(ReqsDsaRequiredType required) { new RegsDsa( - dsarequired: required.value, + dsaRequired: required.value, pubRender: PBSUtils.getRandomNumber(0, 2), dataToPub: PBSUtils.getRandomNumber(0, 2), transparency: [DsaTransparency.defaultRegsDsaTransparency] diff --git a/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy index 620c431ab0e..90f90c905dd 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy @@ -43,11 +43,11 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired + bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams + bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams } and: "Bidder response should not contain DSA" @@ -94,11 +94,11 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired + bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams + bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams } and: "Bidder response should contain DSA" @@ -106,7 +106,7 @@ class DsaSpec extends BaseSpec { def actualDsa = bidderResponse.seatbid[0].bid[0].ext.dsa verifyAll { actualDsa.transparency[0].domain == bidDsa.transparency[0].domain - actualDsa.transparency[0].dsaparams == bidDsa.transparency[0].dsaparams + actualDsa.transparency[0].dsaParams == bidDsa.transparency[0].dsaParams actualDsa.adrender == bidDsa.adrender actualDsa.behalf == bidDsa.behalf actualDsa.paid == bidDsa.paid @@ -149,11 +149,11 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired + bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams + bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams } and: "Bidder response should not contain DSA" @@ -192,11 +192,11 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(bidRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired + bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams + bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams } and: "DSA is not returned" @@ -228,18 +228,18 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(bidRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired + bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams + bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams } and: "DSA is not returned" def actualDsa = response.seatbid[0].bid[0].ext.dsa verifyAll { actualDsa.transparency[0].domain == bidDsa.transparency[0].domain - actualDsa.transparency[0].dsaparams == bidDsa.transparency[0].dsaparams + actualDsa.transparency[0].dsaParams == bidDsa.transparency[0].dsaParams actualDsa.adrender == bidDsa.adrender actualDsa.behalf == bidDsa.behalf actualDsa.paid == bidDsa.paid @@ -270,11 +270,11 @@ class DsaSpec extends BaseSpec { then: "Bidder request should contain DSA" def bidderRequests = bidder.getBidderRequest(bidRequest.id) verifyAll { - bidderRequests.regs.ext.dsa.dsarequired == dsa.dsarequired + bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaparams == dsa.transparency[0].dsaparams + bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams } and: "Response should contain error" From aaa4cc7ccaea1e2b6244e109d74f0def9c21576c Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko Date: Mon, 12 Feb 2024 07:27:20 -0500 Subject: [PATCH 7/7] Feature: Default account DSA support (#2955) --- .../requestfactory/Ortb2RequestFactory.java | 63 ++- .../EnrichingApplicationSettings.java | 1 + .../settings/model/AccountDsaConfig.java | 14 + .../settings/model/AccountPrivacyConfig.java | 2 + .../server/settings/model/DefaultDsa.java | 21 + .../settings/model/DsaTransparency.java | 15 + .../model/config/AccountDsaConfig.groovy | 16 + .../model/config/AccountPrivacyConfig.groovy | 1 + .../model/request/auction/Dsa.groovy | 25 ++ .../model/request/auction/DsaDataToPub.groovy | 19 + .../model/request/auction/DsaPubRender.groovy | 19 + .../model/request/auction/DsaRequired.groovy | 20 + .../request/auction/DsaTransparency.groovy | 7 +- .../auction/DsaTransparencyParam.groovy | 19 + .../model/request/auction/RegsDsa.groovy | 25 -- .../model/request/auction/RegsExt.groovy | 3 +- .../auction/ReqsDsaRequiredType.groovy | 15 - .../model/response/auction/BidExt.groovy | 3 +- .../model/response/auction/BidExtDsa.groovy | 23 - .../model/response/auction/Dsa.groovy | 28 ++ .../model/response/auction/DsaAdRender.groovy | 18 + .../server/functional/tests/DsaSpec.groovy | 293 ------------ .../functional/tests/privacy/DsaSpec.groovy | 424 ++++++++++++++++++ .../tests/privacy/PrivacyBaseSpec.groovy | 5 + .../ActivityInfrastructureCreatorTest.java | 10 +- ...countActivitiesConfigurationUtilsTest.java | 11 +- .../PrivacyEnforcementServiceTest.java | 11 +- .../Ortb2RequestFactoryTest.java | 302 +++++++++++++ .../server/handler/CookieSyncHandlerTest.java | 2 +- .../server/handler/SetuidHandlerTest.java | 2 +- .../EnrichingApplicationSettingsTest.java | 4 + .../settings/FileApplicationSettingsTest.java | 1 + .../settings/HttpApplicationSettingsTest.java | 2 +- .../settings/JdbcApplicationSettingsTest.java | 1 + 34 files changed, 1055 insertions(+), 370 deletions(-) create mode 100644 src/main/java/org/prebid/server/settings/model/AccountDsaConfig.java create mode 100644 src/main/java/org/prebid/server/settings/model/DefaultDsa.java create mode 100644 src/main/java/org/prebid/server/settings/model/DsaTransparency.java create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/AccountDsaConfig.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/Dsa.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/DsaDataToPub.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/DsaPubRender.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/DsaRequired.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparencyParam.groovy delete mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/ReqsDsaRequiredType.groovy delete mode 100644 src/test/groovy/org/prebid/server/functional/model/response/auction/BidExtDsa.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/response/auction/Dsa.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/response/auction/DsaAdRender.groovy delete mode 100644 src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index b1178d3081c..003e9d2204c 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -6,6 +6,7 @@ import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; import io.vertx.core.Future; import io.vertx.core.MultiMap; @@ -13,6 +14,7 @@ import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; @@ -52,6 +54,9 @@ import org.prebid.server.proto.openrtb.ext.FlexibleExtension; import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; @@ -59,8 +64,11 @@ import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountDsaConfig; +import org.prebid.server.settings.model.AccountPrivacyConfig; import org.prebid.server.settings.model.AccountStatus; import org.prebid.server.settings.model.AccountTargetingConfig; +import org.prebid.server.settings.model.DefaultDsa; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; import org.prebid.server.validation.RequestValidator; @@ -217,16 +225,69 @@ public BidRequest enrichBidRequestWithAccountAndPrivacyData(AuctionContext aucti final Device device = bidRequest.getDevice(); final Device enrichedDevice = enrichDevice(device, privacyContext); - if (enrichedRequestExt != null || enrichedDevice != null) { + final Regs regs = bidRequest.getRegs(); + final Regs enrichedRegs = enrichRegs(regs, privacyContext, account); + + if (enrichedRequestExt != null || enrichedDevice != null || enrichedRegs != null) { return bidRequest.toBuilder() .ext(ObjectUtils.defaultIfNull(enrichedRequestExt, requestExt)) .device(ObjectUtils.defaultIfNull(enrichedDevice, device)) + .regs(ObjectUtils.defaultIfNull(enrichedRegs, regs)) .build(); } return bidRequest; } + private static Regs enrichRegs(Regs regs, PrivacyContext privacyContext, Account account) { + final ExtRegs regsExt = regs != null ? regs.getExt() : null; + final ExtRegsDsa regsExtDsa = regsExt != null ? regsExt.getDsa() : null; + if (regsExtDsa != null) { + return null; + } + + final AccountDsaConfig accountDsaConfig = Optional.ofNullable(account) + .map(Account::getPrivacy) + .map(AccountPrivacyConfig::getDsa) + .orElse(null); + final DefaultDsa defaultDsa = accountDsaConfig != null ? accountDsaConfig.getDefaultDsa() : null; + if (defaultDsa == null) { + return null; + } + + final boolean isGdprOnly = BooleanUtils.isTrue(accountDsaConfig.getGdprOnly()); + if (isGdprOnly && !privacyContext.getTcfContext().isInGdprScope()) { + return null; + } + + return Optional.ofNullable(regs) + .map(Regs::toBuilder) + .orElseGet(Regs::builder) + .ext(mapRegsExtDsa(defaultDsa, regsExt)) + .build(); + } + + private static ExtRegs mapRegsExtDsa(DefaultDsa defaultDsa, ExtRegs regsExt) { + final List enrichedDsaTransparencies = defaultDsa.getTransparency() + .stream() + .map(dsaTransparency -> ExtRegsDsaTransparency.of( + dsaTransparency.getDomain(), dsaTransparency.getDsaParams())) + .toList(); + + final ExtRegsDsa enrichedRegsExtDsa = ExtRegsDsa.of( + defaultDsa.getDsaRequired(), + defaultDsa.getPubRender(), + defaultDsa.getDataToPub(), + enrichedDsaTransparencies); + + final boolean isRegsExtPresent = regsExt != null; + return ExtRegs.of( + isRegsExtPresent ? regsExt.getGdpr() : null, + isRegsExtPresent ? regsExt.getUsPrivacy() : null, + isRegsExtPresent ? regsExt.getGpc() : null, + enrichedRegsExtDsa); + } + public Future executeEntrypointHooks(RoutingContext routingContext, String body, AuctionContext auctionContext) { diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java index 30238a6a761..79d2bcb7ce7 100644 --- a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java @@ -129,6 +129,7 @@ private Account validateAndModifyAccount(Account account) { .privacy(AccountPrivacyConfig.of( accountPrivacyConfig.getGdpr(), accountPrivacyConfig.getCcpa(), + accountPrivacyConfig.getDsa(), AccountActivitiesConfigurationUtils .removeInvalidRules(accountPrivacyConfig.getActivities()), accountPrivacyConfig.getModules())) diff --git a/src/main/java/org/prebid/server/settings/model/AccountDsaConfig.java b/src/main/java/org/prebid/server/settings/model/AccountDsaConfig.java new file mode 100644 index 00000000000..59d8913340e --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountDsaConfig.java @@ -0,0 +1,14 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountDsaConfig { + + @JsonProperty("default") + DefaultDsa defaultDsa; + + @JsonProperty("gdpr-only") + Boolean gdprOnly; +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountPrivacyConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPrivacyConfig.java index 77e83aadda7..4542114aa95 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountPrivacyConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountPrivacyConfig.java @@ -16,6 +16,8 @@ public class AccountPrivacyConfig { AccountCcpaConfig ccpa; + AccountDsaConfig dsa; + @JsonProperty("allowactivities") Map activities; diff --git a/src/main/java/org/prebid/server/settings/model/DefaultDsa.java b/src/main/java/org/prebid/server/settings/model/DefaultDsa.java new file mode 100644 index 00000000000..16d6e2cc29f --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/DefaultDsa.java @@ -0,0 +1,21 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class DefaultDsa { + + @JsonProperty("dsarequired") + Integer dsaRequired; + + @JsonProperty("pubrender") + Integer pubRender; + + @JsonProperty("datatopub") + Integer dataToPub; + + List transparency; +} diff --git a/src/main/java/org/prebid/server/settings/model/DsaTransparency.java b/src/main/java/org/prebid/server/settings/model/DsaTransparency.java new file mode 100644 index 00000000000..c3a4c6d8b22 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/DsaTransparency.java @@ -0,0 +1,15 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class DsaTransparency { + + String domain; + + @JsonProperty("dsaparams") + List dsaParams; +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountDsaConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountDsaConfig.groovy new file mode 100644 index 00000000000..ef134c916fd --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountDsaConfig.groovy @@ -0,0 +1,16 @@ +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 +import org.prebid.server.functional.model.request.auction.Dsa + +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +@ToString(includeNames = true, ignoreNulls = true) +class AccountDsaConfig { + + @JsonProperty("default") + Dsa defaultDsa + Boolean gdprOnly +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountPrivacyConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountPrivacyConfig.groovy index 1fdd4ba6cc9..b6f2819adf4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountPrivacyConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountPrivacyConfig.groovy @@ -12,6 +12,7 @@ class AccountPrivacyConfig { AccountGdprConfig gdpr AccountCcpaConfig ccpa + AccountDsaConfig dsa @JsonProperty("allowactivities") AllowActivities allowActivities List modules diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Dsa.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Dsa.groovy new file mode 100644 index 00000000000..ed0beafdc31 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Dsa.groovy @@ -0,0 +1,25 @@ +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 + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@EqualsAndHashCode +@ToString(includeNames = true, ignoreNulls = true) +class Dsa { + + DsaRequired dsaRequired + DsaPubRender pubRender + DsaDataToPub dataToPub + List transparency + + static Dsa getDefaultDsa(DsaRequired dsaRequired = PBSUtils.getRandomEnum(DsaRequired)) { + new Dsa(dsaRequired: dsaRequired, + pubRender: PBSUtils.getRandomEnum(DsaPubRender), + dataToPub: PBSUtils.getRandomEnum(DsaDataToPub), + transparency: [DsaTransparency.defaultDsaTransparency]) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaDataToPub.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaDataToPub.groovy new file mode 100644 index 00000000000..e2039ce523b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaDataToPub.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum DsaDataToPub { + + DO_NOT_SEND_TRANSPARENCY(0), + OPTIONAL_TO_SEND(1), + SEND_TRANSPARENCY(2) + + @JsonValue + final int value + + private DsaDataToPub(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaPubRender.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaPubRender.groovy new file mode 100644 index 00000000000..d8c46588e0a --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaPubRender.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum DsaPubRender { + + PUB_CANT_RENDER(0), + PUB_MIGHT_RENDER(1), + PUB_WILL_RENDER(2) + + @JsonValue + final int value + + private DsaPubRender(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaRequired.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaRequired.groovy new file mode 100644 index 00000000000..e57f6a62e5b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaRequired.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum DsaRequired { + + NOT_REQUIRED(0), + SUPPORTED(1), + REQUIRED(2), + REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM(3) + + @JsonValue + final int value + + private DsaRequired(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy index c396f99eeba..f0a0ece5483 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparency.groovy @@ -2,18 +2,19 @@ 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 @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class DsaTransparency { String domain - List dsaParams + List dsaParams - static DsaTransparency getDefaultRegsDsaTransparency() { + static DsaTransparency getDefaultDsaTransparency() { new DsaTransparency(domain: PBSUtils.randomString) } } - diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparencyParam.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparencyParam.groovy new file mode 100644 index 00000000000..e567f6d52ef --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DsaTransparencyParam.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum DsaTransparencyParam { + + PROFILING(1), + BASIC_ADVERTISING(2), + PRECISE_GEO(3) + + @JsonValue + final int value + + private DsaTransparencyParam(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy index 98db2f6469c..e69de29bb2d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy @@ -1,25 +0,0 @@ -package org.prebid.server.functional.model.request.auction - -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming -import groovy.transform.ToString -import org.prebid.server.functional.util.PBSUtils - -@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) -@ToString(includeNames = true, ignoreNulls = true) -class RegsDsa { - - Integer dsaRequired - Integer pubRender - Integer dataToPub - List transparency - - static RegsDsa getDefaultRegsDsa(ReqsDsaRequiredType required) { - new RegsDsa( - dsaRequired: required.value, - pubRender: PBSUtils.getRandomNumber(0, 2), - dataToPub: PBSUtils.getRandomNumber(0, 2), - transparency: [DsaTransparency.defaultRegsDsaTransparency] - ) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy index 0ec8962c778..f235dfbd600 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy @@ -11,6 +11,5 @@ class RegsExt { Integer gdpr String usPrivacy String gpc - RegsDsa dsa - + Dsa dsa } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ReqsDsaRequiredType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ReqsDsaRequiredType.groovy deleted file mode 100644 index 2cb07b350d4..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ReqsDsaRequiredType.groovy +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.functional.model.request.auction - -import com.fasterxml.jackson.annotation.JsonValue - -enum ReqsDsaRequiredType { - - NOT_REQUIRED(0), SUPPORTED(1), REQUIRED(2), REQUIRED_PUBLISHER_ONLINE_PLATFORM(3) - - @JsonValue - final Integer value - - ReqsDsaRequiredType(Integer value) { - this.value = value - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy index 769f6ff88ff..216b78b3609 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy @@ -9,6 +9,5 @@ class BidExt { Prebid prebid BigDecimal origbidcpm Currency origbidcur - BidExtDsa dsa - + Dsa dsa } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExtDsa.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExtDsa.groovy deleted file mode 100644 index 4712c59e76e..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExtDsa.groovy +++ /dev/null @@ -1,23 +0,0 @@ -package org.prebid.server.functional.model.response.auction - -import groovy.transform.ToString -import org.prebid.server.functional.model.request.auction.DsaTransparency -import org.prebid.server.functional.util.PBSUtils - -@ToString(includeNames = true, ignoreNulls = true) -class BidExtDsa { - - String behalf - String paid - List transparency - Integer adrender - - static BidExtDsa getDefaultBidExtDsa() { - new BidExtDsa( - behalf: PBSUtils.randomString, - paid: PBSUtils.randomString, - adrender: PBSUtils.getRandomNumber(0, 2), - transparency: [DsaTransparency.defaultRegsDsaTransparency] - ) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Dsa.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Dsa.groovy new file mode 100644 index 00000000000..172955e2e18 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Dsa.groovy @@ -0,0 +1,28 @@ +package org.prebid.server.functional.model.response.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.model.request.auction.DsaTransparency +import org.prebid.server.functional.util.PBSUtils + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@EqualsAndHashCode +@ToString(includeNames = true, ignoreNulls = true) +class Dsa { + + String behalf + String paid + List transparency + DsaAdRender adRender + + static Dsa getDefaultDsa() { + new Dsa( + behalf: PBSUtils.randomString, + paid: PBSUtils.randomString, + adRender: PBSUtils.getRandomEnum(DsaAdRender), + transparency: [DsaTransparency.defaultDsaTransparency] + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/DsaAdRender.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/DsaAdRender.groovy new file mode 100644 index 00000000000..555310c9f7b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/DsaAdRender.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum DsaAdRender { + + ADVERTISER_WONT_RENDER(0), + ADVERTISER_WILL_RENDER(1) + + @JsonValue + final int value + + private DsaAdRender(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy deleted file mode 100644 index 90f90c905dd..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/DsaSpec.groovy +++ /dev/null @@ -1,293 +0,0 @@ -package org.prebid.server.functional.tests - -import org.prebid.server.functional.model.bidder.BidderName -import org.prebid.server.functional.model.db.StoredRequest -import org.prebid.server.functional.model.request.amp.AmpRequest -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.auction.RegsDsa -import org.prebid.server.functional.model.request.auction.ReqsDsaRequiredType -import org.prebid.server.functional.model.response.auction.BidExt -import org.prebid.server.functional.model.response.auction.BidExtDsa -import org.prebid.server.functional.model.response.auction.BidResponse - -import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC - -class DsaSpec extends BaseSpec { - - def "AMP request should send DSA to bidder and succeed when DSA is not required"() { - given: "Default AmpRequest" - def ampRequest = AmpRequest.defaultAmpRequest - def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) - - and: "Stored default bid request with DSA" - def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa - setAccountId(ampRequest.account) - } - - and: "Save storedRequest into DB" - def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) - storedRequestDao.save(storedRequest) - - and: "Default bidder response with DSA" - def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest).tap { - seatbid[0].bid[0].ext = bidExt - } - - and: "Set bidder response" - bidder.setResponse(ampStoredRequest.id, bidResponse) - - when: "PBS processes amp request" - def response = defaultPbsService.sendAmpRequest(ampRequest) - - then: "Bidder request should contain DSA" - def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { - bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired - bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub - bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender - bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams - } - - and: "Bidder response should not contain DSA" - def bidderResponse = decode(response.ext.debug.httpcalls.get(BidderName.GENERIC.value)[0].responseBody, BidResponse) - assert !bidderResponse.seatbid[0].bid[0].ext?.dsa - - and: "PBS should not log warning" - assert !response.ext.warnings - assert !response.ext.errors - - where: - dsaRequired | bidExt - ReqsDsaRequiredType.NOT_REQUIRED | null - ReqsDsaRequiredType.SUPPORTED | new BidExt(dsa: null) - } - - def "AMP request should send DSA to bidder and always succeed when bidder returns DSA"() { - given: "Default AmpRequest" - def ampRequest = AmpRequest.defaultAmpRequest - def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) - - and: "Stored default bid request with DSA" - def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa - setAccountId(ampRequest.account) - } - - and: "Save storedRequest into DB" - def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) - storedRequestDao.save(storedRequest) - - and: "Default bidder response with DSA" - def bidDsa = BidExtDsa.getDefaultBidExtDsa() - def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest).tap { - seatbid[0].bid[0].ext = new BidExt(dsa: bidDsa) - } - - and: "Set bidder response" - bidder.setResponse(ampStoredRequest.id, bidResponse) - - when: "PBS processes amp request" - def response = defaultPbsService.sendAmpRequest(ampRequest) - - then: "Bidder request should contain DSA" - def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { - bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired - bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub - bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender - bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams - } - - and: "Bidder response should contain DSA" - def bidderResponse = decode(response.ext.debug.httpcalls.get(BidderName.GENERIC.value)[0].responseBody, BidResponse) - def actualDsa = bidderResponse.seatbid[0].bid[0].ext.dsa - verifyAll { - actualDsa.transparency[0].domain == bidDsa.transparency[0].domain - actualDsa.transparency[0].dsaParams == bidDsa.transparency[0].dsaParams - actualDsa.adrender == bidDsa.adrender - actualDsa.behalf == bidDsa.behalf - actualDsa.paid == bidDsa.paid - } - - and: "PBS should not log warning" - assert !response.ext.warnings - assert !response.ext.errors - - where: - dsaRequired << ReqsDsaRequiredType.values() - } - - def "AMP request should send DSA to bidder and fail on response when DSA is required and bidder does not return DSA"() { - given: "Default AmpRequest" - def ampRequest = AmpRequest.defaultAmpRequest - def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) - - and: "Stored default bid request with DSA" - def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa - setAccountId(ampRequest.account) - } - - and: "Save storedRequest into DB" - def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) - storedRequestDao.save(storedRequest) - - and: "Default bidder response with DSA" - def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest).tap { - seatbid[0].bid[0].ext = bidExt - } - - and: "Set bidder response" - bidder.setResponse(ampStoredRequest.id, bidResponse) - - when: "PBS processes amp request" - def response = defaultPbsService.sendAmpRequest(ampRequest) - - then: "Bidder request should contain DSA" - def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { - bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired - bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub - bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender - bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams - } - - and: "Bidder response should not contain DSA" - def bidderResponse = decode(response.ext.debug.httpcalls.get(BidderName.GENERIC.value)[0].responseBody, BidResponse) - assert !bidderResponse.seatbid[0].bid[0].ext?.dsa - - and: "Response should contain error" - def expectedBidId = bidResponse.seatbid[0].bid[0].id - assert response.ext?.errors[GENERIC]*.code == [5] - assert response.ext?.errors[GENERIC]*.message == ["BidId `$expectedBidId` validation messages: Error: Bid \"$expectedBidId\" missing DSA"] - - where: - dsaRequired | bidExt - ReqsDsaRequiredType.REQUIRED | new BidExt(dsa: null) - ReqsDsaRequiredType.REQUIRED_PUBLISHER_ONLINE_PLATFORM | null - } - - def "Auction request should send DSA to bidder and succeeds when DSA is not required and bidder does not return DSA"() { - given: "Default bid request with DSA" - def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) - def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa - } - - and: "Default bidder response with DSA" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].ext = new BidExt(dsa: null) - } - - and: "Set bidder response" - bidder.setResponse(bidRequest.id, bidResponse) - - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder request should contain DSA" - def bidderRequests = bidder.getBidderRequest(bidRequest.id) - verifyAll { - bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired - bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub - bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender - bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams - } - - and: "DSA is not returned" - assert !response.seatbid[0].bid[0].ext.dsa - - where: - dsaRequired << [ReqsDsaRequiredType.NOT_REQUIRED, ReqsDsaRequiredType.SUPPORTED] - } - - def "Auction request should send DSA to bidder and always succeed when bidder returns DSA"() { - given: "Default bid request with DSA" - def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) - def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa - } - - and: "Default bidder response with DSA" - def bidDsa = BidExtDsa.getDefaultBidExtDsa() - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].ext = new BidExt(dsa: bidDsa) - } - - and: "Set bidder response" - bidder.setResponse(bidRequest.id, bidResponse) - - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder request should contain DSA" - def bidderRequests = bidder.getBidderRequest(bidRequest.id) - verifyAll { - bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired - bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub - bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender - bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams - } - - and: "DSA is not returned" - def actualDsa = response.seatbid[0].bid[0].ext.dsa - verifyAll { - actualDsa.transparency[0].domain == bidDsa.transparency[0].domain - actualDsa.transparency[0].dsaParams == bidDsa.transparency[0].dsaParams - actualDsa.adrender == bidDsa.adrender - actualDsa.behalf == bidDsa.behalf - actualDsa.paid == bidDsa.paid - } - - where: - dsaRequired << ReqsDsaRequiredType.values() - } - - def "Auction request should send DSA to bidder and fail on response when DSA is required and bidder does not return DSA"() { - given: "Default bid request with DSA" - def dsa = RegsDsa.getDefaultRegsDsa(dsaRequired) - def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa - } - - and: "Default bidder response with DSA" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].ext = bidExt - } - - and: "Set bidder response" - bidder.setResponse(bidRequest.id, bidResponse) - - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder request should contain DSA" - def bidderRequests = bidder.getBidderRequest(bidRequest.id) - verifyAll { - bidderRequests.regs.ext.dsa.dsaRequired == dsa.dsaRequired - bidderRequests.regs.ext.dsa.dataToPub == dsa.dataToPub - bidderRequests.regs.ext.dsa.pubRender == dsa.pubRender - bidderRequests.regs.ext.dsa.transparency[0].domain == dsa.transparency[0].domain - bidderRequests.regs.ext.dsa.transparency[0].dsaParams == dsa.transparency[0].dsaParams - } - - and: "Response should contain error" - def expectedBidId = bidResponse.seatbid[0].bid[0].id - verifyAll { - response.seatbid.isEmpty() - response.ext?.errors[GENERIC]*.code == [5] - response.ext?.errors[GENERIC]*.message == ["BidId `$expectedBidId` validation messages: Error: Bid \"$expectedBidId\" missing DSA"] - } - - where: - dsaRequired | bidExt - ReqsDsaRequiredType.REQUIRED | new BidExt(dsa: null) - ReqsDsaRequiredType.REQUIRED_PUBLISHER_ONLINE_PLATFORM | null - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy new file mode 100644 index 00000000000..76e669f7262 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy @@ -0,0 +1,424 @@ +package org.prebid.server.functional.tests.privacy + +import org.prebid.server.functional.model.config.AccountDsaConfig +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.request.amp.AmpRequest +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Dsa +import org.prebid.server.functional.model.request.auction.Dsa as RequestDsa +import org.prebid.server.functional.model.request.auction.DsaRequired +import org.prebid.server.functional.model.response.auction.BidExt +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.Dsa as BidDsa +import org.prebid.server.functional.util.PBSUtils +import org.prebid.server.functional.util.privacy.TcfConsent + +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC +import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID +import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS + +class DsaSpec extends PrivacyBaseSpec { + + def "AMP request should always forward DSA to bidders"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default stored request with DSA" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + setAccountId(ampRequest.account) + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain DSA" + assert bidder.getBidderRequest(ampStoredRequest.id).regs?.ext?.dsa == dsa + + where: + dsa << [null, + new RequestDsa(), + RequestDsa.getDefaultDsa(DsaRequired.NOT_REQUIRED), + RequestDsa.getDefaultDsa(DsaRequired.SUPPORTED), + RequestDsa.getDefaultDsa(DsaRequired.REQUIRED), + RequestDsa.getDefaultDsa(DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] + } + + def "AMP request should always accept bids with DSA"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default stored request with DSA" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + setAccountId(ampRequest.account) + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Default bidder response with DSA" + def bidDsa = BidDsa.defaultDsa + def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest).tap { + seatbid[0].bid[0].ext = new BidExt(dsa: bidDsa) + } + + and: "Set bidder response" + bidder.setResponse(ampStoredRequest.id, bidResponse) + + when: "PBS processes amp request" + def response = privacyPbsService.sendAmpRequest(ampRequest) + + then: "PBS should return bid" + assert response.targeting + + and: "PBS should not log warning" + assert !response.ext.warnings + assert !response.ext.errors + + where: + dsa << [null, + new RequestDsa(), + RequestDsa.getDefaultDsa(DsaRequired.NOT_REQUIRED), + RequestDsa.getDefaultDsa(DsaRequired.SUPPORTED), + RequestDsa.getDefaultDsa(DsaRequired.REQUIRED), + RequestDsa.getDefaultDsa(DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] + } + + def "AMP request should accept bids without DSA when dsarequired is #dsaRequired"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + def dsa = RequestDsa.getDefaultDsa(dsaRequired) + + and: "Default stored request with DSA" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + setAccountId(ampRequest.account) + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Default bidder response with DSA" + def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest).tap { + seatbid[0].bid[0].ext = new BidExt(dsa: null) + } + + and: "Set bidder response" + bidder.setResponse(ampStoredRequest.id, bidResponse) + + when: "PBS processes amp request" + def response = privacyPbsService.sendAmpRequest(ampRequest) + + then: "PBS should return bid" + assert response.targeting + + and: "PBS should not log warning" + assert !response.ext.warnings + assert !response.ext.errors + + where: + dsaRequired << [DsaRequired.NOT_REQUIRED, + DsaRequired.SUPPORTED] + } + + def "AMP request should reject bids without DSA when dsarequired is #dsaRequired"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + def dsa = RequestDsa.getDefaultDsa(dsaRequired) + + and: "Default stored bid request with DSA" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + setAccountId(ampRequest.account) + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Default bidder response without DSA" + def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest).tap { + seatbid[0].bid[0].ext = new BidExt(dsa: null) + } + + and: "Set bidder response" + bidder.setResponse(ampStoredRequest.id, bidResponse) + + when: "PBS processes amp request" + def response = privacyPbsService.sendAmpRequest(ampRequest) + + then: "PBS should reject bid" + assert !response.targeting + + and: "Response should contain an error" + def bidId = bidResponse.seatbid[0].bid[0].id + assert response.ext?.errors[GENERIC]*.code == [5] + assert response.ext?.errors[GENERIC]*.message == ["BidId `$bidId` validation messages: Error: Bid \"$bidId\" missing DSA"] + + where: + dsaRequired << [DsaRequired.REQUIRED, + DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM] + } + + def "Auction request should always forward DSA to bidders"() { + given: "Default bid request with DSA" + def bidRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + } + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain DSA" + assert bidder.getBidderRequest(bidRequest.id).regs?.ext?.dsa == dsa + + where: + dsa << [null, + new RequestDsa(), + RequestDsa.getDefaultDsa(DsaRequired.NOT_REQUIRED), + RequestDsa.getDefaultDsa(DsaRequired.SUPPORTED), + RequestDsa.getDefaultDsa(DsaRequired.REQUIRED), + RequestDsa.getDefaultDsa(DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] + } + + def "Auction request should always accept bids with DSA"() { + given: "Default bid request with DSA" + def bidRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = dsa + } + + and: "Default bidder response with DSA" + def bidDsa = BidDsa.defaultDsa + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].ext = new BidExt(dsa: bidDsa) + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should return bid" + assert response.seatbid.bid + + and: "Returned bid should contain DSA" + assert response.seatbid[0].bid[0].ext.dsa == bidDsa + + and: "PBS should not log warning" + assert !response.ext.warnings + assert !response.ext.errors + + where: + dsa << [null, + new RequestDsa(), + RequestDsa.getDefaultDsa(DsaRequired.NOT_REQUIRED), + RequestDsa.getDefaultDsa(DsaRequired.SUPPORTED), + RequestDsa.getDefaultDsa(DsaRequired.REQUIRED), + RequestDsa.getDefaultDsa(DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] + } + + def "Auction request should accept bids without DSA when dsarequired is #dsaRequired"() { + given: "Default bid request with DSA" + def bidRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired) + } + + and: "Default bidder response with DSA" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].ext = new BidExt(dsa: null) + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should return bid" + assert response.seatbid.bid + + and: "PBS should not log warning" + assert !response.ext.warnings + assert !response.ext.errors + + where: + dsaRequired << [DsaRequired.NOT_REQUIRED, + DsaRequired.SUPPORTED] + } + + def "Auction request should reject bids without DSA when dsarequired is #dsaRequired"() { + given: "Default bid request with DSA" + def bidRequest = BidRequest.defaultBidRequest.tap { + regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired) + } + + and: "Default bidder response without DSA" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].ext = new BidExt(dsa: null) + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject bid" + assert !response.seatbid + + and: "Response should contain an error" + def bidId = bidResponse.seatbid[0].bid[0].id + assert response.ext?.errors[GENERIC]*.code == [5] + assert response.ext?.errors[GENERIC]*.message == ["BidId `$bidId` validation messages: Error: Bid \"$bidId\" missing DSA"] + + where: + dsaRequired << [DsaRequired.REQUIRED, + DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM] + } + + def "Auction request should set account DSA when BidRequest DSA is null"() { + given: "Default bid request without DSA" + def accountId = PBSUtils.randomNumber.toString() + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + regs.ext.dsa = null + } + + and: "Account with default DSA config" + def accountDsa = Dsa.defaultDsa + def account = getAccountWithDsa(accountId, new AccountDsaConfig(defaultDsa: accountDsa)) + accountDao.save(account) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain DSA from account config" + assert bidder.getBidderRequest(bidRequest.id).regs.ext.dsa == accountDsa + } + + def "Auction request shouldn't set account DSA when BidRequest DSA is not null"() { + given: "Default bid request with DSA" + def accountId = PBSUtils.randomNumber.toString() + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + regs.ext.dsa = requestDsa + } + + and: "Account with default DSA config" + def account = getAccountWithDsa(accountId, new AccountDsaConfig(defaultDsa: accountDsa)) + accountDao.save(account) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain DSA from request" + assert bidder.getBidderRequest(bidRequest.id).regs.ext.dsa == requestDsa + + where: + requestDsa || accountDsa + new Dsa() || null + new Dsa() || Dsa.defaultDsa + Dsa.defaultDsa || null + Dsa.defaultDsa || Dsa.defaultDsa + } + + def "Auction request shouldn't populate DSA when account DSA is null and request DSA is null"() { + given: "Default bid request without DSA" + def accountId = PBSUtils.randomNumber.toString() + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + regs.ext.dsa = null + } + + and: "Account without default DSA config" + def account = getAccountWithDsa(accountId, new AccountDsaConfig(defaultDsa: null)) + accountDao.save(account) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain DSA" + assert !bidder.getBidderRequest(bidRequest.id)?.regs?.ext?.dsa + } + + def "Auction request should set account DSA when gdpr-only is false and not in GDPR scope"() { + given: "Default bid request not in GDPR scope without DSA" + def accountId = PBSUtils.randomNumber.toString() + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + regs.ext.dsa = null + regs.ext.gdpr = 0 + } + + and: "Account with default DSA config" + def accountDsa = Dsa.defaultDsa + def account = getAccountWithDsa(accountId, + new AccountDsaConfig(defaultDsa: accountDsa, gdprOnly: false)) + accountDao.save(account) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain DSA from account config" + assert bidder.getBidderRequest(bidRequest.id).regs.ext.dsa == accountDsa + } + + def "Auction request should set account DSA when gdpr-only is #gdprOnly and in GDPR scope"() { + given: "Default bid request in GDPR scope with DSA" + def consentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + def accountId = PBSUtils.randomNumber.toString() + def bidRequest = getGdprBidRequest(consentString).tap { + setAccountId(accountId) + regs.ext.dsa = null + } + + and: "Account with default DSA config" + def accountDsa = Dsa.defaultDsa + def account = getAccountWithDsa(accountId, + new AccountDsaConfig(defaultDsa: accountDsa, gdprOnly: gdprOnly)) + accountDao.save(account) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain DSA from account config" + assert bidder.getBidderRequest(bidRequest.id).regs.ext.dsa == accountDsa + + where: + gdprOnly << [true, false] + } + + def "Auction request shouldn't set account DSA when gdpr-only is true and not in GDPR scope"() { + given: "Default bid request not in GDPR scope without DSA" + def accountId = PBSUtils.randomNumber.toString() + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + regs.ext.dsa = null + regs.ext.gdpr = 0 + } + + and: "Account with default DSA config" + def accountDsa = Dsa.defaultDsa + def account = getAccountWithDsa(accountId, + new AccountDsaConfig(defaultDsa: accountDsa, gdprOnly: true)) + accountDao.save(account) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain DSA" + assert !bidder.getBidderRequest(bidRequest.id)?.regs?.ext?.dsa + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy index 408e6e890a2..0b917c50055 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy @@ -4,6 +4,7 @@ import org.prebid.server.functional.model.config.AccountCcpaConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountCookieSyncConfig import org.prebid.server.functional.model.config.AccountCoopSyncConfig +import org.prebid.server.functional.model.config.AccountDsaConfig import org.prebid.server.functional.model.config.AccountGdprConfig import org.prebid.server.functional.model.config.AccountGppConfig import org.prebid.server.functional.model.config.AccountPrivacyConfig @@ -162,6 +163,10 @@ abstract class PrivacyBaseSpec extends BaseSpec { getAccountWithPrivacy(accountId, new AccountPrivacyConfig(ccpa: ccpaConfig)) } + protected static Account getAccountWithDsa(String accountId, AccountDsaConfig dsaConfig) { + getAccountWithPrivacy(accountId, new AccountPrivacyConfig(dsa: dsaConfig)) + } + private static Account getAccountWithPrivacy(String accountId, AccountPrivacyConfig privacy) { new Account(uuid: accountId, config: new AccountConfig(privacy: privacy)) } diff --git a/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java b/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java index 0931afe953e..ddef30e4900 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java @@ -73,7 +73,13 @@ public void parseShouldReturnExpectedResultIfAccountPrivacyNull() { @Test public void parseShouldReturnExpectedResultIfAccountPrivacyActivitiesNull() { // given - final Account account = Account.builder().privacy(AccountPrivacyConfig.of(null, null, null, null)).build(); + final Account account = Account.builder().privacy(AccountPrivacyConfig.of( + null, + null, + null, + null, + null)) + .build(); // when final Map controllers = creator.parse(account, null, debug); @@ -87,6 +93,7 @@ public void parseShouldSkipPrivacyModulesDuplicatesAndEmitWarnings() { // given final Account account = Account.builder() .privacy(AccountPrivacyConfig.of( + null, null, null, Map.of(Activity.SYNC_USER, AccountActivityConfiguration.of( @@ -109,6 +116,7 @@ public void parseShouldReturnExpectedResult() { // given final Account account = Account.builder() .privacy(AccountPrivacyConfig.of( + null, null, null, Map.of( diff --git a/src/test/java/org/prebid/server/activity/utils/AccountActivitiesConfigurationUtilsTest.java b/src/test/java/org/prebid/server/activity/utils/AccountActivitiesConfigurationUtilsTest.java index 020524a3b0f..38e85b72616 100644 --- a/src/test/java/org/prebid/server/activity/utils/AccountActivitiesConfigurationUtilsTest.java +++ b/src/test/java/org/prebid/server/activity/utils/AccountActivitiesConfigurationUtilsTest.java @@ -42,7 +42,13 @@ public void isInvalidActivitiesConfigurationShouldReturnFalseIfAccountPrivacyNul @Test public void isInvalidActivitiesConfigurationShouldReturnFalseIfAccountPrivacyActivitiesNull() { // given - final Account account = Account.builder().privacy(AccountPrivacyConfig.of(null, null, null, null)).build(); + final Account account = Account.builder().privacy(AccountPrivacyConfig.of( + null, + null, + null, + null, + null)) + .build(); // when final boolean result = AccountActivitiesConfigurationUtils.isInvalidActivitiesConfiguration(account); @@ -56,6 +62,7 @@ public void isInvalidActivitiesConfigurationShouldReturnFalseIfConfigurationVali // given final Account account = Account.builder() .privacy(AccountPrivacyConfig.of( + null, null, null, Map.of( @@ -98,6 +105,7 @@ public void isInvalidActivitiesConfigurationShouldReturnTrueOnInvalidComponentRu // given final Account account = Account.builder() .privacy(AccountPrivacyConfig.of( + null, null, null, Map.of(Activity.CALL_BIDDER, AccountActivityConfiguration.of(null, singletonList( @@ -119,6 +127,7 @@ public void isInvalidActivitiesConfigurationShouldReturnTrueOnInvalidGeoRule() { // given final Account account = Account.builder() .privacy(AccountPrivacyConfig.of( + null, null, null, Map.of(Activity.CALL_BIDDER, AccountActivityConfiguration.of(null, singletonList( diff --git a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java index 6b96611d462..7300468568a 100644 --- a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java +++ b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java @@ -532,6 +532,7 @@ public void shouldMaskForCcpaWhenAccountHasCppaConfigEnabledForRequestType() { EnabledForRequestType.of(false, false, true, false, false)) .build(), null, + null, null)) .build()) .requestTypeMetric(MetricName.openrtb2app) @@ -585,6 +586,7 @@ public void shouldMaskForCcpaWhenAccountHasCppaEnforcedTrue() { null, AccountCcpaConfig.builder().enabled(true).build(), null, + null, null)) .build()) .requestTypeMetric(MetricName.openrtb2app) @@ -638,6 +640,7 @@ public void shouldMaskForCcpaWhenAccountHasCcpaConfigEnabled() { null, AccountCcpaConfig.builder().enabled(true).build(), null, + null, null)) .build()) .requestTypeMetric(null) @@ -1518,7 +1521,12 @@ public void isCcpaEnforcedShouldReturnFalseWhenEnforcedPropertyIsTrueInConfigura final Ccpa ccpa = Ccpa.of("1YYY"); final Account account = Account.builder() - .privacy(AccountPrivacyConfig.of(null, AccountCcpaConfig.builder().enabled(false).build(), null, null)) + .privacy(AccountPrivacyConfig.of( + null, + AccountCcpaConfig.builder().enabled(false).build(), + null, + null, + null)) .build(); // when and then @@ -1552,6 +1560,7 @@ public void isCcpaEnforcedShouldReturnFalseWhenAccountCcpaConfigHasEnabledTrue() null, AccountCcpaConfig.builder().enabled(true).build(), null, + null, null)) .build(); diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java index a65a86cd29a..9b508cdbfd1 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java @@ -7,6 +7,7 @@ import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; import io.vertx.core.Future; import io.vertx.core.MultiMap; @@ -59,6 +60,9 @@ import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; @@ -66,8 +70,12 @@ import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountDsaConfig; +import org.prebid.server.settings.model.AccountPrivacyConfig; import org.prebid.server.settings.model.AccountStatus; import org.prebid.server.settings.model.AccountTargetingConfig; +import org.prebid.server.settings.model.DefaultDsa; +import org.prebid.server.settings.model.DsaTransparency; import org.prebid.server.validation.RequestValidator; import org.prebid.server.validation.model.ValidationResult; @@ -1452,6 +1460,300 @@ public void updateTimeoutShouldReturnContextWithUpdatedTimeoutAndBidRequestTmax( assertThat(result.getTimeout()).isEqualTo(updatedTimeout); } + @Test + public void enrichBidRequestWithAccountAndPrivacyDataShouldSetDsaFromAccountWhenRequestLacksDsa() { + // given + final String accountId = "accId"; + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(emptyList()) + .site(Site.builder() + .publisher(Publisher.builder().id(accountId).build()) + .build())); + + final PrivacyContext privacyContext = PrivacyContext.of( + Privacy.builder() + .gdpr("") + .consentString("") + .ccpa(Ccpa.EMPTY) + .coppa(0) + .build(), + TcfContext.empty(), + ""); + + final Account account = Account.builder() + .id(accountId) + .privacy(AccountPrivacyConfig.of(null, + null, + AccountDsaConfig.of(DefaultDsa.of(0, + 1, + 2, + List.of(DsaTransparency.of("", + List.of(0)))), + null), + null, + null)) + .build(); + given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account)); + + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .account(account) + .privacyContext(privacyContext) + .build(); + + // when + final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(auctionContext); + + // then + assertThat(result) + .extracting(BidRequest::getRegs) + .extracting(Regs::getExt) + .extracting(ExtRegs::getDsa) + .satisfies(dsa -> { + assertThat(dsa.getDsaRequired()).isEqualTo(0); + assertThat(dsa.getPubRender()).isEqualTo(1); + assertThat(dsa.getDataToPub()).isEqualTo(2); + assertThat(dsa.getTransparency()).satisfies(transparencies -> + assertThat(transparencies).isEqualTo(List.of(ExtRegsDsaTransparency.of("", + List.of(0))))); + }); + } + + @Test + public void enrichBidRequestWithAccountAndPrivacyDataShouldNotSetDsaFromAccountWhenAccountLacksDefaultDsa() { + // given + final String accountId = "accId"; + final Regs regs = Regs.builder().build(); + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(emptyList()) + .site(Site.builder() + .publisher(Publisher.builder().id(accountId).build()) + .build()) + .regs(regs)); + + final PrivacyContext privacyContext = PrivacyContext.of( + Privacy.builder() + .gdpr("") + .consentString("") + .ccpa(Ccpa.EMPTY) + .coppa(0) + .build(), + TcfContext.empty(), + ""); + + final Account account = Account.builder() + .id(accountId) + .privacy(AccountPrivacyConfig.of(null, + null, + AccountDsaConfig.of(null, + null), + null, + null)) + .build(); + given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account)); + + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .account(account) + .privacyContext(privacyContext) + .build(); + + // when + final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(auctionContext); + + // then + assertThat(result) + .extracting(BidRequest::getRegs) + .extracting(Regs::getExt) + .isNull(); + assertThat(result) + .extracting(BidRequest::getRegs) + .isSameAs(regs); + } + + @Test + public void enrichBidRequestWithAccountAndPrivacyDataShouldNotSetDsaFromAccountWhenRequestContainsDsa() { + // given + final String accountId = "accId"; + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(emptyList()) + .site(Site.builder() + .publisher(Publisher.builder().id(accountId).build()) + .build()) + .regs(Regs.builder().ext(ExtRegs.of(null, + null, + null, + ExtRegsDsa.of(0, + 1, + 2, + List.of(ExtRegsDsaTransparency.of("", List.of(0)))))) + .build()) + ); + + final PrivacyContext privacyContext = PrivacyContext.of( + Privacy.builder() + .gdpr("") + .consentString("") + .ccpa(Ccpa.EMPTY) + .coppa(0) + .build(), + TcfContext.empty(), + ""); + + final Account account = Account.builder() + .id(accountId) + .privacy(AccountPrivacyConfig.of(null, + null, + AccountDsaConfig.of(DefaultDsa.of(3, + 4, + 5, + List.of(DsaTransparency.of("domain", + List.of(1)))), + null), + null, + null)) + .build(); + given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account)); + + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .account(account) + .privacyContext(privacyContext) + .build(); + + // when + final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(auctionContext); + + // then + assertThat(result) + .extracting(BidRequest::getRegs) + .extracting(Regs::getExt) + .extracting(ExtRegs::getDsa) + .satisfies(dsa -> { + assertThat(dsa.getDsaRequired()).isEqualTo(0); + assertThat(dsa.getPubRender()).isEqualTo(1); + assertThat(dsa.getDataToPub()).isEqualTo(2); + assertThat(dsa.getTransparency()).satisfies(transparencies -> + assertThat(transparencies).isEqualTo(List.of(ExtRegsDsaTransparency.of("", + List.of(0))))); + }); + } + + @Test + public void enrichBidRequestWithAccountAndPrivacyDataShouldSetDsaFromAccountWhenGdprScopeMatches() { + // given + final String accountId = "accId"; + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(emptyList()) + .site(Site.builder() + .publisher(Publisher.builder().id(accountId).build()) + .build())); + + final PrivacyContext privacyContext = PrivacyContext.of( + Privacy.builder() + .gdpr("") + .consentString("") + .ccpa(Ccpa.EMPTY) + .coppa(0) + .build(), + TcfContext.builder().inGdprScope(true).build(), + ""); + + final Account account = Account.builder() + .id(accountId) + .privacy(AccountPrivacyConfig.of(null, + null, + AccountDsaConfig.of(DefaultDsa.of(0, + 1, + 2, + List.of(DsaTransparency.of("", + List.of(0)))), + true), + null, + null)) + .build(); + given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account)); + + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .account(account) + .privacyContext(privacyContext) + .build(); + + // when + final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(auctionContext); + + // then + assertThat(result) + .extracting(BidRequest::getRegs) + .extracting(Regs::getExt) + .extracting(ExtRegs::getDsa) + .satisfies(dsa -> { + assertThat(dsa.getDsaRequired()).isEqualTo(0); + assertThat(dsa.getPubRender()).isEqualTo(1); + assertThat(dsa.getDataToPub()).isEqualTo(2); + assertThat(dsa.getTransparency()).satisfies(transparencies -> + assertThat(transparencies).isEqualTo(List.of(ExtRegsDsaTransparency.of("", + List.of(0))))); + }); + } + + @Test + public void enrichBidRequestWithAccountAndPrivacyDataShouldNotSetDsaFromAccountWhenGdprScopeDoesntMatch() { + // given + final String accountId = "accId"; + final Regs regs = Regs.builder().build(); + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(emptyList()) + .site(Site.builder() + .publisher(Publisher.builder().id(accountId).build()) + .build()) + .regs(regs)); + + final PrivacyContext privacyContext = PrivacyContext.of( + Privacy.builder() + .gdpr("") + .consentString("") + .ccpa(Ccpa.EMPTY) + .coppa(0) + .build(), + TcfContext.builder().inGdprScope(false).build(), + ""); + + final Account account = Account.builder() + .id(accountId) + .privacy(AccountPrivacyConfig.of(null, + null, + AccountDsaConfig.of(DefaultDsa.of(0, + 1, + 2, + List.of(DsaTransparency.of("", + List.of(0)))), + true), + null, + null)) + .build(); + given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account)); + + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .account(account) + .privacyContext(privacyContext) + .build(); + + // when + final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(auctionContext); + + // then + assertThat(result) + .extracting(BidRequest::getRegs) + .extracting(Regs::getExt) + .isNull(); + assertThat(result) + .extracting(BidRequest::getRegs) + .isSameAs(regs); + } + private static String bidRequestToString(BidRequest bidRequest) { try { return mapper.writeValueAsString(bidRequest); diff --git a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java index 84a8d3a76a3..b6536dec25c 100644 --- a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java @@ -317,7 +317,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsFound() { final AccountGdprConfig accountGdprConfig = AccountGdprConfig.builder() .enabledForRequestType(EnabledForRequestType.of(true, true, true, true, true)).build(); final Account account = Account.builder() - .privacy(AccountPrivacyConfig.of(accountGdprConfig, null, null, null)) + .privacy(AccountPrivacyConfig.of(accountGdprConfig, null, null, null, null)) .build(); given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account)); diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index da2885f42d2..0f99716f44b 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -394,7 +394,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsFound() { .enabledForRequestType(EnabledForRequestType.of(true, true, true, true, true)) .build(); final Account account = Account.builder() - .privacy(AccountPrivacyConfig.of(accountGdprConfig, null, null, null)) + .privacy(AccountPrivacyConfig.of(accountGdprConfig, null, null, null, null)) .build(); final Future accountFuture = Future.succeededFuture(account); given(applicationSettings.getAccountById(any(), any())).willReturn(accountFuture); diff --git a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java index 683a430edca..9de1879aab9 100644 --- a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java @@ -124,6 +124,7 @@ public void getAccountByIdShouldMergeAccountWithDefaultAccount() { .build(), null, null, + null, null)) .build())); @@ -144,6 +145,7 @@ public void getAccountByIdShouldMergeAccountWithDefaultAccount() { .build(), null, null, + null, null)) .build()); } @@ -230,6 +232,7 @@ public void getAccountByIdShouldRemoveInvalidRulesFromAccountActivitiesConfigura given(delegate.getAccountById(anyString(), any())).willReturn(Future.succeededFuture(Account.builder() .privacy(AccountPrivacyConfig.of( + null, null, null, Map.of( @@ -257,6 +260,7 @@ public void getAccountByIdShouldRemoveInvalidRulesFromAccountActivitiesConfigura // then assertThat(accountFuture).succeededWith(Account.builder() .privacy(AccountPrivacyConfig.of( + null, null, null, Map.of( diff --git a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java index 505b779c8f7..711442be41e 100644 --- a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java @@ -173,6 +173,7 @@ public void getAccountByIdShouldReturnPresentAccount() { .build(), null, null, + null, null)) .analytics(AccountAnalyticsConfig.of( expectedEventsConfig, diff --git a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java index 628b4feb936..cd4d0c557c2 100644 --- a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java @@ -107,7 +107,7 @@ public void getAccountByIdShouldReturnFetchedAccount() throws JsonProcessingExce .auction(AccountAuctionConfig.builder() .priceGranularity("testPriceGranularity") .build()) - .privacy(AccountPrivacyConfig.of(null, null, null, null)) + .privacy(AccountPrivacyConfig.of(null, null, null, null, null)) .build(); final HttpAccountsResponse response = HttpAccountsResponse.of(Collections.singletonMap("someId", account)); givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response)); diff --git a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java index a086dc27a20..53b9e518605 100644 --- a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java @@ -251,6 +251,7 @@ public void getAccountByIdShouldReturnAccountWithAllFieldsPopulated(TestContext .build(), null, null, + null, null)) .analytics(AccountAnalyticsConfig.of( expectedEventsConfig,