From 75b65c6402b14ea60c2d874307864787b273d42d Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Fri, 29 May 2026 14:32:11 +0530 Subject: [PATCH 1/2] Yahoo Ads: migrate outbound wire from OpenRTB 2.5 to 2.6 Bump the x-openrtb-version header from 2.5 to 2.6 and stop down-converting the auction request inside the bidder. The bidder now passes the 2.6 request through, relying on the PBS-Java up-converter to normalize gdpr, us_privacy, consent, eids, schain and rwdd to their 2.6 top-level slots. For the three privacy/regulatory fields that neither converter handles (gpp, gpp_sid, coppa), add ext->top promotion in modifyRegs: read the 2.6 top-level value, falling back to the legacy 2.5 regs.ext property, and strip the promoted keys from ext. Typed ext-only fields (gpc, dsa) and any other ext properties are preserved. No cattax default is synthesized. Remove the now-unused BidRequestOrtbVersionConversionManager dependency from the bidder and its Spring configuration. Update unit tests for the 2.6 wire shape and add coverage for the ext->top promotion, mixed publisher shapes, wrong-type ext guards, and the no-ext short-circuit. Update the integration fixture to the 2.6 wire shape. --- .../bidder/yahooads/YahooAdsBidder.java | 131 ++++++++----- .../config/bidder/YahooAdsConfiguration.java | 6 +- .../bidder/yahooads/YahooAdsBidderTest.java | 182 ++++++++++++++---- .../yahooads/test-yahooads-bid-request.json | 8 +- 4 files changed, 234 insertions(+), 93 deletions(-) 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 3e15eb554a3..7a990de7b92 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.yahooads; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; @@ -20,8 +18,6 @@ import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; -import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -32,9 +28,7 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; 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; @@ -44,7 +38,6 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.Optional; public class YahooAdsBidder implements Bidder { @@ -52,16 +45,17 @@ public class YahooAdsBidder implements Bidder { new TypeReference<>() { }; + private static final String GPP_PROPERTY = "gpp"; + private static final String GPP_SID_PROPERTY = "gpp_sid"; + private static final String COPPA_PROPERTY = "coppa"; + private final String endpointUrl; - private final BidRequestOrtbVersionConversionManager conversionManager; private final JacksonMapper mapper; public YahooAdsBidder(String endpointUrl, - BidRequestOrtbVersionConversionManager conversionManager, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); - this.conversionManager = Objects.requireNonNull(conversionManager); } @Override @@ -70,15 +64,13 @@ public Result>> makeHttpRequests(BidRequest bidRequ final List errors = new ArrayList<>(); final Regs regs = bidRequest.getRegs(); - final BidRequest bidRequestOpenRtb25 = this.conversionManager.convertFromAuctionSupportedVersion(bidRequest, - OrtbVersion.ORTB_2_5); - final List impList = bidRequestOpenRtb25.getImp(); + final List impList = bidRequest.getImp(); for (int i = 0; i < impList.size(); i++) { try { final Imp imp = impList.get(i); final ExtImpYahooAds extImpYahooAds = parseAndValidateImpExt(imp.getExt(), i); - final BidRequest modifiedRequest = modifyRequest(bidRequestOpenRtb25, imp, extImpYahooAds, + final BidRequest modifiedRequest = modifyRequest(bidRequest, imp, extImpYahooAds, regs); bidRequests.add(makeHttpRequest(modifiedRequest)); } catch (PreBidException e) { @@ -170,50 +162,89 @@ private static Banner modifyBanner(Banner banner) { .build(); } - private Regs modifyRegs(Regs regs) { - final ExtRegs extRegs = resolveExtRegs(regs); + private static Regs modifyRegs(Regs regs) { + final ExtRegs originalExt = regs.getExt(); + if (originalExt == null + || originalExt.getProperties() == null + || originalExt.getProperties().isEmpty()) { + return regs; + } - return Regs.builder().ext(extRegs).build(); - } + final String resolvedGpp = resolveGpp(regs, originalExt); + final List resolvedGppSid = resolveGppSid(regs, originalExt); + final Integer resolvedCoppa = resolveCoppa(regs, originalExt); - private ExtRegs resolveExtRegs(Regs regs) { - final Integer gdpr = resolveGdpr(regs); - final String usPrivacy = resolveUsPrivacy(regs); - final String gpp = regs.getGpp(); - final List gppSid = regs.getGppSid(); - - final String gpc = Optional.ofNullable(regs.getExt()) - .map(ExtRegs::getGpc) - .orElse(null); - 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(); - gppSid.forEach(gppArrayNode::add); - extRegs.addProperty("gpp_sid", gppArrayNode); + final boolean changed = !Objects.equals(resolvedGpp, regs.getGpp()) + || !Objects.equals(resolvedGppSid, regs.getGppSid()) + || !Objects.equals(resolvedCoppa, regs.getCoppa()); + + if (!changed) { + return regs; } - if (regs.getCoppa() != null) { - extRegs.addProperty("coppa", IntNode.valueOf(regs.getCoppa())); + + return regs.toBuilder() + .gpp(resolvedGpp) + .gppSid(resolvedGppSid) + .coppa(resolvedCoppa) + .ext(stripPromotedFromExt(originalExt)) + .build(); + } + + private static String resolveGpp(Regs regs, ExtRegs ext) { + if (regs.getGpp() != null) { + return regs.getGpp(); } + final JsonNode node = ext.getProperties().get(GPP_PROPERTY); + return node != null && node.isTextual() ? node.asText() : null; + } - Optional.ofNullable(regs.getExt()) - .map(FlexibleExtension::getProperties) - .ifPresent(extRegs::addProperties); + private static List resolveGppSid(Regs regs, ExtRegs ext) { + if (!CollectionUtils.isEmpty(regs.getGppSid())) { + return regs.getGppSid(); + } + final JsonNode node = ext.getProperties().get(GPP_SID_PROPERTY); + if (node == null || !node.isArray()) { + return regs.getGppSid(); + } + final List sids = new ArrayList<>(node.size()); + node.forEach(elem -> { + if (elem.isIntegralNumber()) { + sids.add(elem.asInt()); + } + }); + return sids.isEmpty() ? regs.getGppSid() : sids; + } - return extRegs; + private static Integer resolveCoppa(Regs regs, ExtRegs ext) { + if (regs.getCoppa() != null) { + return regs.getCoppa(); + } + final JsonNode node = ext.getProperties().get(COPPA_PROPERTY); + return node != null && node.isIntegralNumber() ? node.asInt() : null; } - private static Integer resolveGdpr(Regs regs) { - return regs.getGdpr() != null ? regs.getGdpr() - : (regs.getExt() != null ? regs.getExt().getGdpr() : null); + private static ExtRegs stripPromotedFromExt(ExtRegs original) { + final ExtRegs stripped = ExtRegs.of( + original.getGdpr(), + original.getUsPrivacy(), + original.getGpc(), + original.getDsa()); + original.getProperties().forEach((key, value) -> { + if (!GPP_PROPERTY.equals(key) + && !GPP_SID_PROPERTY.equals(key) + && !COPPA_PROPERTY.equals(key)) { + stripped.addProperty(key, value); + } + }); + return isExtEmpty(stripped) ? null : stripped; } - private static String resolveUsPrivacy(Regs regs) { - return regs.getUsPrivacy() != null ? regs.getUsPrivacy() - : (regs.getExt() != null ? regs.getExt().getUsPrivacy() : null); + private static boolean isExtEmpty(ExtRegs ext) { + return ext.getGdpr() == null + && ext.getUsPrivacy() == null + && ext.getGpc() == null + && ext.getDsa() == null + && (ext.getProperties() == null || ext.getProperties().isEmpty()); } private HttpRequest makeHttpRequest(BidRequest outgoingRequest) { @@ -228,7 +259,7 @@ private HttpRequest makeHttpRequest(BidRequest outgoingRequest) { private static MultiMap makeHeaders(Device device) { final MultiMap headers = HttpUtil.headers() - .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.6"); final String deviceUa = device != null ? device.getUa() : null; HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, deviceUa); diff --git a/src/main/java/org/prebid/server/spring/config/bidder/YahooAdsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/YahooAdsConfiguration.java index 9cd3ed1249b..fae8aa0a1b1 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/YahooAdsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/YahooAdsConfiguration.java @@ -1,6 +1,5 @@ package org.prebid.server.spring.config.bidder; -import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.yahooads.YahooAdsBidder; import org.prebid.server.json.JacksonMapper; @@ -31,13 +30,12 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps yahooAdsBidderDeps(BidderConfigurationProperties yahooAdsConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper, - BidRequestOrtbVersionConversionManager conversionManager) { + JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(yahooAdsConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new YahooAdsBidder(config.getEndpoint(), conversionManager, mapper)) + .bidderCreator(config -> new YahooAdsBidder(config.getEndpoint(), mapper)) .assemble(); } } 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 7966a94e911..a9f09f58c95 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -1,6 +1,9 @@ package org.prebid.server.bidder.yahooads; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; @@ -16,11 +19,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; -import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; -import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -43,10 +43,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.tuple; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mock.Strictness.LENIENT; -import static org.mockito.Mockito.when; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; @@ -55,22 +51,16 @@ public class YahooAdsBidderTest extends VertxTest { private static final String ENDPOINT_URL = "https://test.endpoint.com"; - @Mock(strictness = LENIENT) - private BidRequestOrtbVersionConversionManager conversionManager; - private YahooAdsBidder target; @BeforeEach public void setUp() { - when(conversionManager.convertFromAuctionSupportedVersion(any(BidRequest.class), eq(OrtbVersion.ORTB_2_5))) - .thenAnswer(answer -> answer.getArgument(0)); - target = new YahooAdsBidder(ENDPOINT_URL, conversionManager, jacksonMapper); + target = new YahooAdsBidder(ENDPOINT_URL, jacksonMapper); } @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new YahooAdsBidder("invalid_url", - conversionManager, jacksonMapper)); + assertThatIllegalArgumentException().isThrownBy(() -> new YahooAdsBidder("invalid_url", jacksonMapper)); } @Test @@ -282,7 +272,7 @@ public void makeHttpRequestsShouldSetExpectedHeaders() { assertThat(result.getValue().getFirst().getHeaders()) .extracting(Map.Entry::getKey, Map.Entry::getValue) .containsOnly(tuple("User-Agent", "UA"), - tuple("x-openrtb-version", "2.5"), + tuple("x-openrtb-version", "2.6"), tuple("Content-Type", "application/json;charset=utf-8"), tuple("Accept", "application/json")); } @@ -402,8 +392,9 @@ public void makeBidsShouldSkipNotSupportedImpAndReturnVideoBidWhenVideoPresent() } @Test - public void makeBidsShouldRemoveTheOpenRTB26Regs() { - // given + public void makeHttpRequestsShouldPreserveTopLevel26RegsAndExtTypedFields() { + // 2.6-shape publisher: all regulatory signals at top-level + typed ext fields. + // Bidder should pass them through untouched. final ExtRegsDsa dsa = ExtRegsDsa.of(2, 2, 3, emptyList()); final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() @@ -411,6 +402,7 @@ public void makeBidsShouldRemoveTheOpenRTB26Regs() { .usPrivacy("1YNN") .gpp("gppconsent") .gppSid(List.of(6)) + .coppa(1) .ext(ExtRegs.of(null, null, "1", dsa)) .build()).device(Device.builder().ua("UA").build())); @@ -420,26 +412,150 @@ public void makeBidsShouldRemoveTheOpenRTB26Regs() { // then assertThat(result.getErrors()).isEmpty(); final Regs regs = result.getValue().getFirst().getPayload().getRegs(); - assertThat(regs.getGdpr()).isNull(); - assertThat(regs.getUsPrivacy()).isNull(); - assertThat(regs.getGpp()).isNull(); - assertThat(regs.getGppSid()).isNull(); + // 2.6 top-level fields preserved + assertThat(regs.getGdpr()).isEqualTo(1); + assertThat(regs.getUsPrivacy()).isEqualTo("1YNN"); + assertThat(regs.getGpp()).isEqualTo("gppconsent"); + assertThat(regs.getGppSid()).containsExactly(6); + assertThat(regs.getCoppa()).isEqualTo(1); + // Typed ext fields preserved (gpc, dsa have no top-level slot in 2.6) assertThat(regs.getExt()).isNotNull(); - 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"); } @Test - public void makeBidsShouldOverwriteRegsExtValues() { - // given + public void makeHttpRequestsShouldPromoteLegacyExtGppGppSidAndCoppaToTopLevel() { + // 2.5-shape publisher: gpp/gpp_sid/coppa carried as ext properties. + // Bidder should promote them to 2.6 top-level slots and strip from ext. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + bidRequest.getRegs().getExt().addProperty("gpp", TextNode.valueOf("legacy_gpp_value")); + final ArrayNode sidArray = mapper.createArrayNode(); + sidArray.add(6); + sidArray.add(8); + bidRequest.getRegs().getExt().addProperty("gpp_sid", sidArray); + bidRequest.getRegs().getExt().addProperty("coppa", IntNode.valueOf(1)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + // promoted to top-level + assertThat(regs.getGpp()).isEqualTo("legacy_gpp_value"); + assertThat(regs.getGppSid()).containsExactly(6, 8); + assertThat(regs.getCoppa()).isEqualTo(1); + // stripped from ext (ext was empty after stripping, so it becomes null) + assertThat(regs.getExt()).isNull(); + } + + @Test + public void makeHttpRequestsShouldPromoteOnlyGppFromExtAndStripIt() { + // Only gpp lives in ext; gpp_sid and coppa remain unset everywhere. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + bidRequest.getRegs().getExt().addProperty("gpp", TextNode.valueOf("only_gpp")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getGpp()).isEqualTo("only_gpp"); + assertThat(regs.getGppSid()).isNull(); + assertThat(regs.getCoppa()).isNull(); + assertThat(regs.getExt()).isNull(); + } + + @Test + public void makeHttpRequestsShouldPreserveTopLevelGdprWhilePromotingGppFromExt() { + // Mixed-shape publisher: 2.6 gdpr top-level + legacy 2.5 gpp in ext. final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .gdpr(1) - .ext(ExtRegs.of(0, "1YNN", null, null)) + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + bidRequest.getRegs().getExt().addProperty("gpp", TextNode.valueOf("mixed_gpp")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getGdpr()).isEqualTo(1); + assertThat(regs.getGpp()).isEqualTo("mixed_gpp"); + assertThat(regs.getExt()).isNull(); + } + + @Test + public void makeHttpRequestsShouldKeepGpcAndUnrelatedExtPropertyAfterPromotion() { + // Publisher sent gpp in ext + typed gpc + an unrelated ext property. + // After promoting gpp: gpc must stay in typed ext, the unrelated property must + // also survive, and ext must NOT be nulled. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .ext(ExtRegs.of(null, null, "1", null)) + .build()).device(Device.builder().ua("UA").build())); + bidRequest.getRegs().getExt().addProperty("gpp", TextNode.valueOf("with_gpc")); + bidRequest.getRegs().getExt().addProperty("unrelated", TextNode.valueOf("keep_me")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getGpp()).isEqualTo("with_gpc"); + // ext survives: gpc kept, unrelated property kept, gpp stripped + assertThat(regs.getExt()).isNotNull(); + assertThat(regs.getExt().getGpc()).isEqualTo("1"); + assertThat(regs.getExt().getProperty("gpp")).isNull(); + assertThat(regs.getExt().getProperty("unrelated").asText()).isEqualTo("keep_me"); + } + + @Test + public void makeHttpRequestsShouldNotPromoteWhenExtPropertyHasWrongType() { + // Defensive type guards: gpp as integer (not text), gpp_sid as text (not array), + // coppa as text (not integer). None should promote; regs should be unchanged. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + bidRequest.getRegs().getExt().addProperty("gpp", IntNode.valueOf(99)); + bidRequest.getRegs().getExt().addProperty("gpp_sid", TextNode.valueOf("not_array")); + bidRequest.getRegs().getExt().addProperty("coppa", TextNode.valueOf("not_int")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getGpp()).isNull(); + assertThat(regs.getGppSid()).isNull(); + assertThat(regs.getCoppa()).isNull(); + // ext is preserved with the malformed properties untouched + assertThat(regs.getExt()).isNotNull(); + assertThat(regs.getExt().getProperty("gpp").asInt()).isEqualTo(99); + assertThat(regs.getExt().getProperty("gpp_sid").asText()).isEqualTo("not_array"); + assertThat(regs.getExt().getProperty("coppa").asText()).isEqualTo("not_int"); + } + + @Test + public void makeHttpRequestsShouldShortCircuitWhenRegsHasNoExt() { + // regs is set but ext is null — modifyRegs should return regs unchanged. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .gdpr(0) + .gpp("already_top") .build()).device(Device.builder().ua("UA").build())); // when @@ -448,11 +564,9 @@ public void makeBidsShouldOverwriteRegsExtValues() { // then assertThat(result.getErrors()).isEmpty(); final Regs regs = result.getValue().getFirst().getPayload().getRegs(); - assertThat(regs.getGdpr()).isNull(); - assertThat(regs.getUsPrivacy()).isNull(); - assertThat(regs.getExt().getGdpr()).isEqualTo(1); - assertThat(regs.getExt().getUsPrivacy()).isEqualTo("1YNN"); - assertThat(regs.getExt().getDsa()).isNull(); + assertThat(regs.getGdpr()).isEqualTo(0); + assertThat(regs.getGpp()).isEqualTo("already_top"); + assertThat(regs.getExt()).isNull(); } private static BidRequest givenBidRequest( diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-yahooads-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-yahooads-bid-request.json index d5dbb07bade..dfe01cf07f1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-yahooads-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-yahooads-bid-request.json @@ -42,11 +42,9 @@ "ip": "193.168.244.1" }, "regs": { - "ext": { - "gpp": "gppstring", - "gpp_sid": [6], - "gdpr": 0 - } + "gpp": "gppstring", + "gpp_sid": [6], + "gdpr": 0 }, "ext": { "prebid": { From 1e558709a1fcb2488644fa74c69c2af4010c4fc3 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Fri, 29 May 2026 17:00:39 +0530 Subject: [PATCH 2/2] Yahoo Ads: preserve non-promoted regs.ext values during 2.6 promotion Make stripPromotedFromExt remove a regs.ext key only when its value was actually promoted to top-level (resolved value non-null). A malformed, non-promotable value (e.g. a non-textual gpp) is now left untouched in regs.ext instead of being dropped when a sibling field triggers the rebuild. Add a test covering the mixed case (valid coppa promoted, malformed gpp kept in ext) and trim redundant comments. --- .../bidder/yahooads/YahooAdsBidder.java | 17 +++++--- .../bidder/yahooads/YahooAdsBidderTest.java | 40 ++++++++++--------- 2 files changed, 34 insertions(+), 23 deletions(-) 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 7a990de7b92..642bb7da325 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -162,6 +162,7 @@ private static Banner modifyBanner(Banner banner) { .build(); } + // Promote legacy 2.5 regs.ext gpp/gpp_sid/coppa to their 2.6 top-level slots. private static Regs modifyRegs(Regs regs) { final ExtRegs originalExt = regs.getExt(); if (originalExt == null @@ -186,7 +187,7 @@ private static Regs modifyRegs(Regs regs) { .gpp(resolvedGpp) .gppSid(resolvedGppSid) .coppa(resolvedCoppa) - .ext(stripPromotedFromExt(originalExt)) + .ext(stripPromotedFromExt(originalExt, resolvedGpp, resolvedGppSid, resolvedCoppa)) .build(); } @@ -223,16 +224,22 @@ private static Integer resolveCoppa(Regs regs, ExtRegs ext) { return node != null && node.isIntegralNumber() ? node.asInt() : null; } - private static ExtRegs stripPromotedFromExt(ExtRegs original) { + // Drop a key from ext only if it was promoted; keep gpc/dsa, unknown, and non-promoted values. + private static ExtRegs stripPromotedFromExt(ExtRegs original, + String resolvedGpp, + List resolvedGppSid, + Integer resolvedCoppa) { final ExtRegs stripped = ExtRegs.of( original.getGdpr(), original.getUsPrivacy(), original.getGpc(), original.getDsa()); original.getProperties().forEach((key, value) -> { - if (!GPP_PROPERTY.equals(key) - && !GPP_SID_PROPERTY.equals(key) - && !COPPA_PROPERTY.equals(key)) { + final boolean promoted = + (GPP_PROPERTY.equals(key) && resolvedGpp != null) + || (GPP_SID_PROPERTY.equals(key) && !CollectionUtils.isEmpty(resolvedGppSid)) + || (COPPA_PROPERTY.equals(key) && resolvedCoppa != null); + if (!promoted) { stripped.addProperty(key, value); } }); 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 a9f09f58c95..74c1b0ea6a1 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -393,8 +393,6 @@ public void makeBidsShouldSkipNotSupportedImpAndReturnVideoBidWhenVideoPresent() @Test public void makeHttpRequestsShouldPreserveTopLevel26RegsAndExtTypedFields() { - // 2.6-shape publisher: all regulatory signals at top-level + typed ext fields. - // Bidder should pass them through untouched. final ExtRegsDsa dsa = ExtRegsDsa.of(2, 2, 3, emptyList()); final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() @@ -412,13 +410,11 @@ public void makeHttpRequestsShouldPreserveTopLevel26RegsAndExtTypedFields() { // then assertThat(result.getErrors()).isEmpty(); final Regs regs = result.getValue().getFirst().getPayload().getRegs(); - // 2.6 top-level fields preserved assertThat(regs.getGdpr()).isEqualTo(1); assertThat(regs.getUsPrivacy()).isEqualTo("1YNN"); assertThat(regs.getGpp()).isEqualTo("gppconsent"); assertThat(regs.getGppSid()).containsExactly(6); assertThat(regs.getCoppa()).isEqualTo(1); - // Typed ext fields preserved (gpc, dsa have no top-level slot in 2.6) assertThat(regs.getExt()).isNotNull(); assertThat(regs.getExt().getGpc()).isEqualTo("1"); assertThat(regs.getExt().getDsa()).isEqualTo(dsa); @@ -426,8 +422,6 @@ public void makeHttpRequestsShouldPreserveTopLevel26RegsAndExtTypedFields() { @Test public void makeHttpRequestsShouldPromoteLegacyExtGppGppSidAndCoppaToTopLevel() { - // 2.5-shape publisher: gpp/gpp_sid/coppa carried as ext properties. - // Bidder should promote them to 2.6 top-level slots and strip from ext. final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .ext(ExtRegs.of(null, null, null, null)) @@ -445,17 +439,14 @@ public void makeHttpRequestsShouldPromoteLegacyExtGppGppSidAndCoppaToTopLevel() // then assertThat(result.getErrors()).isEmpty(); final Regs regs = result.getValue().getFirst().getPayload().getRegs(); - // promoted to top-level assertThat(regs.getGpp()).isEqualTo("legacy_gpp_value"); assertThat(regs.getGppSid()).containsExactly(6, 8); assertThat(regs.getCoppa()).isEqualTo(1); - // stripped from ext (ext was empty after stripping, so it becomes null) assertThat(regs.getExt()).isNull(); } @Test public void makeHttpRequestsShouldPromoteOnlyGppFromExtAndStripIt() { - // Only gpp lives in ext; gpp_sid and coppa remain unset everywhere. final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .ext(ExtRegs.of(null, null, null, null)) @@ -476,7 +467,6 @@ public void makeHttpRequestsShouldPromoteOnlyGppFromExtAndStripIt() { @Test public void makeHttpRequestsShouldPreserveTopLevelGdprWhilePromotingGppFromExt() { - // Mixed-shape publisher: 2.6 gdpr top-level + legacy 2.5 gpp in ext. final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .gdpr(1) @@ -497,9 +487,6 @@ public void makeHttpRequestsShouldPreserveTopLevelGdprWhilePromotingGppFromExt() @Test public void makeHttpRequestsShouldKeepGpcAndUnrelatedExtPropertyAfterPromotion() { - // Publisher sent gpp in ext + typed gpc + an unrelated ext property. - // After promoting gpp: gpc must stay in typed ext, the unrelated property must - // also survive, and ext must NOT be nulled. final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .ext(ExtRegs.of(null, null, "1", null)) @@ -514,7 +501,6 @@ public void makeHttpRequestsShouldKeepGpcAndUnrelatedExtPropertyAfterPromotion() assertThat(result.getErrors()).isEmpty(); final Regs regs = result.getValue().getFirst().getPayload().getRegs(); assertThat(regs.getGpp()).isEqualTo("with_gpc"); - // ext survives: gpc kept, unrelated property kept, gpp stripped assertThat(regs.getExt()).isNotNull(); assertThat(regs.getExt().getGpc()).isEqualTo("1"); assertThat(regs.getExt().getProperty("gpp")).isNull(); @@ -523,8 +509,6 @@ public void makeHttpRequestsShouldKeepGpcAndUnrelatedExtPropertyAfterPromotion() @Test public void makeHttpRequestsShouldNotPromoteWhenExtPropertyHasWrongType() { - // Defensive type guards: gpp as integer (not text), gpp_sid as text (not array), - // coppa as text (not integer). None should promote; regs should be unchanged. final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .ext(ExtRegs.of(null, null, null, null)) @@ -542,16 +526,36 @@ public void makeHttpRequestsShouldNotPromoteWhenExtPropertyHasWrongType() { assertThat(regs.getGpp()).isNull(); assertThat(regs.getGppSid()).isNull(); assertThat(regs.getCoppa()).isNull(); - // ext is preserved with the malformed properties untouched assertThat(regs.getExt()).isNotNull(); assertThat(regs.getExt().getProperty("gpp").asInt()).isEqualTo(99); assertThat(regs.getExt().getProperty("gpp_sid").asText()).isEqualTo("not_array"); assertThat(regs.getExt().getProperty("coppa").asText()).isEqualTo("not_int"); } + @Test + public void makeHttpRequestsShouldLeaveMalformedExtValueInExtWhenSiblingFieldIsPromoted() { + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + bidRequest.getRegs().getExt().addProperty("coppa", IntNode.valueOf(1)); + bidRequest.getRegs().getExt().addProperty("gpp", IntNode.valueOf(99)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getCoppa()).isEqualTo(1); + assertThat(regs.getGpp()).isNull(); + assertThat(regs.getExt()).isNotNull(); + assertThat(regs.getExt().getProperty("coppa")).isNull(); + assertThat(regs.getExt().getProperty("gpp").asInt()).isEqualTo(99); + } + @Test public void makeHttpRequestsShouldShortCircuitWhenRegsHasNoExt() { - // regs is set but ext is null — modifyRegs should return regs unchanged. final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .gdpr(0)