From 75b65c6402b14ea60c2d874307864787b273d42d Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Fri, 29 May 2026 14:32:11 +0530 Subject: [PATCH 01/17] 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 02/17] 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) From cb2fa2cc8ca1b36b62d94b60ed7ecacfa45b66c1 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Mon, 1 Jun 2026 14:43:45 +0530 Subject: [PATCH 03/17] Yahoo Ads: treat a non-integer gpp_sid element as malformed resolveGppSid now promotes regs.ext.gpp_sid only when every element is an integer. If any element is non-integral the whole array is left in regs.ext untouched rather than promoting a filtered subset, matching the gpp and coppa handling so no value is silently dropped. Adds a test for the mixed-array case. --- .../bidder/yahooads/YahooAdsBidder.java | 9 +++--- .../bidder/yahooads/YahooAdsBidderTest.java | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 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 642bb7da325..6abae9206cc 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -208,11 +208,12 @@ private static List resolveGppSid(Regs regs, ExtRegs ext) { return regs.getGppSid(); } final List sids = new ArrayList<>(node.size()); - node.forEach(elem -> { - if (elem.isIntegralNumber()) { - sids.add(elem.asInt()); + for (final JsonNode elem : node) { + if (!elem.isIntegralNumber()) { + return regs.getGppSid(); } - }); + sids.add(elem.asInt()); + } return sids.isEmpty() ? regs.getGppSid() : sids; } 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 74c1b0ea6a1..38446fdbb71 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,7 @@ package org.prebid.server.bidder.yahooads; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.TextNode; @@ -554,6 +555,35 @@ public void makeHttpRequestsShouldLeaveMalformedExtValueInExtWhenSiblingFieldIsP assertThat(regs.getExt().getProperty("gpp").asInt()).isEqualTo(99); } + @Test + public void makeHttpRequestsShouldNotPromoteGppSidWhenArrayHasNonIntegerElement() { + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + final ArrayNode mixed = mapper.createArrayNode(); + mixed.add(7); + mixed.add("foo"); + mixed.add(8); + bidRequest.getRegs().getExt().addProperty("gpp_sid", mixed); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + // whole array treated as malformed: not promoted, left in ext untouched (nothing dropped) + assertThat(regs.getGppSid()).isNull(); + assertThat(regs.getExt()).isNotNull(); + final JsonNode keptSid = regs.getExt().getProperty("gpp_sid"); + assertThat(keptSid.isArray()).isTrue(); + assertThat(keptSid).hasSize(3); + assertThat(keptSid.get(0).asInt()).isEqualTo(7); + assertThat(keptSid.get(1).asText()).isEqualTo("foo"); + assertThat(keptSid.get(2).asInt()).isEqualTo(8); + } + @Test public void makeHttpRequestsShouldShortCircuitWhenRegsHasNoExt() { final BidRequest bidRequest = givenBidRequest(identity(), From 3fc6153c0f5911c6694100291e4fe4b5639aa4f0 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Mon, 1 Jun 2026 15:28:07 +0530 Subject: [PATCH 04/17] Yahoo Ads: make regs ext-to-top promotion consistent and self-describing Restructure the regs handling so a single per-field signal decides both the promotion and the ext strip. A field is promoted only when it is absent at top-level and present and well-formed in ext; a key is removed from regs.ext if and only if that field was promoted. This removes the previous dependency on whether a sibling field triggered the rebuild (e.g. a value present at both top-level and in ext is now handled the same regardless of siblings). Rename for clarity: modifyRegs -> promoteRegsExtToTopLevel, resolveX -> xToPromote (return the value to lift or null), stripPromotedFromExt -> removePromotedKeys. Add a test for the same field present at both top-level and ext. --- .../bidder/yahooads/YahooAdsBidder.java | 97 ++++++++++--------- .../bidder/yahooads/YahooAdsBidderTest.java | 28 ++++++ 2 files changed, 77 insertions(+), 48 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 6abae9206cc..630fe1d4d34 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -117,7 +117,7 @@ private BidRequest modifyRequest(BidRequest request, Imp imp, ExtImpYahooAds ext } if (regs != null) { - requestBuilder.regs(modifyRegs(regs)); + requestBuilder.regs(promoteRegsExtToTopLevel(regs)); } return requestBuilder @@ -163,88 +163,89 @@ private static Banner modifyBanner(Banner banner) { } // 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 - || originalExt.getProperties() == null - || originalExt.getProperties().isEmpty()) { + // A field is promoted only when it is absent at top-level and present and well-formed + // in ext; each promoted key is then removed from ext. Anything not promoted (already + // top-level, missing, or malformed) is left untouched, in ext. + private static Regs promoteRegsExtToTopLevel(Regs regs) { + final ExtRegs ext = regs.getExt(); + if (ext == null || ext.getProperties().isEmpty()) { return regs; } - final String resolvedGpp = resolveGpp(regs, originalExt); - final List resolvedGppSid = resolveGppSid(regs, originalExt); - final Integer resolvedCoppa = resolveCoppa(regs, originalExt); + final String promotedGpp = gppToPromote(regs, ext); + final List promotedGppSid = gppSidToPromote(regs, ext); + final Integer promotedCoppa = coppaToPromote(regs, ext); - final boolean changed = !Objects.equals(resolvedGpp, regs.getGpp()) - || !Objects.equals(resolvedGppSid, regs.getGppSid()) - || !Objects.equals(resolvedCoppa, regs.getCoppa()); - - if (!changed) { + if (promotedGpp == null && promotedGppSid == null && promotedCoppa == null) { return regs; } - return regs.toBuilder() - .gpp(resolvedGpp) - .gppSid(resolvedGppSid) - .coppa(resolvedCoppa) - .ext(stripPromotedFromExt(originalExt, resolvedGpp, resolvedGppSid, resolvedCoppa)) + final Regs.RegsBuilder builder = regs.toBuilder(); + if (promotedGpp != null) { + builder.gpp(promotedGpp); + } + if (promotedGppSid != null) { + builder.gppSid(promotedGppSid); + } + if (promotedCoppa != null) { + builder.coppa(promotedCoppa); + } + return builder + .ext(removePromotedKeys(ext, promotedGpp != null, promotedGppSid != null, promotedCoppa != null)) .build(); } - private static String resolveGpp(Regs regs, ExtRegs ext) { + // Value to lift from ext, or null when top-level already has it or ext lacks a valid value. + private static String gppToPromote(Regs regs, ExtRegs ext) { if (regs.getGpp() != null) { - return regs.getGpp(); + return null; } final JsonNode node = ext.getProperties().get(GPP_PROPERTY); return node != null && node.isTextual() ? node.asText() : null; } - private static List resolveGppSid(Regs regs, ExtRegs ext) { + private static List gppSidToPromote(Regs regs, ExtRegs ext) { if (!CollectionUtils.isEmpty(regs.getGppSid())) { - return regs.getGppSid(); + return null; } final JsonNode node = ext.getProperties().get(GPP_SID_PROPERTY); - if (node == null || !node.isArray()) { - return regs.getGppSid(); + if (node == null || !node.isArray() || node.isEmpty()) { + return null; } final List sids = new ArrayList<>(node.size()); for (final JsonNode elem : node) { if (!elem.isIntegralNumber()) { - return regs.getGppSid(); + return null; } sids.add(elem.asInt()); } - return sids.isEmpty() ? regs.getGppSid() : sids; + return sids; } - private static Integer resolveCoppa(Regs regs, ExtRegs ext) { + private static Integer coppaToPromote(Regs regs, ExtRegs ext) { if (regs.getCoppa() != null) { - return regs.getCoppa(); + return null; } final JsonNode node = ext.getProperties().get(COPPA_PROPERTY); return node != null && node.isIntegralNumber() ? node.asInt() : null; } - // 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) -> { - 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); + // Rebuild regs.ext keeping the typed fields and every property except the promoted ones. + private static ExtRegs removePromotedKeys(ExtRegs ext, + boolean gppPromoted, + boolean gppSidPromoted, + boolean coppaPromoted) { + final ExtRegs result = ExtRegs.of( + ext.getGdpr(), ext.getUsPrivacy(), ext.getGpc(), ext.getDsa()); + ext.getProperties().forEach((key, value) -> { + final boolean isPromotedKey = (gppPromoted && GPP_PROPERTY.equals(key)) + || (gppSidPromoted && GPP_SID_PROPERTY.equals(key)) + || (coppaPromoted && COPPA_PROPERTY.equals(key)); + if (!isPromotedKey) { + result.addProperty(key, value); } }); - return isExtEmpty(stripped) ? null : stripped; + return isExtEmpty(result) ? null : result; } private static boolean isExtEmpty(ExtRegs ext) { @@ -252,7 +253,7 @@ private static boolean isExtEmpty(ExtRegs ext) { && ext.getUsPrivacy() == null && ext.getGpc() == null && ext.getDsa() == null - && (ext.getProperties() == null || ext.getProperties().isEmpty()); + && ext.getProperties().isEmpty(); } private HttpRequest makeHttpRequest(BidRequest outgoingRequest) { 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 38446fdbb71..170569171d5 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -584,6 +584,34 @@ public void makeHttpRequestsShouldNotPromoteGppSidWhenArrayHasNonIntegerElement( assertThat(keptSid.get(2).asInt()).isEqualTo(8); } + @Test + public void makeHttpRequestsShouldKeepExtGppWhenTopLevelGppAlreadySetEvenIfSiblingIsPromoted() { + // gpp is present at BOTH top-level and in ext; a sibling (coppa) is promoted from ext. + // gpp is not promoted (top-level wins), so its ext copy must be left untouched - + // the strip decision does not depend on the sibling rebuild. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .gpp("top-level-gpp") + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + bidRequest.getRegs().getExt().addProperty("gpp", TextNode.valueOf("ext-gpp")); + 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(); + // coppa promoted and removed from ext + assertThat(regs.getCoppa()).isEqualTo(1); + // gpp top-level untouched; ext gpp left in place (not promoted, not stripped) + assertThat(regs.getGpp()).isEqualTo("top-level-gpp"); + assertThat(regs.getExt()).isNotNull(); + assertThat(regs.getExt().getProperty("coppa")).isNull(); + assertThat(regs.getExt().getProperty("gpp").asText()).isEqualTo("ext-gpp"); + } + @Test public void makeHttpRequestsShouldShortCircuitWhenRegsHasNoExt() { final BidRequest bidRequest = givenBidRequest(identity(), From 68fc1c5a2ec8d3bc958b1345b5814f29aac8e7d6 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Fri, 17 Jul 2026 00:32:38 +0530 Subject: [PATCH 05/17] Yahoo Ads: strip superseded regs.ext copies so top-level always wins Align the regs ext-to-top promotion with core's rule for gdpr/us_privacy in BidRequestOrtb25To26Converter: an ext gpp/gpp_sid/coppa key is removed whenever the outbound request carries a top-level value for that field, whether the value was just promoted or was already set, so one signal is never sent with two conflicting values. Malformed ext values with no top-level counterpart and all unrelated regs.ext content stay untouched. --- .../bidder/yahooads/YahooAdsBidder.java | 34 +++++++++++-------- .../bidder/yahooads/YahooAdsBidderTest.java | 32 +++++++++++++---- 2 files changed, 46 insertions(+), 20 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 630fe1d4d34..21b466c0401 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -163,9 +163,8 @@ private static Banner modifyBanner(Banner banner) { } // Promote legacy 2.5 regs.ext gpp/gpp_sid/coppa to their 2.6 top-level slots. - // A field is promoted only when it is absent at top-level and present and well-formed - // in ext; each promoted key is then removed from ext. Anything not promoted (already - // top-level, missing, or malformed) is left untouched, in ext. + // An ext key is removed whenever the outbound request has a top-level value for that + // field, so one signal never goes out with two values; everything else stays in ext. private static Regs promoteRegsExtToTopLevel(Regs regs) { final ExtRegs ext = regs.getExt(); if (ext == null || ext.getProperties().isEmpty()) { @@ -176,7 +175,14 @@ private static Regs promoteRegsExtToTopLevel(Regs regs) { final List promotedGppSid = gppSidToPromote(regs, ext); final Integer promotedCoppa = coppaToPromote(regs, ext); - if (promotedGpp == null && promotedGppSid == null && promotedCoppa == null) { + final boolean gppSuperseded = ext.containsProperty(GPP_PROPERTY) + && (promotedGpp != null || regs.getGpp() != null); + final boolean gppSidSuperseded = ext.containsProperty(GPP_SID_PROPERTY) + && (promotedGppSid != null || !CollectionUtils.isEmpty(regs.getGppSid())); + final boolean coppaSuperseded = ext.containsProperty(COPPA_PROPERTY) + && (promotedCoppa != null || regs.getCoppa() != null); + + if (!gppSuperseded && !gppSidSuperseded && !coppaSuperseded) { return regs; } @@ -191,7 +197,7 @@ private static Regs promoteRegsExtToTopLevel(Regs regs) { builder.coppa(promotedCoppa); } return builder - .ext(removePromotedKeys(ext, promotedGpp != null, promotedGppSid != null, promotedCoppa != null)) + .ext(removeSupersededKeys(ext, gppSuperseded, gppSidSuperseded, coppaSuperseded)) .build(); } @@ -230,18 +236,18 @@ private static Integer coppaToPromote(Regs regs, ExtRegs ext) { return node != null && node.isIntegralNumber() ? node.asInt() : null; } - // Rebuild regs.ext keeping the typed fields and every property except the promoted ones. - private static ExtRegs removePromotedKeys(ExtRegs ext, - boolean gppPromoted, - boolean gppSidPromoted, - boolean coppaPromoted) { + // Rebuild regs.ext keeping the typed fields and every property except the superseded keys. + private static ExtRegs removeSupersededKeys(ExtRegs ext, + boolean gppSuperseded, + boolean gppSidSuperseded, + boolean coppaSuperseded) { final ExtRegs result = ExtRegs.of( ext.getGdpr(), ext.getUsPrivacy(), ext.getGpc(), ext.getDsa()); ext.getProperties().forEach((key, value) -> { - final boolean isPromotedKey = (gppPromoted && GPP_PROPERTY.equals(key)) - || (gppSidPromoted && GPP_SID_PROPERTY.equals(key)) - || (coppaPromoted && COPPA_PROPERTY.equals(key)); - if (!isPromotedKey) { + final boolean isSupersededKey = (gppSuperseded && GPP_PROPERTY.equals(key)) + || (gppSidSuperseded && GPP_SID_PROPERTY.equals(key)) + || (coppaSuperseded && COPPA_PROPERTY.equals(key)); + if (!isSupersededKey) { result.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 170569171d5..0fcc85baa41 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -585,10 +585,9 @@ public void makeHttpRequestsShouldNotPromoteGppSidWhenArrayHasNonIntegerElement( } @Test - public void makeHttpRequestsShouldKeepExtGppWhenTopLevelGppAlreadySetEvenIfSiblingIsPromoted() { - // gpp is present at BOTH top-level and in ext; a sibling (coppa) is promoted from ext. - // gpp is not promoted (top-level wins), so its ext copy must be left untouched - - // the strip decision does not depend on the sibling rebuild. + public void makeHttpRequestsShouldDropExtGppWhenTopLevelGppAlreadySet() { + // gpp sits at both top-level and ext with different values: top-level wins and the + // superseded ext copy is stripped, while the sibling coppa is promoted as usual. final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .gpp("top-level-gpp") @@ -605,11 +604,32 @@ public void makeHttpRequestsShouldKeepExtGppWhenTopLevelGppAlreadySetEvenIfSibli final Regs regs = result.getValue().getFirst().getPayload().getRegs(); // coppa promoted and removed from ext assertThat(regs.getCoppa()).isEqualTo(1); - // gpp top-level untouched; ext gpp left in place (not promoted, not stripped) + // gpp top-level untouched; superseded ext copy stripped assertThat(regs.getGpp()).isEqualTo("top-level-gpp"); + assertThat(regs.getExt()).isNull(); + } + + @Test + public void makeHttpRequestsShouldKeepExplicitZeroCoppaAndDropExtCopy() { + // An explicit top-level coppa 0 is a value, not "unset": it survives and the + // stale ext copy saying 1 is stripped rather than promoted over it. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .coppa(0) + .ext(ExtRegs.of(null, null, "1", null)) + .build()).device(Device.builder().ua("UA").build())); + 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(); + assertThat(regs.getCoppa()).isEqualTo(0); assertThat(regs.getExt()).isNotNull(); assertThat(regs.getExt().getProperty("coppa")).isNull(); - assertThat(regs.getExt().getProperty("gpp").asText()).isEqualTo("ext-gpp"); + assertThat(regs.getExt().getGpc()).isEqualTo("1"); } @Test From 81895da0c31787e054b9cb6fdd00071a01d3e9a9 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Sun, 19 Jul 2026 22:07:22 +0530 Subject: [PATCH 06/17] Yahoo Ads: treat out-of-int-range regs.ext values as malformed isIntegralNumber accepts long/BigInteger nodes whose asInt conversion silently overflows, so an oversized gpp_sid element or coppa value was promoted as a corrupted number. canConvertToInt rejects both non-integral and out-of-range values, letting such input fall through to the existing leave-in-ext-untouched path. --- .../bidder/yahooads/YahooAdsBidder.java | 4 +-- .../bidder/yahooads/YahooAdsBidderTest.java | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 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 21b466c0401..fe0f8b2d1d0 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -220,7 +220,7 @@ private static List gppSidToPromote(Regs regs, ExtRegs ext) { } final List sids = new ArrayList<>(node.size()); for (final JsonNode elem : node) { - if (!elem.isIntegralNumber()) { + if (!elem.canConvertToInt()) { return null; } sids.add(elem.asInt()); @@ -233,7 +233,7 @@ private static Integer coppaToPromote(Regs regs, ExtRegs ext) { return null; } final JsonNode node = ext.getProperties().get(COPPA_PROPERTY); - return node != null && node.isIntegralNumber() ? node.asInt() : null; + return node != null && node.canConvertToInt() ? node.asInt() : null; } // Rebuild regs.ext keeping the typed fields and every property except the superseded keys. 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 0fcc85baa41..19cba2ed5b8 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; @@ -584,6 +585,33 @@ public void makeHttpRequestsShouldNotPromoteGppSidWhenArrayHasNonIntegerElement( assertThat(keptSid.get(2).asInt()).isEqualTo(8); } + @Test + public void makeHttpRequestsShouldNotPromoteExtValuesThatDoNotFitInInt() { + // Values outside int range would silently overflow in asInt, so they are treated + // as malformed and left in ext untouched instead of being promoted corrupted. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + final ArrayNode oversized = mapper.createArrayNode(); + oversized.add(7); + oversized.add(3_000_000_000L); + bidRequest.getRegs().getExt().addProperty("gpp_sid", oversized); + bidRequest.getRegs().getExt().addProperty("coppa", LongNode.valueOf(2_147_483_648L)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getGppSid()).isNull(); + assertThat(regs.getCoppa()).isNull(); + assertThat(regs.getExt()).isNotNull(); + assertThat(regs.getExt().getProperty("gpp_sid").get(1).asLong()).isEqualTo(3_000_000_000L); + assertThat(regs.getExt().getProperty("coppa").asLong()).isEqualTo(2_147_483_648L); + } + @Test public void makeHttpRequestsShouldDropExtGppWhenTopLevelGppAlreadySet() { // gpp sits at both top-level and ext with different values: top-level wins and the From 985da4bf117313aef801c5ee2e03a650d01f4066 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Sun, 19 Jul 2026 22:11:42 +0530 Subject: [PATCH 07/17] Yahoo Ads: name the outbound OpenRTB version constant Hoist the hard-coded x-openrtb-version header value to an OPENRTB_VERSION constant, following the convention used by newer adapters, so the version lives in one visible place for the next migration. --- .../java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 fe0f8b2d1d0..2bdca15affd 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -45,6 +45,7 @@ public class YahooAdsBidder implements Bidder { new TypeReference<>() { }; + private static final String OPENRTB_VERSION = "2.6"; private static final String GPP_PROPERTY = "gpp"; private static final String GPP_SID_PROPERTY = "gpp_sid"; private static final String COPPA_PROPERTY = "coppa"; @@ -274,7 +275,7 @@ private HttpRequest makeHttpRequest(BidRequest outgoingRequest) { private static MultiMap makeHeaders(Device device) { final MultiMap headers = HttpUtil.headers() - .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.6"); + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, OPENRTB_VERSION); final String deviceUa = device != null ? device.getUa() : null; HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, deviceUa); From e449b0def378c27a3de8c530f385e6bdc5f50caf Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Sun, 19 Jul 2026 22:23:14 +0530 Subject: [PATCH 08/17] Yahoo Ads: exercise regs.ext promotion in the integration test The incoming fixture carried gpp/gpp_sid at top-level, so the bidder's ext-to-top promotion short-circuited and the integration test only covered passthrough. Nest gdpr/gpp/gpp_sid/coppa in regs.ext and expect the flattened top-level shape on the wire, so the test fails if the promotion stops working. coppa uses 0 to guard the explicit-zero case. --- .../openrtb2/yahooads/test-auction-yahooads-request.json | 7 ++++--- .../it/openrtb2/yahooads/test-yahooads-bid-request.json | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-request.json index fc5d0ee2c1a..3fd2d1d616e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-request.json @@ -17,10 +17,11 @@ ], "tmax": 5000, "regs": { - "gpp": "gppstring", - "gpp_sid": [6], "ext": { - "gdpr": 0 + "gdpr": 0, + "gpp": "gppstring", + "gpp_sid": [6], + "coppa": 0 } } } 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 dfe01cf07f1..145861aae25 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 @@ -44,7 +44,8 @@ "regs": { "gpp": "gppstring", "gpp_sid": [6], - "gdpr": 0 + "gdpr": 0, + "coppa": 0 }, "ext": { "prebid": { From 219759f843713ecb2cbfb673c93b996adadf19c5 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Sun, 19 Jul 2026 22:27:24 +0530 Subject: [PATCH 09/17] Yahoo Ads: promote regs.ext once per request instead of per imp promoteRegsExtToTopLevel is a pure function of the request-level regs, which never changes across imp iterations, so compute it once in makeHttpRequests and pass the promoted result into modifyRequest rather than recomputing it for every impression. --- .../prebid/server/bidder/yahooads/YahooAdsBidder.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 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 2bdca15affd..ae6ea87ba90 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -65,6 +65,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ final List errors = new ArrayList<>(); final Regs regs = bidRequest.getRegs(); + final Regs promotedRegs = regs != null ? promoteRegsExtToTopLevel(regs) : null; final List impList = bidRequest.getImp(); for (int i = 0; i < impList.size(); i++) { @@ -72,7 +73,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ final Imp imp = impList.get(i); final ExtImpYahooAds extImpYahooAds = parseAndValidateImpExt(imp.getExt(), i); final BidRequest modifiedRequest = modifyRequest(bidRequest, imp, extImpYahooAds, - regs); + promotedRegs); bidRequests.add(makeHttpRequest(modifiedRequest)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); @@ -105,7 +106,7 @@ private ExtImpYahooAds parseAndValidateImpExt(ObjectNode impExtNode, int index) } private BidRequest modifyRequest(BidRequest request, Imp imp, ExtImpYahooAds extImpYahooAds, - Regs regs) { + Regs promotedRegs) { final BidRequest.BidRequestBuilder requestBuilder = request.toBuilder(); final Site site = request.getSite(); @@ -117,8 +118,8 @@ private BidRequest modifyRequest(BidRequest request, Imp imp, ExtImpYahooAds ext requestBuilder.app(app.toBuilder().id(extImpYahooAds.getDcn()).build()); } - if (regs != null) { - requestBuilder.regs(promoteRegsExtToTopLevel(regs)); + if (promotedRegs != null) { + requestBuilder.regs(promotedRegs); } return requestBuilder From 7c56279e4a316871ce12f37663b8b01120764353 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Mon, 20 Jul 2026 14:55:06 +0530 Subject: [PATCH 10/17] Yahoo Ads: drop coppa from the regs.ext promotion coppa has been a top-level Regs field since OpenRTB 2.2 and never had a regs.ext location, so the bidder no longer promotes or strips it; an ext coppa is treated like any unknown ext property and passes through untouched. Promotion scope is now exactly gpp and gpp_sid, the two fields with a genuine 2.5 ext location. --- .../bidder/yahooads/YahooAdsBidder.java | 28 ++------ .../bidder/yahooads/YahooAdsBidderTest.java | 71 +++++++++---------- .../test-auction-yahooads-request.json | 3 +- .../yahooads/test-yahooads-bid-request.json | 3 +- 4 files changed, 40 insertions(+), 65 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 ae6ea87ba90..61c3037b899 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -48,7 +48,6 @@ public class YahooAdsBidder implements Bidder { private static final String OPENRTB_VERSION = "2.6"; 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 JacksonMapper mapper; @@ -164,7 +163,8 @@ private static Banner modifyBanner(Banner banner) { .build(); } - // Promote legacy 2.5 regs.ext gpp/gpp_sid/coppa to their 2.6 top-level slots. + // Promote legacy 2.5 regs.ext gpp/gpp_sid to their 2.6 top-level slots. coppa is not + // handled: it has been a top-level field since OpenRTB 2.2 and never had an ext slot. // An ext key is removed whenever the outbound request has a top-level value for that // field, so one signal never goes out with two values; everything else stays in ext. private static Regs promoteRegsExtToTopLevel(Regs regs) { @@ -175,16 +175,13 @@ private static Regs promoteRegsExtToTopLevel(Regs regs) { final String promotedGpp = gppToPromote(regs, ext); final List promotedGppSid = gppSidToPromote(regs, ext); - final Integer promotedCoppa = coppaToPromote(regs, ext); final boolean gppSuperseded = ext.containsProperty(GPP_PROPERTY) && (promotedGpp != null || regs.getGpp() != null); final boolean gppSidSuperseded = ext.containsProperty(GPP_SID_PROPERTY) && (promotedGppSid != null || !CollectionUtils.isEmpty(regs.getGppSid())); - final boolean coppaSuperseded = ext.containsProperty(COPPA_PROPERTY) - && (promotedCoppa != null || regs.getCoppa() != null); - if (!gppSuperseded && !gppSidSuperseded && !coppaSuperseded) { + if (!gppSuperseded && !gppSidSuperseded) { return regs; } @@ -195,11 +192,8 @@ private static Regs promoteRegsExtToTopLevel(Regs regs) { if (promotedGppSid != null) { builder.gppSid(promotedGppSid); } - if (promotedCoppa != null) { - builder.coppa(promotedCoppa); - } return builder - .ext(removeSupersededKeys(ext, gppSuperseded, gppSidSuperseded, coppaSuperseded)) + .ext(removeSupersededKeys(ext, gppSuperseded, gppSidSuperseded)) .build(); } @@ -230,25 +224,15 @@ private static List gppSidToPromote(Regs regs, ExtRegs ext) { return sids; } - private static Integer coppaToPromote(Regs regs, ExtRegs ext) { - if (regs.getCoppa() != null) { - return null; - } - final JsonNode node = ext.getProperties().get(COPPA_PROPERTY); - return node != null && node.canConvertToInt() ? node.asInt() : null; - } - // Rebuild regs.ext keeping the typed fields and every property except the superseded keys. private static ExtRegs removeSupersededKeys(ExtRegs ext, boolean gppSuperseded, - boolean gppSidSuperseded, - boolean coppaSuperseded) { + boolean gppSidSuperseded) { final ExtRegs result = ExtRegs.of( ext.getGdpr(), ext.getUsPrivacy(), ext.getGpc(), ext.getDsa()); ext.getProperties().forEach((key, value) -> { final boolean isSupersededKey = (gppSuperseded && GPP_PROPERTY.equals(key)) - || (gppSidSuperseded && GPP_SID_PROPERTY.equals(key)) - || (coppaSuperseded && COPPA_PROPERTY.equals(key)); + || (gppSidSuperseded && GPP_SID_PROPERTY.equals(key)); if (!isSupersededKey) { result.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 19cba2ed5b8..53c0cebd65d 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.IntNode; -import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; @@ -423,7 +422,7 @@ public void makeHttpRequestsShouldPreserveTopLevel26RegsAndExtTypedFields() { } @Test - public void makeHttpRequestsShouldPromoteLegacyExtGppGppSidAndCoppaToTopLevel() { + public void makeHttpRequestsShouldPromoteLegacyExtGppAndGppSidToTopLevel() { final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .ext(ExtRegs.of(null, null, null, null)) @@ -433,7 +432,6 @@ public void makeHttpRequestsShouldPromoteLegacyExtGppGppSidAndCoppaToTopLevel() 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); @@ -443,10 +441,33 @@ public void makeHttpRequestsShouldPromoteLegacyExtGppGppSidAndCoppaToTopLevel() final Regs regs = result.getValue().getFirst().getPayload().getRegs(); assertThat(regs.getGpp()).isEqualTo("legacy_gpp_value"); assertThat(regs.getGppSid()).containsExactly(6, 8); - assertThat(regs.getCoppa()).isEqualTo(1); assertThat(regs.getExt()).isNull(); } + @Test + public void makeHttpRequestsShouldNotPromoteExtCoppa() { + // coppa has been a top-level field since OpenRTB 2.2 and never had an ext slot, + // so an ext coppa is an unknown property: not promoted, passed through in 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("coppa", IntNode.valueOf(1)); + bidRequest.getRegs().getExt().addProperty("gpp", TextNode.valueOf("with_coppa")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getCoppa()).isNull(); + assertThat(regs.getGpp()).isEqualTo("with_coppa"); + assertThat(regs.getExt()).isNotNull(); + assertThat(regs.getExt().getProperty("coppa").asInt()).isEqualTo(1); + assertThat(regs.getExt().getProperty("gpp")).isNull(); + } + @Test public void makeHttpRequestsShouldPromoteOnlyGppFromExtAndStripIt() { final BidRequest bidRequest = givenBidRequest(identity(), @@ -540,7 +561,9 @@ public void makeHttpRequestsShouldLeaveMalformedExtValueInExtWhenSiblingFieldIsP 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)); + final ArrayNode sidArray = mapper.createArrayNode(); + sidArray.add(6); + bidRequest.getRegs().getExt().addProperty("gpp_sid", sidArray); bidRequest.getRegs().getExt().addProperty("gpp", IntNode.valueOf(99)); // when @@ -549,10 +572,10 @@ public void makeHttpRequestsShouldLeaveMalformedExtValueInExtWhenSiblingFieldIsP // then assertThat(result.getErrors()).isEmpty(); final Regs regs = result.getValue().getFirst().getPayload().getRegs(); - assertThat(regs.getCoppa()).isEqualTo(1); + assertThat(regs.getGppSid()).containsExactly(6); assertThat(regs.getGpp()).isNull(); assertThat(regs.getExt()).isNotNull(); - assertThat(regs.getExt().getProperty("coppa")).isNull(); + assertThat(regs.getExt().getProperty("gpp_sid")).isNull(); assertThat(regs.getExt().getProperty("gpp").asInt()).isEqualTo(99); } @@ -597,7 +620,6 @@ public void makeHttpRequestsShouldNotPromoteExtValuesThatDoNotFitInInt() { oversized.add(7); oversized.add(3_000_000_000L); bidRequest.getRegs().getExt().addProperty("gpp_sid", oversized); - bidRequest.getRegs().getExt().addProperty("coppa", LongNode.valueOf(2_147_483_648L)); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -606,23 +628,20 @@ public void makeHttpRequestsShouldNotPromoteExtValuesThatDoNotFitInInt() { assertThat(result.getErrors()).isEmpty(); final Regs regs = result.getValue().getFirst().getPayload().getRegs(); assertThat(regs.getGppSid()).isNull(); - assertThat(regs.getCoppa()).isNull(); assertThat(regs.getExt()).isNotNull(); assertThat(regs.getExt().getProperty("gpp_sid").get(1).asLong()).isEqualTo(3_000_000_000L); - assertThat(regs.getExt().getProperty("coppa").asLong()).isEqualTo(2_147_483_648L); } @Test public void makeHttpRequestsShouldDropExtGppWhenTopLevelGppAlreadySet() { - // gpp sits at both top-level and ext with different values: top-level wins and the - // superseded ext copy is stripped, while the sibling coppa is promoted as usual. + // gpp sits at both top-level and ext with different values: top-level wins and + // the superseded ext copy is stripped. final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder -> requestBuilder.regs(Regs.builder() .gpp("top-level-gpp") .ext(ExtRegs.of(null, null, null, null)) .build()).device(Device.builder().ua("UA").build())); bidRequest.getRegs().getExt().addProperty("gpp", TextNode.valueOf("ext-gpp")); - bidRequest.getRegs().getExt().addProperty("coppa", IntNode.valueOf(1)); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -630,36 +649,10 @@ public void makeHttpRequestsShouldDropExtGppWhenTopLevelGppAlreadySet() { // then assertThat(result.getErrors()).isEmpty(); final Regs regs = result.getValue().getFirst().getPayload().getRegs(); - // coppa promoted and removed from ext - assertThat(regs.getCoppa()).isEqualTo(1); - // gpp top-level untouched; superseded ext copy stripped assertThat(regs.getGpp()).isEqualTo("top-level-gpp"); assertThat(regs.getExt()).isNull(); } - @Test - public void makeHttpRequestsShouldKeepExplicitZeroCoppaAndDropExtCopy() { - // An explicit top-level coppa 0 is a value, not "unset": it survives and the - // stale ext copy saying 1 is stripped rather than promoted over it. - final BidRequest bidRequest = givenBidRequest(identity(), - requestBuilder -> requestBuilder.regs(Regs.builder() - .coppa(0) - .ext(ExtRegs.of(null, null, "1", null)) - .build()).device(Device.builder().ua("UA").build())); - 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(); - assertThat(regs.getCoppa()).isEqualTo(0); - assertThat(regs.getExt()).isNotNull(); - assertThat(regs.getExt().getProperty("coppa")).isNull(); - assertThat(regs.getExt().getGpc()).isEqualTo("1"); - } - @Test public void makeHttpRequestsShouldShortCircuitWhenRegsHasNoExt() { final BidRequest bidRequest = givenBidRequest(identity(), diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-request.json index 3fd2d1d616e..40ca0f80c8e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-request.json @@ -20,8 +20,7 @@ "ext": { "gdpr": 0, "gpp": "gppstring", - "gpp_sid": [6], - "coppa": 0 + "gpp_sid": [6] } } } 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 145861aae25..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 @@ -44,8 +44,7 @@ "regs": { "gpp": "gppstring", "gpp_sid": [6], - "gdpr": 0, - "coppa": 0 + "gdpr": 0 }, "ext": { "prebid": { From b124a99162bb5ddaa16821c5c00e781e14a37903 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Mon, 20 Jul 2026 15:51:05 +0530 Subject: [PATCH 11/17] Yahoo Ads: reject fractional gpp_sid elements and pin promotion edge cases canConvertToInt alone only range-checks a double node, so an ext gpp_sid element like 6.5 passed the malformed guard and silently truncated to 6 in asInt. Combine it with isIntegralNumber so fractional values take the leave-in-ext path, and add tests for that case plus three previously unpinned behaviors: an empty top-level gpp_sid array counts as unset, per-field independence when one field is superseded while the other promotes, and the no-op pass when ext holds only unrelated properties. --- .../bidder/yahooads/YahooAdsBidder.java | 2 +- .../bidder/yahooads/YahooAdsBidderTest.java | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) 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 61c3037b899..9dde7d5f07d 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -216,7 +216,7 @@ private static List gppSidToPromote(Regs regs, ExtRegs ext) { } final List sids = new ArrayList<>(node.size()); for (final JsonNode elem : node) { - if (!elem.canConvertToInt()) { + if (!elem.isIntegralNumber() || !elem.canConvertToInt()) { return null; } sids.add(elem.asInt()); 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 53c0cebd65d..691686f5bcb 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -632,6 +632,96 @@ public void makeHttpRequestsShouldNotPromoteExtValuesThatDoNotFitInInt() { assertThat(regs.getExt().getProperty("gpp_sid").get(1).asLong()).isEqualTo(3_000_000_000L); } + @Test + public void makeHttpRequestsShouldNotPromoteGppSidWhenArrayHasFractionalElement() { + // canConvertToInt alone range-checks a double, so 6.5 would truncate to 6 in asInt; + // the integral-number guard treats it as malformed and leaves the array in 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())); + final ArrayNode fractional = mapper.createArrayNode(); + fractional.add(6.5); + bidRequest.getRegs().getExt().addProperty("gpp_sid", fractional); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getGppSid()).isNull(); + assertThat(regs.getExt()).isNotNull(); + assertThat(regs.getExt().getProperty("gpp_sid").get(0).asDouble()).isEqualTo(6.5); + } + + @Test + public void makeHttpRequestsShouldPromoteExtGppSidOverEmptyTopLevelArray() { + // An empty top-level gpp_sid carries no signal, so it counts as unset for promotion. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .gppSid(emptyList()) + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + final ArrayNode sidArray = mapper.createArrayNode(); + sidArray.add(6); + bidRequest.getRegs().getExt().addProperty("gpp_sid", sidArray); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getGppSid()).containsExactly(6); + assertThat(regs.getExt()).isNull(); + } + + @Test + public void makeHttpRequestsShouldPromoteGppSidWhileTopLevelGppSupersedesItsExtCopy() { + // Per-field independence: gpp keeps its top-level value and loses its ext copy + // while gpp_sid is promoted from ext in the same pass. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .gpp("top-level-gpp") + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + bidRequest.getRegs().getExt().addProperty("gpp", TextNode.valueOf("stale-ext-gpp")); + final ArrayNode sidArray = mapper.createArrayNode(); + sidArray.add(6); + bidRequest.getRegs().getExt().addProperty("gpp_sid", sidArray); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getGpp()).isEqualTo("top-level-gpp"); + assertThat(regs.getGppSid()).containsExactly(6); + assertThat(regs.getExt()).isNull(); + } + + @Test + public void makeHttpRequestsShouldReturnRegsUnchangedWhenExtHasOnlyUnrelatedProperties() { + 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("customKey", 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()).isNull(); + assertThat(regs.getGppSid()).isNull(); + assertThat(regs.getExt()).isNotNull(); + assertThat(regs.getExt().getProperty("customKey").asText()).isEqualTo("keep_me"); + } + @Test public void makeHttpRequestsShouldDropExtGppWhenTopLevelGppAlreadySet() { // gpp sits at both top-level and ext with different values: top-level wins and From ff9cb8da612a4ed6965a968cd172f2f6815dc020 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Tue, 21 Jul 2026 14:39:35 +0530 Subject: [PATCH 12/17] Yahoo Ads: drop stale coppa assertions from the wrong-type test coppa is no longer promoted at any type, so the wrong-type case is covered by the dedicated not-promoted test and these assertions were vacuous. The test now exercises only gpp and gpp_sid type handling. --- .../org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java | 3 --- 1 file changed, 3 deletions(-) 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 691686f5bcb..c37f837d486 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -538,7 +538,6 @@ public void makeHttpRequestsShouldNotPromoteWhenExtPropertyHasWrongType() { .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); @@ -548,11 +547,9 @@ public void makeHttpRequestsShouldNotPromoteWhenExtPropertyHasWrongType() { final Regs regs = result.getValue().getFirst().getPayload().getRegs(); assertThat(regs.getGpp()).isNull(); assertThat(regs.getGppSid()).isNull(); - assertThat(regs.getCoppa()).isNull(); 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 From aa383ceb3d8bf8d9cd8cec80bb2456a5fb72a34a Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Tue, 21 Jul 2026 14:49:22 +0530 Subject: [PATCH 13/17] Yahoo Ads: remove coppa references from tests and comments coppa is outside this migration's scope, so drop the dedicated not-promoted test, the coppa lines in the passthrough tests, and the comment sentence about it. Its passthrough behavior is the default unknown-key path already covered by the unrelated-properties test. --- .../bidder/yahooads/YahooAdsBidder.java | 3 +-- .../bidder/yahooads/YahooAdsBidderTest.java | 27 ------------------- 2 files changed, 1 insertion(+), 29 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 9dde7d5f07d..d786ab13ea0 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -163,8 +163,7 @@ private static Banner modifyBanner(Banner banner) { .build(); } - // Promote legacy 2.5 regs.ext gpp/gpp_sid to their 2.6 top-level slots. coppa is not - // handled: it has been a top-level field since OpenRTB 2.2 and never had an ext slot. + // Promote legacy 2.5 regs.ext gpp/gpp_sid to their 2.6 top-level slots. // An ext key is removed whenever the outbound request has a top-level value for that // field, so one signal never goes out with two values; everything else stays in ext. private static Regs promoteRegsExtToTopLevel(Regs regs) { 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 c37f837d486..033d929c6a7 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -401,7 +401,6 @@ public void makeHttpRequestsShouldPreserveTopLevel26RegsAndExtTypedFields() { .usPrivacy("1YNN") .gpp("gppconsent") .gppSid(List.of(6)) - .coppa(1) .ext(ExtRegs.of(null, null, "1", dsa)) .build()).device(Device.builder().ua("UA").build())); @@ -415,7 +414,6 @@ public void makeHttpRequestsShouldPreserveTopLevel26RegsAndExtTypedFields() { assertThat(regs.getUsPrivacy()).isEqualTo("1YNN"); assertThat(regs.getGpp()).isEqualTo("gppconsent"); assertThat(regs.getGppSid()).containsExactly(6); - assertThat(regs.getCoppa()).isEqualTo(1); assertThat(regs.getExt()).isNotNull(); assertThat(regs.getExt().getGpc()).isEqualTo("1"); assertThat(regs.getExt().getDsa()).isEqualTo(dsa); @@ -444,30 +442,6 @@ public void makeHttpRequestsShouldPromoteLegacyExtGppAndGppSidToTopLevel() { assertThat(regs.getExt()).isNull(); } - @Test - public void makeHttpRequestsShouldNotPromoteExtCoppa() { - // coppa has been a top-level field since OpenRTB 2.2 and never had an ext slot, - // so an ext coppa is an unknown property: not promoted, passed through in 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("coppa", IntNode.valueOf(1)); - bidRequest.getRegs().getExt().addProperty("gpp", TextNode.valueOf("with_coppa")); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - final Regs regs = result.getValue().getFirst().getPayload().getRegs(); - assertThat(regs.getCoppa()).isNull(); - assertThat(regs.getGpp()).isEqualTo("with_coppa"); - assertThat(regs.getExt()).isNotNull(); - assertThat(regs.getExt().getProperty("coppa").asInt()).isEqualTo(1); - assertThat(regs.getExt().getProperty("gpp")).isNull(); - } - @Test public void makeHttpRequestsShouldPromoteOnlyGppFromExtAndStripIt() { final BidRequest bidRequest = givenBidRequest(identity(), @@ -484,7 +458,6 @@ public void makeHttpRequestsShouldPromoteOnlyGppFromExtAndStripIt() { 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(); } From 158a0ebc8d5b6c03bf005ee405dace104ea0623b Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Tue, 21 Jul 2026 15:29:38 +0530 Subject: [PATCH 14/17] Yahoo Ads: pin top-level gpp_sid winning over a conflicting ext copy Mirrors the existing gpp conflict test for the array field: when gpp_sid is set at both top-level and in regs.ext with different values, the top-level value is kept and the superseded ext copy is stripped. --- .../bidder/yahooads/YahooAdsBidderTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 033d929c6a7..4316e9a6525 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -713,6 +713,29 @@ public void makeHttpRequestsShouldDropExtGppWhenTopLevelGppAlreadySet() { assertThat(regs.getExt()).isNull(); } + @Test + public void makeHttpRequestsShouldDropExtGppSidWhenTopLevelGppSidAlreadySet() { + // gpp_sid sits at both top-level and ext with different values: top-level wins and + // the superseded ext copy is stripped, mirroring the gpp scalar-field behavior. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .gppSid(List.of(99)) + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + final ArrayNode sidArray = mapper.createArrayNode(); + sidArray.add(6); + bidRequest.getRegs().getExt().addProperty("gpp_sid", sidArray); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getGppSid()).containsExactly(99); + assertThat(regs.getExt()).isNull(); + } + @Test public void makeHttpRequestsShouldShortCircuitWhenRegsHasNoExt() { final BidRequest bidRequest = givenBidRequest(identity(), From 1edf2eb996cee59480b91a9d2e859f25819ee407 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Tue, 21 Jul 2026 15:53:04 +0530 Subject: [PATCH 15/17] Yahoo Ads: pin empty top-level gpp counting as set An empty top-level gpp is a non-null value, so it blocks promotion and its superseded ext copy is stripped, matching core's null-check rule for us_privacy in BidRequestOrtb25To26Converter. --- .../bidder/yahooads/YahooAdsBidderTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 4316e9a6525..c60e6016759 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -713,6 +713,27 @@ public void makeHttpRequestsShouldDropExtGppWhenTopLevelGppAlreadySet() { assertThat(regs.getExt()).isNull(); } + @Test + public void makeHttpRequestsShouldTreatEmptyTopLevelGppAsSet() { + // An empty top-level gpp is a non-null value, so it counts as set: no promotion + // and the superseded ext copy is stripped, matching core's rule for us_privacy. + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.regs(Regs.builder() + .gpp("") + .ext(ExtRegs.of(null, null, null, null)) + .build()).device(Device.builder().ua("UA").build())); + bidRequest.getRegs().getExt().addProperty("gpp", TextNode.valueOf("ext-gpp")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getGpp()).isEmpty(); + assertThat(regs.getExt()).isNull(); + } + @Test public void makeHttpRequestsShouldDropExtGppSidWhenTopLevelGppSidAlreadySet() { // gpp_sid sits at both top-level and ext with different values: top-level wins and From 6e927d457e8ae1c012bd17071cf0e255b184bbd4 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Tue, 21 Jul 2026 16:02:28 +0530 Subject: [PATCH 16/17] Yahoo Ads: pin the empty ext gpp_sid array as not promotable An empty ext gpp_sid array carries no value, so nothing is promoted and the array stays in ext untouched. Covers the node.isEmpty guard arm that no other test reached. --- .../bidder/yahooads/YahooAdsBidderTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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 c60e6016759..325657882cd 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -602,6 +602,28 @@ public void makeHttpRequestsShouldNotPromoteExtValuesThatDoNotFitInInt() { assertThat(regs.getExt().getProperty("gpp_sid").get(1).asLong()).isEqualTo(3_000_000_000L); } + @Test + public void makeHttpRequestsShouldNotPromoteEmptyExtGppSidArray() { + // An empty ext gpp_sid array carries no value to promote: nothing changes and + // the empty array stays in ext untouched. + 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_sid", mapper.createArrayNode()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final Regs regs = result.getValue().getFirst().getPayload().getRegs(); + assertThat(regs.getGppSid()).isNull(); + assertThat(regs.getExt()).isNotNull(); + assertThat(regs.getExt().getProperty("gpp_sid").isArray()).isTrue(); + assertThat(regs.getExt().getProperty("gpp_sid")).isEmpty(); + } + @Test public void makeHttpRequestsShouldNotPromoteGppSidWhenArrayHasFractionalElement() { // canConvertToInt alone range-checks a double, so 6.5 would truncate to 6 in asInt; From b6fc04a663fc7f85928370087bcfc2196bf150c3 Mon Sep 17 00:00:00 2001 From: MananRPatel Date: Tue, 21 Jul 2026 16:08:50 +0530 Subject: [PATCH 17/17] Yahoo Ads: pin the shared promoted regs across a multi-imp request Promotion runs once per request and the promoted regs instance is shared by every per-imp outbound request, so pin that all of them carry the same correctly promoted values. --- .../bidder/yahooads/YahooAdsBidderTest.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) 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 325657882cd..fa0fc5a2533 100644 --- a/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yahooads/YahooAdsBidderTest.java @@ -779,6 +779,36 @@ public void makeHttpRequestsShouldDropExtGppSidWhenTopLevelGppSidAlreadySet() { assertThat(regs.getExt()).isNull(); } + @Test + public void makeHttpRequestsShouldApplyTheSamePromotedRegsToEveryImp() { + // Promotion runs once per request and the promoted regs is shared by every + // per-imp outbound request, so all of them carry the same correct values. + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().id("123").build()) + .imp(asList( + givenImp(impBuilder -> impBuilder.id("imp1")), + givenImp(impBuilder -> impBuilder.id("imp2")))) + .regs(Regs.builder() + .ext(ExtRegs.of(null, null, null, null)) + .build()) + .device(Device.builder().ua("UA").build()) + .build(); + bidRequest.getRegs().getExt().addProperty("gpp", TextNode.valueOf("shared-gpp")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getRegs) + .allSatisfy(regs -> { + assertThat(regs.getGpp()).isEqualTo("shared-gpp"); + assertThat(regs.getExt()).isNull(); + }); + } + @Test public void makeHttpRequestsShouldShortCircuitWhenRegsHasNoExt() { final BidRequest bidRequest = givenBidRequest(identity(),