From 06893a553686d0e89ab8f6b80b22a8e397fe48b2 Mon Sep 17 00:00:00 2001 From: przemkaczmarek <167743744+przemkaczmarek@users.noreply.github.com> Date: Fri, 16 May 2025 17:38:52 +0200 Subject: [PATCH 1/7] NextMillennium: Adapter and server version (#3814) --- .../nextmillennium/NextMillenniumBidder.java | 94 +++-- .../proto/NextMillenniumExt.java | 11 + .../proto/NextMillenniumExtBidder.java | 17 + .../bidder/NextMillenniumConfiguration.java | 5 +- .../NextMillenniumBidderTest.java | 350 ++++++++++-------- .../test-nextmillennium-bid-request.json | 21 +- 6 files changed, 287 insertions(+), 211 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExt.java create mode 100644 src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExtBidder.java diff --git a/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java b/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java index 426e287190e..c026a6c271b 100644 --- a/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java +++ b/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java @@ -12,7 +12,6 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -22,17 +21,22 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.nextmillennium.proto.NextMillenniumExt; +import org.prebid.server.bidder.nextmillennium.proto.NextMillenniumExtBidder; import org.prebid.server.exception.PreBidException; 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.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidServer; import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.nextmillennium.ExtImpNextMillennium; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; +import org.prebid.server.version.PrebidVersionProvider; import java.util.ArrayList; import java.util.Collection; @@ -46,15 +50,22 @@ public class NextMillenniumBidder implements Bidder { new TypeReference<>() { }; + private static final String NM_ADAPTER_VERSION = "v1.0.0"; + private final String endpointUrl; private final JacksonMapper mapper; private final List nmmFlags; + private final PrebidVersionProvider versionProvider; + + public NextMillenniumBidder(String endpointUrl, + JacksonMapper mapper, + List nmmFlags, + PrebidVersionProvider versionProvider) { - public NextMillenniumBidder(String endpointUrl, JacksonMapper mapper, List nmmFlags) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); this.nmmFlags = nmmFlags; - + this.versionProvider = Objects.requireNonNull(versionProvider); } @Override @@ -70,7 +81,12 @@ public final Result>> makeHttpRequests(BidRequest b errors.add(BidderError.badInput(e.getMessage())); continue; } - httpRequests.add(makeHttpRequest(updateBidRequest(bidRequest, extImpNextMillennium))); + final BidRequest updatedRequest = updateBidRequest(bidRequest, extImpNextMillennium); + httpRequests.add(BidderUtil.defaultRequest( + updatedRequest, + headers(), + endpointUrl, + mapper)); } return errors.isEmpty() ? Result.withValues(httpRequests) : Result.withErrors(errors); @@ -84,31 +100,19 @@ private ExtImpNextMillennium convertExt(ObjectNode impExt) { } } - private BidRequest updateBidRequest(BidRequest bidRequest, ExtImpNextMillennium ext) { - final ExtStoredRequest storedRequest = ExtStoredRequest.of(resolveStoredRequestId(bidRequest, ext)); - - final ExtRequestPrebid createdExtRequestPrebid = ExtRequestPrebid.builder() - .storedrequest(storedRequest) - .build(); - - final ExtRequestPrebid extRequestPrebid = Optional.of(bidRequest) - .map(BidRequest::getExt) + private BidRequest updateBidRequest(BidRequest bidRequest, ExtImpNextMillennium extImp) { + final String soredRequestId = resolveStoredRequestId(bidRequest, extImp); + final ExtRequestPrebidServer extRequestPrebidServer = Optional.ofNullable(bidRequest.getExt()) .map(ExtRequest::getPrebid) - .map(prebid -> prebid.toBuilder().storedrequest(storedRequest).build()) - .orElse(createdExtRequestPrebid); + .map(ExtRequestPrebid::getServer) + .orElse(null); return bidRequest.toBuilder() - .imp(updateImps(bidRequest, createdExtRequestPrebid)) - .ext(ExtRequest.of(extRequestPrebid)) + .imp(modifyFirstImp(bidRequest.getImp(), soredRequestId)) + .ext(createExtRequest(soredRequestId, extRequestPrebidServer)) .build(); } - private List updateImps(BidRequest bidRequest, ExtRequestPrebid extRequestPrebid) { - return bidRequest.getImp().stream() - .map(imp -> imp.toBuilder().ext(createImpExt(extRequestPrebid)).build()) - .toList(); - } - private static String resolveStoredRequestId(BidRequest bidRequest, ExtImpNextMillennium extImpNextMillennium) { final String groupId = extImpNextMillennium.getGroupId(); if (StringUtils.isEmpty(groupId)) { @@ -146,24 +150,38 @@ private static String formatSize(Integer w, Integer h) { : null; } - private ObjectNode createImpExt(ExtRequestPrebid prebid) { - final ObjectNode impExt = mapper.mapper().createObjectNode(); - impExt.set("prebid", mapper.mapper().valueToTree(prebid)); - if (CollectionUtils.isNotEmpty(nmmFlags)) { - impExt.putObject("nextMillennium") - .set("nmmFlags", mapper.mapper().valueToTree(nmmFlags)); - } - return impExt; + private List modifyFirstImp(List imps, String storedRequestId) { + final ExtRequestPrebid extRequestPrebid = ExtRequestPrebid.builder() + .storedrequest(ExtStoredRequest.of(storedRequestId)) + .build(); + + final NextMillenniumExt nextMillenniumExt = NextMillenniumExt.of( + NextMillenniumExtBidder.of(nmmFlags, null, null)); + + final ExtRequest extRequest = ExtRequest.of(extRequestPrebid); + mapper.fillExtension(extRequest, nextMillenniumExt); + + final ObjectNode impExt = mapper.mapper().valueToTree(extRequest); + + final List modifiedImps = new ArrayList<>(imps); + modifiedImps.set(0, imps.getFirst().toBuilder().ext(impExt).build()); + + return modifiedImps; } - private HttpRequest makeHttpRequest(BidRequest bidRequest) { - return HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .headers(headers()) - .payload(bidRequest) - .body(mapper.encodeToBytes(bidRequest)) + private ExtRequest createExtRequest(String storedRequestId, ExtRequestPrebidServer extRequestPrebidServer) { + final ExtRequestPrebid extRequestPrebid = ExtRequestPrebid.builder() + .storedrequest(ExtStoredRequest.of(storedRequestId)) + .server(extRequestPrebidServer) .build(); + + final NextMillenniumExt nextMillenniumExt = NextMillenniumExt.of( + NextMillenniumExtBidder.of(nmmFlags, NM_ADAPTER_VERSION, versionProvider.getNameVersionRecord())); + + final ExtRequest extRequest = ExtRequest.of(extRequestPrebid); + mapper.fillExtension(extRequest, nextMillenniumExt); + + return extRequest; } private static MultiMap headers() { diff --git a/src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExt.java b/src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExt.java new file mode 100644 index 00000000000..b5b8f58f6c2 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.nextmillennium.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class NextMillenniumExt { + + @JsonProperty("nextMillennium") + NextMillenniumExtBidder nextMillennium; +} diff --git a/src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExtBidder.java b/src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExtBidder.java new file mode 100644 index 00000000000..1673fc5114d --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExtBidder.java @@ -0,0 +1,17 @@ +package org.prebid.server.bidder.nextmillennium.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class NextMillenniumExtBidder { + + @JsonProperty("nmmFlags") + List nmmFlags; + + String nmVersion; + + String serverVersion; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/NextMillenniumConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/NextMillenniumConfiguration.java index 16fbbb37aca..4d102b90f9c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/NextMillenniumConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/NextMillenniumConfiguration.java @@ -10,6 +10,7 @@ import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.prebid.server.version.PrebidVersionProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -34,6 +35,7 @@ NextMillenniumConfigurationProperties configurationProperties() { @Bean BidderDeps nextMillenniumBidderDeps(NextMillenniumConfigurationProperties nextMillenniumConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, + PrebidVersionProvider prebidVersionProvider, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) @@ -42,7 +44,8 @@ BidderDeps nextMillenniumBidderDeps(NextMillenniumConfigurationProperties nextMi .bidderCreator(config -> new NextMillenniumBidder( config.getEndpoint(), mapper, - config.getExtraInfo().getNmmFlags()) + config.getExtraInfo().getNmmFlags(), + prebidVersionProvider) ).assemble(); } diff --git a/src/test/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidderTest.java b/src/test/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidderTest.java index 6f3524a5448..71dfe2a8552 100644 --- a/src/test/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidderTest.java @@ -1,7 +1,6 @@ package org.prebid.server.bidder.nextmillennium; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; @@ -12,7 +11,11 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; +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.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -23,10 +26,12 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidServer; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.nextmillennium.ExtImpNextMillennium; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.version.PrebidVersionProvider; import java.util.Arrays; import java.util.List; @@ -39,17 +44,33 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +@ExtendWith(MockitoExtension.class) public class NextMillenniumBidderTest extends VertxTest { private static final String ENDPOINT_URL = "https://test-url.com/"; - private final NextMillenniumBidder target = - new NextMillenniumBidder(ENDPOINT_URL, jacksonMapper, List.of("valueOne", "valueTwo")); + @Mock + private PrebidVersionProvider prebidVersionProvider; + + private NextMillenniumBidder target; + + @BeforeEach + public void setUp() { + target = new NextMillenniumBidder( + ENDPOINT_URL, + jacksonMapper, + List.of("valueOne", "valueTwo"), + prebidVersionProvider); + } @Test public void creationShouldFailOnInvalidEndpointUrl() { assertThatIllegalArgumentException().isThrownBy(() -> - new NextMillenniumBidder("invalid_url", jacksonMapper, List.of("valueOne", "valueTwo"))); + new NextMillenniumBidder( + "invalid_url", + jacksonMapper, + List.of("valueOne", "valueTwo"), + prebidVersionProvider)); } @Test @@ -258,10 +279,162 @@ public void makeHttpRequestsWithInvalidImpsShouldReturnError() { } @Test - public void makeHttpRequestsShouldReturnImpExtNextMillenniumWhenNmmFlagsConfigured() { + public void makeHttpRequestsImpExtComparison() { // given - final BidRequest bidRequest = givenBidRequest(identity(), - givenImpWithExt(identity(), ExtImpNextMillennium.of("placement1", "group1"))); + final String placementId = "6821"; + final Imp givenImp = givenImpWithExt(imp -> imp + .id("custom_imp_id") + .tagid("custom_imp_tagid") + .secure(1) + .banner(Banner.builder() + .w(728) + .h(90) + .format(singletonList(Format.builder() + .w(728) + .h(90) + .build())) + .build()), + ExtImpNextMillennium.of(placementId, null)); + + final ExtRequest extRequest = ExtRequest.of(ExtRequestPrebid.builder() + .schains(emptyList()) + .targeting(ExtRequestTargeting.builder() + .includewinners(true) + .includebidderkeys(false) + .build()) + .build()); + + final BidRequest bidRequest = givenBidRequest(b -> b + .id("c868fd0b-960c-4f49-a8d6-2b3e938b41f2") + .test(1) + .tmax(845L) + .device(Device.builder().w(994).h(1768).build()) + .imp(singletonList(givenImp)) + .ext(extRequest) + .site(Site.builder().page("https://www.advfn.com").domain("www.advfn.com").build())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getStoredrequest) + .extracting(ExtStoredRequest::getId) + .containsExactly(placementId); + } + + @Test + public void makeHttpRequestsImpShouldBeIdenticalExceptExt() { + // given + final String placementId = "6821"; + final List bannerFormat = singletonList(Format.builder() + .w(728) + .h(90) + .build()); + + final Banner banner = Banner.builder() + .w(728) + .h(90) + .format(bannerFormat) + .build(); + + final Imp givenImp = givenImpWithExt(imp -> imp + .id("custom_imp_id") + .tagid("custom_imp_tagid") + .secure(1) + .banner(banner), + ExtImpNextMillennium.of(placementId, null)); + + final BidRequest bidRequest = givenBidRequest(b -> b + .id("c868fd0b-960c-4f49-a8d6-2b3e938b41f2") + .test(1) + .imp(singletonList(givenImp))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::getFirst) + .element(0) + .usingRecursiveComparison() + .ignoringFields("ext") + .isEqualTo(givenImp); + } + + @Test + public void makeHttpRequestsBidRequestShouldBeIdenticalExceptImpExt() { + // given + final String placementId = "6821"; + final List bannerFormat = singletonList(Format.builder() + .w(728) + .h(90) + .build()); + final Banner banner = Banner.builder() + .w(728) + .h(90) + .format(bannerFormat) + .build(); + + final Imp initialImp = givenImpWithExt(imp -> imp + .id("custom_imp_id") + .tagid("custom_imp_tagid") + .secure(1) + .banner(banner), + ExtImpNextMillennium.of(placementId, null)); + + final ExtRequest extRequest = ExtRequest.of(ExtRequestPrebid.builder() + .schains(emptyList()) + .targeting(ExtRequestTargeting.builder() + .includewinners(true) + .includebidderkeys(false) + .build()) + .build()); + + final BidRequest bidRequest = givenBidRequest(b -> b + .id("c868fd0b-960c-4f49-a8d6-2b3e938b41f2") + .test(1) + .tmax(845L) + .device(Device.builder().w(994).h(1768).build()) + .imp(singletonList(initialImp)) + .ext(extRequest) + .site(Site.builder().page("https://www.advfn.com").domain("www.advfn.com").build())); + // when + final Result>> result = target.makeHttpRequests(bidRequest); + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .element(0) + .usingRecursiveComparison() + .ignoringFields("imp", "ext") + .isEqualTo(bidRequest); + } + + @Test + public void makeHttpRequestsShouldPreserveExtPrebidServer() { + // given + final ExtRequestPrebidServer extRequestPrebidServer = ExtRequestPrebidServer.of( + "http://localhost:8080", + 1, + "dc-test", + "/openrtb2/auction"); + + final ExtRequest extRequest = ExtRequest.of(ExtRequestPrebid.builder() + .server(extRequestPrebidServer) + .build()); + + final BidRequest bidRequest = BidRequest.builder() + .id("test-request") + .imp(singletonList(givenImpWithExt( + imp -> imp.banner(Banner.builder().w(300).h(250).build()), + ExtImpNextMillennium.of("placement_id", null)))) + .ext(extRequest) + .build(); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -270,9 +443,10 @@ public void makeHttpRequestsShouldReturnImpExtNextMillenniumWhenNmmFlagsConfigur assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) .extracting(HttpRequest::getPayload) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getExt) - .containsExactly(createImpExt(List.of("valueOne", "valueTwo"))); + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getServer) + .containsExactly(extRequestPrebidServer); } @Test @@ -377,12 +551,6 @@ public void makeBidsWithZeroSeatBidsShouldReturnNoErrorsAndNoValues() throws Jso assertThat(result.getValue()).isEmpty(); } - private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer, - Imp... imps) { - - return bidRequestCustomizer.apply(BidRequest.builder().imp(asList(imps))).build(); - } - @Test public void makeBidsWithUnparsableBidResponseShouldReturnError() { // given @@ -401,145 +569,10 @@ public void makeBidsWithUnparsableBidResponseShouldReturnError() { }); } - @Test - public void makeHttpRequestsImpExtComparison() { - // given - final String placementId = "6821"; - final Imp givenImp = givenImpWithExt(imp -> imp - .id("custom_imp_id") - .tagid("custom_imp_tagid") - .secure(1) - .banner(Banner.builder() - .w(728) - .h(90) - .format(singletonList(Format.builder() - .w(728) - .h(90) - .build())) - .build()), - ExtImpNextMillennium.of(placementId, null)); - - final ExtRequest extRequest = ExtRequest.of(ExtRequestPrebid.builder() - .schains(emptyList()) - .targeting(ExtRequestTargeting.builder() - .includewinners(true) - .includebidderkeys(false) - .build()) - .build()); - - final BidRequest bidRequest = givenBidRequest(b -> b - .id("c868fd0b-960c-4f49-a8d6-2b3e938b41f2") - .test(1) - .tmax(845L) - .device(Device.builder().w(994).h(1768).build()) - .imp(singletonList(givenImp)) - .ext(extRequest) - .site(Site.builder().page("https://www.advfn.com").domain("www.advfn.com").build())); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - // then - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getImp) - .extracting(imps -> imps.getFirst()) - .extracting(Imp::getExt) - .isNotEqualTo(givenImp.getExt()) - .extracting(jsonNodes -> mapper.treeToValue(jsonNodes, ExtRequest.class)) - .extracting(ExtRequest::getPrebid) - .extracting(ExtRequestPrebid::getStoredrequest) - .extracting(ExtStoredRequest::getId) - .element(0) - .isEqualTo(placementId); - } - - @Test - public void makeHttpRequestsImpShouldBeIdenticalExceptExt() { - // given - final String placementId = "6821"; - final List bannerFormat = singletonList(Format.builder() - .w(728) - .h(90) - .build()); - - final Banner banner = Banner.builder() - .w(728) - .h(90) - .format(bannerFormat) - .build(); - - final Imp givenImp = givenImpWithExt(imp -> imp - .id("custom_imp_id") - .tagid("custom_imp_tagid") - .secure(1) - .banner(banner), - ExtImpNextMillennium.of(placementId, null)); - - final BidRequest bidRequest = givenBidRequest(b -> b - .id("c868fd0b-960c-4f49-a8d6-2b3e938b41f2") - .test(1) - .imp(singletonList(givenImp))); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getImp) - .extracting(imps -> imps.getFirst()) - .element(0) - .usingRecursiveComparison() - .ignoringFields("ext") - .isEqualTo(givenImp); - } - - @Test - public void makeHttpRequestsBidRequestShouldBeIdenticalExceptImpExt() { - // given - final String placementId = "6821"; - final List bannerFormat = singletonList(Format.builder() - .w(728) - .h(90) - .build()); - final Banner banner = Banner.builder() - .w(728) - .h(90) - .format(bannerFormat) - .build(); - - final Imp initialImp = givenImpWithExt(imp -> imp - .id("custom_imp_id") - .tagid("custom_imp_tagid") - .secure(1) - .banner(banner), - ExtImpNextMillennium.of(placementId, null)); - - final ExtRequest extRequest = ExtRequest.of(ExtRequestPrebid.builder() - .schains(emptyList()) - .targeting(ExtRequestTargeting.builder() - .includewinners(true) - .includebidderkeys(false) - .build()) - .build()); + private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer, + Imp... imps) { - final BidRequest bidRequest = givenBidRequest(b -> b - .id("c868fd0b-960c-4f49-a8d6-2b3e938b41f2") - .test(1) - .tmax(845L) - .device(Device.builder().w(994).h(1768).build()) - .imp(singletonList(initialImp)) - .ext(extRequest) - .site(Site.builder().page("https://www.advfn.com").domain("www.advfn.com").build())); - // when - final Result>> result = target.makeHttpRequests(bidRequest); - // then - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .element(0) - .usingRecursiveComparison() - .ignoringFields("imp", "ext") - .isEqualTo(bidRequest); + return bidRequestCustomizer.apply(BidRequest.builder().imp(asList(imps))).build(); } private static Imp givenImp(UnaryOperator impCustomizer) { @@ -570,13 +603,4 @@ private static Imp givenImpWithExt(UnaryOperator impCustomizer, return givenImp(impCustomizer.andThen(imp -> imp.ext(mapper.valueToTree( ExtPrebid.of(null, extImpNextMillennium))))::apply); } - - private static ObjectNode createImpExt(List values) { - final ObjectNode objectNode = mapper.createObjectNode(); - objectNode.set("prebid", mapper.valueToTree(ExtRequestPrebid.builder() - .storedrequest(ExtStoredRequest.of("ggroup1;;")).build())); - objectNode.putObject("nextMillennium") - .set("nmmFlags", mapper.valueToTree(values)); - return objectNode; - } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-request.json index 24a61053439..6802911a251 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-request.json @@ -3,28 +3,23 @@ "imp": [ { "id": "imp_id", - "secure": 1, "banner": { "w": 300, "h": 250 }, + "secure": 1, "ext": { + "nextMillennium" : { + "nmmFlags" : [ "1" ] + }, "prebid": { "storedrequest": { "id": "placement_id" } - }, - "nextMillennium": { - "nmmFlags": [ - "1" - ] } } } ], - "source": { - "tid": "${json-unit.any-string}" - }, "site": { "domain": "www.example.com", "page": "http://www.example.com", @@ -44,6 +39,9 @@ "cur": [ "USD" ], + "source": { + "tid": "${json-unit.any-string}" + }, "regs": { "ext": { "gdpr": 0 @@ -60,6 +58,11 @@ "datacenter": "local", "endpoint": "/openrtb2/auction" } + }, + "nextMillennium": { + "nmmFlags": [ "1" ], + "nm_version": "v1.0.0", + "server_version": "${json-unit.any-string}" } } } From fc6a3c1fff2c30f3827577fe6b5b898e6910418b Mon Sep 17 00:00:00 2001 From: sangarbe Date: Fri, 16 May 2025 17:39:01 +0200 Subject: [PATCH 2/7] New Adapter: Seedtag (#3916) --- .../server/bidder/seedtag/SeedtagBidder.java | 152 +++++++++++ .../ext/request/seedtag/ExtImpSeedtag.java | 12 + .../config/bidder/SeedtagConfiguration.java | 46 ++++ src/main/resources/bidder-config/seedtag.yaml | 17 ++ .../static/bidder-params/seedtag.json | 16 ++ .../bidder/seedtag/SeedtagBidderTest.java | 257 ++++++++++++++++++ .../org/prebid/server/it/SeedtagTest.java | 33 +++ .../seedtag/test-auction-seedtag-request.json | 23 ++ .../test-auction-seedtag-response.json | 44 +++ .../seedtag/test-seedtag-bid-request.json | 56 ++++ .../seedtag/test-seedtag-bid-response.json | 21 ++ .../server/it/test-application.properties | 2 + 12 files changed, 679 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/seedtag/SeedtagBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/seedtag/ExtImpSeedtag.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/SeedtagConfiguration.java create mode 100644 src/main/resources/bidder-config/seedtag.yaml create mode 100644 src/main/resources/static/bidder-params/seedtag.json create mode 100644 src/test/java/org/prebid/server/bidder/seedtag/SeedtagBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/SeedtagTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-auction-seedtag-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-auction-seedtag-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-seedtag-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-seedtag-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/seedtag/SeedtagBidder.java b/src/main/java/org/prebid/server/bidder/seedtag/SeedtagBidder.java new file mode 100644 index 00000000000..399437fb636 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/seedtag/SeedtagBidder.java @@ -0,0 +1,152 @@ +package org.prebid.server.bidder.seedtag; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +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.request.seedtag.ExtImpSeedtag; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class SeedtagBidder implements Bidder { + + private static final TypeReference> SEEDTAG_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String BIDDER_CURRENCY = "USD"; + + private final String endpointUrl; + private final JacksonMapper mapper; + private final CurrencyConversionService currencyConversionService; + + public SeedtagBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final Price bidFloorPrice = resolveBidFloor(imp, request); + + modifiedImps.add(modifyImp(imp, bidFloorPrice)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (modifiedImps.size() < 1) { + return Result.withErrors(errors); + } + + final BidRequest modifiedBidRequest = request.toBuilder() + .imp(modifiedImps) + .build(); + return Result.of( + Collections.singletonList(BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper)), + errors); + } + + private static Imp modifyImp(Imp imp, Price bidFloorPrice) { + return imp.toBuilder() + .bidfloorcur(bidFloorPrice.getCurrency()) + .bidfloor(bidFloorPrice.getValue()) + .build(); + } + + private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, BIDDER_CURRENCY) + ? convertBidFloor(initialBidFloorPrice, imp.getId(), bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency( + bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + BIDDER_CURRENCY); + + return Price.of(BIDDER_CURRENCY, convertedPrice); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + final List bidderBids = extractBids(bidResponse, errors); + return Result.of(bidderBids, errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, List errors) { + final BidType bidType; + try { + bidType = getBidType(bid); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + return BidderBid.of(bid, bidType, BIDDER_CURRENCY); + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + default -> throw new PreBidException("Invalid bid.mtype for bid.id: '%s'".formatted(bid.getId())); + }; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedtag/ExtImpSeedtag.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedtag/ExtImpSeedtag.java new file mode 100644 index 00000000000..364b63fb7dc --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedtag/ExtImpSeedtag.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.seedtag; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpSeedtag { + + @JsonProperty("adUnitId") + String adUnitId; + +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SeedtagConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SeedtagConfiguration.java new file mode 100644 index 00000000000..08d5c527d62 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/SeedtagConfiguration.java @@ -0,0 +1,46 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.seedtag.SeedtagBidder; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/seedtag.yaml", factory = YamlPropertySourceFactory.class) +public class SeedtagConfiguration { + + private static final String BIDDER_NAME = "seedtag"; + + @Bean("seedtagConfigurationProperties") + @ConfigurationProperties("adapters.seedtag") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps seedtagBidderDeps(BidderConfigurationProperties seedtagConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(seedtagConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new SeedtagBidder( + config.getEndpoint(), + currencyConversionService, + mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/seedtag.yaml b/src/main/resources/bidder-config/seedtag.yaml new file mode 100644 index 00000000000..6012eb5a2de --- /dev/null +++ b/src/main/resources/bidder-config/seedtag.yaml @@ -0,0 +1,17 @@ +adapters: + seedtag: + endpoint: "https://s.seedtag.com/c/openrtb?partner=prebidserver" + endpoint-compression: gzip + meta-info: + maintainer-email: prebid@seedtag.com + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 157 + usersync: + cookie-family-name: seedtag + iframe: + url: https://s.seedtag.com/cs/cookiesync/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&usp_consent={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/static/bidder-params/seedtag.json b/src/main/resources/static/bidder-params/seedtag.json new file mode 100644 index 00000000000..8d84b059fd0 --- /dev/null +++ b/src/main/resources/static/bidder-params/seedtag.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Seedtag Adapter Params", + "description": "A schema which validates params accepted by the Seedtag adapter", + "type": "object", + "properties": { + "adUnitId": { + "type": "string", + "description": "Ad Unit ID", + "minLength": 1 + } + }, + "required": [ + "adUnitId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/seedtag/SeedtagBidderTest.java b/src/test/java/org/prebid/server/bidder/seedtag/SeedtagBidderTest.java new file mode 100644 index 00000000000..722ea6be2a6 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/seedtag/SeedtagBidderTest.java @@ -0,0 +1,257 @@ +package org.prebid.server.bidder.seedtag; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +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.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; + +import java.math.BigDecimal; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +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.anyString; +import static org.mockito.BDDMockito.given; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; + +@ExtendWith(MockitoExtension.class) +public class SeedtagBidderTest extends VertxTest { + + public static final String ENDPOINT_URL = "https://test.endpoint.com"; + + @Mock + private CurrencyConversionService currencyConversionService; + + private SeedtagBidder target; + + @BeforeEach + public void setUp() { + target = new SeedtagBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new SeedtagBidder( + "invalid_url", + currencyConversionService, + jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestWithAllImps() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + requestBuilder -> requestBuilder.imp(asList( + givenImp(identity()), + givenImp(identity())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .hasSize(2); + } + + @Test + public void makeHttpRequestsShouldConvertCurrency() { + // given + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willReturn(BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest(imp -> imp + .bidfloor(BigDecimal.TWO) + .bidfloorcur("EUR")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsExactly(tuple(BigDecimal.TEN, "USD")); + } + + @Test + public void makeHttpRequestsShouldSkipImpsWithCurrencyThatCanNotBeConverted() { + // given + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willThrow(PreBidException.class); + + final BidRequest bidRequest = givenBidRequest( + identity(), + requestBuilder -> requestBuilder.imp(asList( + givenImp(identity()), + givenImp(impBuilder -> impBuilder + .bidfloor(BigDecimal.TEN) + .bidfloorcur("EUR"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .hasSize(1); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(BidResponse.builder().build()); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidIfMediaTypeBanner() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(1))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(1).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfMediaTypeVideo() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(2))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(2).build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorIfMediaTypeInvalid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(4).id("456"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Invalid bid.mtype for bid.id: '456'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest givenBidRequest( + UnaryOperator impCustomizer, + UnaryOperator requestCustomizer) { + return requestCustomizer.apply(BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return givenBidRequest(impCustomizer, identity()); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("123")).build(); + } + + private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static BidderCall givenHttpCall(String responseBody) { + return BidderCall.succeededHttp(null, HttpResponse.of(200, null, responseBody), null); + } + + private static BidderCall givenHttpCall(BidResponse bidResponse) throws JsonProcessingException { + return BidderCall.succeededHttp( + null, + HttpResponse.of(200, null, mapper.writeValueAsString(bidResponse)), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/SeedtagTest.java b/src/test/java/org/prebid/server/it/SeedtagTest.java new file mode 100644 index 00000000000..318f2bb506d --- /dev/null +++ b/src/test/java/org/prebid/server/it/SeedtagTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class SeedtagTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromSeedtag() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/seedtag-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/seedtag/test-seedtag-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/seedtag/test-seedtag-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/seedtag/test-auction-seedtag-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/seedtag/test-auction-seedtag-response.json", response, + singletonList("seedtag")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-auction-seedtag-request.json b/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-auction-seedtag-request.json new file mode 100644 index 00000000000..410829c8448 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-auction-seedtag-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "seedtag": { + "adUnitId": "someAdUnitId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-auction-seedtag-response.json b/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-auction-seedtag-response.json new file mode 100644 index 00000000000..d95fbe5ae64 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-auction-seedtag-response.json @@ -0,0 +1,44 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "mtype": 1, + "ext":{ + "origbidcpm":3.33, + "origbidcur":"USD", + "prebid": { + "type":"banner", + "meta": { + "adaptercode": "seedtag" + } + } + } + } + ], + "seat": "seedtag", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "seedtag": "{{ seedtag.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-seedtag-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-seedtag-bid-request.json new file mode 100644 index 00000000000..cd9e1a11235 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-seedtag-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "adUnitId": "someAdUnitId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-seedtag-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-seedtag-bid-response.json new file mode 100644 index 00000000000..1992d34c094 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/seedtag/test-seedtag-bid-response.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300, + "mtype": 1 + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index bf0af8f2c13..a88d3f10216 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -439,6 +439,8 @@ adapters.seedingAlliance.aliases.suntContent.enabled=true adapters.seedingAlliance.aliases.suntContent.endpoint=http://localhost:8090/suntContent-exchange?ssp={{AccountId}} adapters.seedingAlliance.aliases.finative.enabled=true adapters.seedingAlliance.aliases.finative.endpoint=http://localhost:8090/finative-exchange?ssp={{AccountId}} +adapters.seedtag.enabled=true +adapters.seedtag.endpoint=http://localhost:8090/seedtag-exchange adapters.smaato.enabled=true adapters.smaato.endpoint=http://localhost:8090/smaato-exchange adapters.smartadserver.enabled=true From 2284af7752cdfc7608495748fa6073e3993843ca Mon Sep 17 00:00:00 2001 From: Andrea Castello Date: Fri, 16 May 2025 17:39:10 +0200 Subject: [PATCH 3/7] Core: Support bidder specific device data (#3922) --- .../server/auction/ExchangeService.java | 26 +++++-- .../prebid/server/auction/FpdResolver.java | 8 +- .../PrivacyEnforcementService.java | 10 ++- .../ext/request/ExtBidderConfigOrtb.java | 5 ++ .../server/auction/ExchangeServiceTest.java | 60 +++++++++++--- .../server/auction/FpdResolverTest.java | 78 +++++++++++++++++++ .../PrivacyEnforcementServiceTest.java | 8 +- .../prebid/server/json/JsonMergerTest.java | 16 +++- 8 files changed, 183 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 4ae191bcaad..9a1326b4837 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -5,6 +5,7 @@ import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Content; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Imp; @@ -97,6 +98,7 @@ import org.prebid.server.util.ListUtil; import org.prebid.server.util.PbsUtil; import org.prebid.server.util.StreamUtil; +import org.apache.commons.lang3.tuple.Pair; import java.math.BigDecimal; import java.time.Clock; @@ -502,10 +504,10 @@ private Future> makeAuctionParticipation( final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); final Map biddersToConfigs = getBiddersToConfigs(prebid); final Map> eidPermissions = getEidPermissions(prebid); - final Map bidderToUser = - prepareUsers(bidders, context, aliases, biddersToConfigs, eidPermissions); + final Map> bidderToUserAndDevice = + prepareUsersAndDevices(bidders, context, aliases, biddersToConfigs, eidPermissions); - return privacyEnforcementService.mask(context, bidderToUser, aliases) + return privacyEnforcementService.mask(context, bidderToUserAndDevice, aliases) .map(bidderToPrivacyResult -> getAuctionParticipation( bidderToPrivacyResult, bidRequest, @@ -557,7 +559,7 @@ private static List firstPartyDataBidders(ExtRequest requestExt) { return data == null ? null : data.getBidders(); } - private Map prepareUsers(List bidders, + private Map> prepareUsersAndDevices(List bidders, AuctionContext context, BidderAliases aliases, Map biddersToConfigs, @@ -566,7 +568,7 @@ private Map prepareUsers(List bidders, final BidRequest bidRequest = context.getBidRequest(); final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); - final Map bidderToUser = new HashMap<>(); + final Map> bidderToUserAndDevice = new HashMap<>(); for (String bidder : bidders) { final ExtBidderConfigOrtb fpdConfig = ObjectUtils.defaultIfNull(biddersToConfigs.get(bidder), biddersToConfigs.get(ALL_BIDDERS_CONFIG)); @@ -574,9 +576,11 @@ private Map prepareUsers(List bidders, .anyMatch(fpdBidder -> StringUtils.equalsIgnoreCase(fpdBidder, bidder)); final User preparedUser = prepareUser( bidder, context, aliases, useFirstPartyData, fpdConfig, eidPermissions); - bidderToUser.put(bidder, preparedUser); + final Device preparedDevice = prepareDevice( + bidRequest.getDevice(), fpdConfig, useFirstPartyData); + bidderToUserAndDevice.put(bidder, Pair.of(preparedUser, preparedDevice)); } - return bidderToUser; + return bidderToUserAndDevice; } private User prepareUser(String bidder, @@ -813,7 +817,6 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, final boolean isApp = preparedApp != null; final boolean isDooh = !isApp && preparedDooh != null; final boolean isSite = !isApp && !isDooh && preparedSite != null; - final List preparedImps = prepareImps( bidder, bidRequest, @@ -945,6 +948,13 @@ private App prepareApp(App app, ObjectNode fpdApp, boolean useFirstPartyData) { return useFirstPartyData ? fpdResolver.resolveApp(maskedApp, fpdApp) : maskedApp; } + private Device prepareDevice(Device device, ExtBidderConfigOrtb fpdConfig, boolean useFirstPartyData) { + if (fpdConfig == null) { + return device; + } + return useFirstPartyData ? fpdResolver.resolveDevice(device, fpdConfig.getDevice()) : device; + } + private static ExtApp maskExtApp(ExtApp appExt) { final ExtApp maskedExtApp = ExtApp.of(appExt.getPrebid(), null); return maskedExtApp.isEmpty() ? null : maskedExtApp; diff --git a/src/main/java/org/prebid/server/auction/FpdResolver.java b/src/main/java/org/prebid/server/auction/FpdResolver.java index f0ade099ece..5e91e38a2de 100644 --- a/src/main/java/org/prebid/server/auction/FpdResolver.java +++ b/src/main/java/org/prebid/server/auction/FpdResolver.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.User; @@ -23,7 +24,8 @@ public class FpdResolver { private static final String BIDDERS = "bidders"; private static final String APP = "app"; private static final String DOOH = "dooh"; - private static final Set KNOWN_FPD_ATTRIBUTES = Set.of(USER, SITE, APP, DOOH, BIDDERS); + private static final String DEVICE = "device"; + private static final Set KNOWN_FPD_ATTRIBUTES = Set.of(USER, SITE, APP, DOOH, DEVICE, BIDDERS); private static final String CONTEXT = "context"; private static final String DATA = "data"; @@ -51,6 +53,10 @@ public Dooh resolveDooh(Dooh originDooh, ObjectNode fpdDooh) { return mergeFpd(originDooh, fpdDooh, Dooh.class); } + public Device resolveDevice(Device originDevice, ObjectNode fpdDevice) { + return mergeFpd(originDevice, fpdDevice, Device.class); + } + private T mergeFpd(T original, ObjectNode fpd, Class tClass) { if (fpd == null || fpd.isNull() || fpd.isMissingNode()) { return original; diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java index 9d18300197b..b14ff44444c 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java @@ -1,10 +1,12 @@ package org.prebid.server.auction.privacy.enforcement; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.User; import io.vertx.core.Future; import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; +import org.apache.commons.lang3.tuple.Pair; import java.util.List; import java.util.Map; @@ -22,14 +24,14 @@ public PrivacyEnforcementService(final List enforcements) { } public Future> mask(AuctionContext auctionContext, - Map bidderToUser, + Map> bidderToUserAndDevice, BidderAliases aliases) { - final List initialResults = bidderToUser.entrySet().stream() + final List initialResults = bidderToUserAndDevice.entrySet().stream() .map(entry -> BidderPrivacyResult.builder() .requestBidder(entry.getKey()) - .user(entry.getValue()) - .device(auctionContext.getBidRequest().getDevice()) + .user(entry.getValue().getLeft()) + .device(entry.getValue().getRight()) .build()) .toList(); diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java index d8dcf51e4aa..83203fb60cd 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java @@ -25,4 +25,9 @@ public class ExtBidderConfigOrtb { * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2.user */ ObjectNode user; + + /** + * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2.device + */ + ObjectNode device; } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 25ee4657df9..c13ca6eb56f 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -322,10 +322,12 @@ public void setUp() { given(privacyEnforcementService.mask(any(), argThat(MapUtils::isNotEmpty), any())) .willAnswer(inv -> - Future.succeededFuture(((Map) inv.getArgument(1)).entrySet().stream() + Future.succeededFuture(((Map>) inv.getArgument(1)).entrySet() + .stream() .map(bidderAndUser -> BidderPrivacyResult.builder() .requestBidder(bidderAndUser.getKey()) - .user(bidderAndUser.getValue()) + .user(bidderAndUser.getValue().getLeft()) + .device(bidderAndUser.getValue().getRight()) .build()) .toList())); @@ -2691,12 +2693,12 @@ public void shouldUseConcreteOverGeneralSiteWithExtPrebidBidderConfigIgnoringCas final ObjectNode siteWithPage = mapper.valueToTree(Site.builder().page("testPage").build()); final ExtBidderConfig extBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(siteWithPage, null, null, null)); + ExtBidderConfigOrtb.of(siteWithPage, null, null, null, null)); final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( singletonList("SoMeBiDdEr"), extBidderConfig); final ObjectNode siteWithDomain = mapper.valueToTree(Site.builder().domain("notUsed").build()); final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(siteWithDomain, null, null, null)); + ExtBidderConfigOrtb.of(siteWithDomain, null, null, null, null)); final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"), allExtBidderConfig); @@ -2738,12 +2740,12 @@ public void shouldUseConcreteOverGeneralDoohWithExtPrebidBidderConfig() { final ObjectNode doohWithVenueType = mapper.valueToTree(Dooh.builder().venuetype(List.of("venuetype")).build()); final ExtBidderConfig extBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, null, doohWithVenueType, null)); + ExtBidderConfigOrtb.of(null, null, doohWithVenueType, null, null)); final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( singletonList("someBidder"), extBidderConfig); final ObjectNode doohWithDomain = mapper.valueToTree(Dooh.builder().domain("notUsed").build()); final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, null, doohWithDomain, null)); + ExtBidderConfigOrtb.of(null, null, doohWithDomain, null, null)); final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of( singletonList("*"), allExtBidderConfig); @@ -2787,7 +2789,7 @@ public void shouldUseConcreteOverGeneralAppWithExtPrebidBidderConfigIgnoringCase final Publisher publisherWithId = Publisher.builder().id("testId").build(); final ObjectNode appWithPublisherId = mapper.valueToTree(App.builder().publisher(publisherWithId).build()); final ExtBidderConfig extBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, appWithPublisherId, null, null)); + ExtBidderConfigOrtb.of(null, appWithPublisherId, null, null, null)); final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( singletonList("SoMeBiDdEr"), extBidderConfig); @@ -2795,7 +2797,7 @@ public void shouldUseConcreteOverGeneralAppWithExtPrebidBidderConfigIgnoringCase final ObjectNode appWithUpdatedPublisher = mapper.valueToTree( App.builder().publisher(publisherWithIdAndDomain).build()); final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, appWithUpdatedPublisher, null, null)); + ExtBidderConfigOrtb.of(null, appWithUpdatedPublisher, null, null, null)); final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"), allExtBidderConfig); @@ -2834,13 +2836,13 @@ public void shouldUseConcreteOverGeneralUserWithExtPrebidBidderConfig() { givenBidder("someBidder", bidder, givenEmptySeatBid()); final ObjectNode bidderConfigUser = mapper.valueToTree(User.builder().id("userFromConfig").build()); final ExtBidderConfig extBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, null, null, bidderConfigUser)); + ExtBidderConfigOrtb.of(null, null, null, bidderConfigUser, null)); final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( singletonList("SomMeBiDdEr"), extBidderConfig); final ObjectNode emptyUser = mapper.valueToTree(User.builder().build()); final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, null, null, emptyUser)); + ExtBidderConfigOrtb.of(null, null, null, emptyUser, null)); final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"), allExtBidderConfig); final User requestUser = User.builder().id("erased").buyeruid("testBuyerId").build(); @@ -2870,6 +2872,44 @@ public void shouldUseConcreteOverGeneralUserWithExtPrebidBidderConfig() { .containsOnly(mergedUser); } + @Test + public void shouldUseBidderSpecificDeviceDataInBidderRequest() { + // given + final Bidder bidder = mock(Bidder.class); + givenBidder("someBidder", bidder, givenEmptySeatBid()); + + final ObjectNode deviceWithMakeAndModel = mapper.valueToTree( + Device.builder().make("TestMake_001").model("TestModel_001").build()); + final ExtBidderConfig extBidderConfig = ExtBidderConfig.of( + ExtBidderConfigOrtb.of(null, null, null, null, deviceWithMakeAndModel)); + final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( + singletonList("someBidder"), extBidderConfig); + final Device requestDevice = Device.builder().build(); + final ExtRequestPrebid extRequestPrebid = ExtRequestPrebid.builder() + .bidderconfig(singletonList(concreteFpdConfig)) + .build(); + final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), + builder -> builder.device(requestDevice).ext(ExtRequest.of(extRequestPrebid))); + final Device mergedDevice = Device.builder() + .make("TestMake_001").model("TestModel_001").build(); + + given(fpdResolver.resolveDevice(any(), any())).willReturn(mergedDevice); + + // when + target.holdAuction(givenRequestContext(bidRequest)); + + // then + final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class); + verify(httpBidderRequester) + .requestBids(any(), bidderRequestCaptor.capture(), any(), any(), any(), any(), anyBoolean()); + final List capturedBidRequests = bidderRequestCaptor.getAllValues(); + + assertThat(capturedBidRequests) + .extracting(BidderRequest::getBidRequest) + .extracting(BidRequest::getDevice) + .containsOnly(mergedDevice); + } + @Test public void shouldAddBuyeridToUserFromRequest() { // given diff --git a/src/test/java/org/prebid/server/auction/FpdResolverTest.java b/src/test/java/org/prebid/server/auction/FpdResolverTest.java index d3b7530ab84..1e094a120d6 100644 --- a/src/test/java/org/prebid/server/auction/FpdResolverTest.java +++ b/src/test/java/org/prebid/server/auction/FpdResolverTest.java @@ -4,6 +4,7 @@ import com.iab.openrtb.request.App; import com.iab.openrtb.request.Content; import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.Publisher; @@ -17,6 +18,9 @@ import org.prebid.server.json.JsonMerger; import org.prebid.server.proto.openrtb.ext.request.ExtApp; import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; +import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; import org.prebid.server.proto.openrtb.ext.request.ExtDooh; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -134,6 +138,80 @@ public void resolveUserShouldReturnCopyOfUserExtDataIfFPDUserExtDataIsMissing() assertThat(resultUser.getExt().getData().equals(originExtUserData)).isTrue(); // but the same by value } + @Test + public void resolveDeviceShouldOverrideFpdFieldsFromFpdDevice() { + // given + final Device originDevice = Device.builder() + .devicetype(1) + .make("original_make") + .model("original_model") + .os("original_os") + .osv("original_osv") + .hwv("original_hwv") + .language("original_language") + .h(1111) + .js(1) + .ip("original_ip") + .build(); + + final Device fpdDevice = Device.builder() + .devicetype(2) + .make("fpd_make") + .model("fpd_model") + .os("fpd_os") + .osv("fpd_osv") + .hwv("fpd_hwv") + .ip("new_ip") + .build(); + + // when + final Device resultDevice = target.resolveDevice(originDevice, mapper.valueToTree(fpdDevice)); + + // then + assertThat(resultDevice).isEqualTo(Device.builder() + .devicetype(2) + .make("fpd_make") + .model("fpd_model") + .os("fpd_os") + .osv("fpd_osv") + .hwv("fpd_hwv") + .language("original_language") + .h(1111) + .js(1) + .ip("new_ip") + .build()); + } + + @Test + public void resolveDeviceShouldReturnOriginDeviceIfFpdDeviceIsNull() { + assertThat(target.resolveDevice(Device.builder().make("test_make").build(), null)) + .isEqualTo(Device.builder().make("test_make").build()); + } + + @Test + public void resolveDeviceShouldReturnFpdDeviceIfOriginDeviceIsNull() { + assertThat(target.resolveDevice(null, mapper.valueToTree(Device.builder().model("test_model").build()))) + .isEqualTo(Device.builder().model("test_model").build()); + } + + @Test + public void resolveDeviceShouldNotChangeOriginExtDataIfFPDDoesNotHaveExt() { + // given + final Device originDevice = Device.builder() + .ext(ExtDevice.of(1, ExtDevicePrebid.of(ExtDeviceInt.of(10, 20)))) + .build(); + + final Device fpdDevice = Device.builder().build(); + + // when + final Device resultDevice = target.resolveDevice(originDevice, mapper.valueToTree(fpdDevice)); + + // then + assertThat(resultDevice).isEqualTo(Device.builder() + .ext(ExtDevice.of(1, ExtDevicePrebid.of(ExtDeviceInt.of(10, 20)))) + .build()); + } + @Test public void resolveAppShouldOverrideFpdFieldsFromFpdApp() { // given diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java index 906952bc9b3..187985e4149 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java @@ -11,6 +11,7 @@ import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; +import org.apache.commons.lang3.tuple.Pair; import java.util.List; import java.util.Map; @@ -55,10 +56,13 @@ public void maskShouldPassBidderPrivacyThroughAllEnforcements() { .build(); final User user = User.builder().id("originalUser").build(); - final Map bidderToUser = singletonMap("bidder", user); + final Device device = Device.builder().build(); + final Pair userAndDevice = Pair.of(user, device); + final Map> bidderToUserAndDevice = singletonMap("bidder", userAndDevice); // when - final Future> result = target.mask(auctionContext, bidderToUser, bidderAliases); + final Future> result = target.mask( + auctionContext, bidderToUserAndDevice, bidderAliases); // then assertThat(result) diff --git a/src/test/java/org/prebid/server/json/JsonMergerTest.java b/src/test/java/org/prebid/server/json/JsonMergerTest.java index 48957f12511..33551d129a8 100644 --- a/src/test/java/org/prebid/server/json/JsonMergerTest.java +++ b/src/test/java/org/prebid/server/json/JsonMergerTest.java @@ -34,6 +34,7 @@ public void mergeShouldReturnMergedObject() { siteWithPage, appWithPublisherId, doohWithVenueType, + null, null); final ObjectNode siteWithDomain = mapper.valueToTree(Site.builder().domain("testDomain").build()); @@ -41,8 +42,12 @@ public void mergeShouldReturnMergedObject() { final ObjectNode appWithUpdatedPublisher = mapper.valueToTree(App.builder() .publisher(publisherWithIdAndDomain).build()); final ObjectNode doohWithVenueTypeTax = mapper.valueToTree(Dooh.builder().venuetypetax(3).build()); - final ExtBidderConfigOrtb secondBidderConfigFpd = - ExtBidderConfigOrtb.of(siteWithDomain, appWithUpdatedPublisher, doohWithVenueTypeTax, null); + final ExtBidderConfigOrtb secondBidderConfigFpd = ExtBidderConfigOrtb.of( + siteWithDomain, + appWithUpdatedPublisher, + doohWithVenueTypeTax, + null, + null); // when final ExtBidderConfigOrtb result = target.merge( @@ -56,7 +61,12 @@ public void mergeShouldReturnMergedObject() { final ObjectNode mergedApp = mapper.valueToTree(App.builder().publisher(mergedPublisher).build()); final ObjectNode mergedDooh = mapper.valueToTree( Dooh.builder().venuetype(List.of("venuetype")).venuetypetax(3).build()); - final ExtBidderConfigOrtb mergedConfigFpd = ExtBidderConfigOrtb.of(mergedSite, mergedApp, mergedDooh, null); + final ExtBidderConfigOrtb mergedConfigFpd = ExtBidderConfigOrtb.of( + mergedSite, + mergedApp, + mergedDooh, + null, + null); assertThat(result).isEqualTo(mergedConfigFpd); } From ceb96dd17015c37c72d03a4692eb41429080ba83 Mon Sep 17 00:00:00 2001 From: Markiyan Mykush <95693607+marki1an@users.noreply.github.com> Date: Fri, 16 May 2025 18:39:21 +0300 Subject: [PATCH 4/7] Test: Rename `ModelGroup` (#3949) --- ...delGroup.groovy => FloorModelGroup.groovy} | 6 ++--- .../model/pricefloors/PriceFloorData.groovy | 4 ++-- .../PriceFloorsFetchingSpec.groovy | 22 +++++++++---------- .../pricefloors/PriceFloorsRulesSpec.groovy | 4 ++-- .../PriceFloorsSignalingSpec.groovy | 4 ++-- 5 files changed, 20 insertions(+), 20 deletions(-) rename src/test/groovy/org/prebid/server/functional/model/pricefloors/{ModelGroup.groovy => FloorModelGroup.groovy} (91%) diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/ModelGroup.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/FloorModelGroup.groovy similarity index 91% rename from src/test/groovy/org/prebid/server/functional/model/pricefloors/ModelGroup.groovy rename to src/test/groovy/org/prebid/server/functional/model/pricefloors/FloorModelGroup.groovy index 24b57f57bba..14ffa92bcb0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/ModelGroup.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/FloorModelGroup.groovy @@ -9,7 +9,7 @@ import org.prebid.server.functional.util.PBSUtils @EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) -class ModelGroup { +class FloorModelGroup { Currency currency Integer skipRate @@ -21,8 +21,8 @@ class ModelGroup { BigDecimal defaultFloor List noFloorSignalBidders - static ModelGroup getModelGroup() { - new ModelGroup( + static FloorModelGroup getModelGroup() { + new FloorModelGroup( currency: Currency.USD, schema: PriceFloorSchema.priceFloorSchema, values: [(new Rule(mediaType: MediaType.MULTIPLE, country: Country.MULTIPLE) diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy index a9d913eccd1..1c87c85b794 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy @@ -19,13 +19,13 @@ class PriceFloorData implements ResponseModel { Integer useFetchDataRate String floorsSchemaVersion Integer modelTimestamp - List modelGroups + List modelGroups List noFloorSignalBidders static PriceFloorData getPriceFloorData() { new PriceFloorData(floorProvider: PBSUtils.randomString, currency: USD, floorsSchemaVersion: 2, - modelGroups: [ModelGroup.modelGroup]) + modelGroups: [FloorModelGroup.modelGroup]) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy index a897fbeff7c..c5ce101b156 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy @@ -2,7 +2,7 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.config.PriceFloorsFetch import org.prebid.server.functional.model.db.StoredRequest -import org.prebid.server.functional.model.pricefloors.ModelGroup +import org.prebid.server.functional.model.pricefloors.FloorModelGroup import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.Rule import org.prebid.server.functional.model.request.amp.AmpRequest @@ -1352,7 +1352,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorValue = PBSUtils.randomFloorValue def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups.first().modelWeight = invalidModelWeight ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] @@ -1389,7 +1389,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorValue = PBSUtils.randomFloorValue def ampStoredRequest = storedRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups.first().modelWeight = invalidModelWeight ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] @@ -1426,7 +1426,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorValue = PBSUtils.randomFloorValue def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups[0].skipRate = 0 ext.prebid.floors.data.skipRate = 0 @@ -1466,7 +1466,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorValue = PBSUtils.randomFloorValue def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups[0].skipRate = 0 ext.prebid.floors.data.skipRate = invalidSkipRate @@ -1506,7 +1506,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorValue = PBSUtils.randomFloorValue def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups[0].skipRate = invalidSkipRate ext.prebid.floors.data.skipRate = 0 @@ -1547,7 +1547,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def invalidDefaultFloorValue = DEFAULT_FLOOR_VALUE_MIN - 1 def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups[0].defaultFloor = invalidDefaultFloorValue ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] @@ -1642,7 +1642,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups.first().modelWeight = invalidModelWeight modelGroups.last().values = [(rule): floorValue] @@ -1700,7 +1700,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups[0].skipRate = 0 skipRate = invalidSkipRate @@ -1759,7 +1759,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups[0].skipRate = invalidSkipRate skipRate = 0 @@ -1819,7 +1819,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorValue = PBSUtils.randomFloorValue def invalidDefaultFloor = DEFAULT_FLOOR_VALUE_MIN - 1 def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups[0].defaultFloor = invalidDefaultFloor modelGroups.last().values = [(rule): floorValue] diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy index ee861a24587..5d985596679 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy @@ -9,7 +9,7 @@ import org.prebid.server.functional.model.config.BidderConfig import org.prebid.server.functional.model.db.StoredImp import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.pricefloors.MediaType -import org.prebid.server.functional.model.pricefloors.ModelGroup +import org.prebid.server.functional.model.pricefloors.FloorModelGroup import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.PriceFloorSchema import org.prebid.server.functional.model.pricefloors.Rule @@ -191,7 +191,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups[0].schema = new PriceFloorSchema(fields: [BOGUS]) modelGroups[0].values = [(new Rule(domain: domain).rule): floorValue + 0.1] modelGroups[1].schema = new PriceFloorSchema(fields: [DOMAIN]) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy index 72be2f0a0f3..93f26180213 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy @@ -2,7 +2,7 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.pricefloors.Country -import org.prebid.server.functional.model.pricefloors.ModelGroup +import org.prebid.server.functional.model.pricefloors.FloorModelGroup import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.PriceFloorSchema import org.prebid.server.functional.model.pricefloors.Rule @@ -473,7 +473,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups.last().schema = new PriceFloorSchema(fields: [SITE_DOMAIN]) modelGroups.last().values = [(new Rule(siteDomain: domain).rule): floorValue] From c2f5d88452dcfaa80928fb567b898370c76ecb71 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Fri, 16 May 2025 17:39:32 +0200 Subject: [PATCH 5/7] Port TheTradeDesk: Dynamically construct endpoint using supplySourceId (#3951) --- .../thetradedesk/TheTradeDeskBidder.java | 22 ++++---- .../thetradedesk/ExtImpTheTradeDesk.java | 3 ++ .../static/bidder-params/thetradedesk.json | 10 ++-- .../thetradedesk/TheTradeDeskBidderTest.java | 50 ++++++++++++++++--- .../test-auction-thetradedesk-request.json | 3 +- .../test-thetradedesk-bid-request.json | 3 +- .../server/it/test-application.properties | 1 - 7 files changed, 70 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java index 94913a36213..99f5eda1ec2 100644 --- a/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java +++ b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java @@ -11,8 +11,8 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.vertx.core.MultiMap; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -42,11 +42,6 @@ public class TheTradeDeskBidder implements Bidder { new TypeReference<>() { }; - private static final String PREBID_INTEGRATION_TYPE_HEADER = "x-integration-type"; - private static final String PREBID_INTEGRATION_TYPE = "1"; - private static final MultiMap HEADERS = HttpUtil.headers() - .add(PREBID_INTEGRATION_TYPE_HEADER, PREBID_INTEGRATION_TYPE); - private static final String SUPPLY_ID_MACRO = "{{SupplyId}}"; private static final Pattern SUPPLY_ID_PATTERN = Pattern.compile("([a-z]+)$"); @@ -73,6 +68,7 @@ public Result>> makeHttpRequests(BidRequest request final List modifiedImps = new ArrayList<>(); String publisherId = null; + String sourceSupplyId = null; for (Imp imp : request.getImp()) { try { final ExtImpTheTradeDesk extImp = parseImpExt(imp); @@ -82,6 +78,11 @@ public Result>> makeHttpRequests(BidRequest request ? extImpPublisherId : publisherId; + final String extImpSourceSupplyId = extImp.getSupplySourceId(); + sourceSupplyId = sourceSupplyId == null && StringUtils.isNotBlank(extImpSourceSupplyId) + ? extImpSourceSupplyId + : sourceSupplyId; + modifiedImps.add(modifyImp(imp)); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); @@ -91,8 +92,7 @@ public Result>> makeHttpRequests(BidRequest request final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, publisherId); final HttpRequest httpRequest = BidderUtil.defaultRequest( outgoingRequest, - HEADERS, - resolveEndpoint(), + resolveEndpoint(sourceSupplyId), mapper); return Result.withValue(httpRequest); @@ -165,8 +165,10 @@ private static App modifyApp(BidRequest request, String publisherId) { .build(); } - private String resolveEndpoint() { - return endpointUrl.replace(SUPPLY_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(supplyId))); + private String resolveEndpoint(String sourceSupplyId) { + return endpointUrl.replace( + SUPPLY_ID_MACRO, + HttpUtil.encodeUrl(StringUtils.defaultString(ObjectUtils.defaultIfNull(sourceSupplyId, supplyId)))); } @Override diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java index ee3b9cc7d83..91a3b8e14b2 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java @@ -8,4 +8,7 @@ public class ExtImpTheTradeDesk { @JsonProperty("publisherId") String publisherId; + + @JsonProperty("supplySourceId") + String supplySourceId; } diff --git a/src/main/resources/static/bidder-params/thetradedesk.json b/src/main/resources/static/bidder-params/thetradedesk.json index d0b305a5a1e..566e7e556e6 100644 --- a/src/main/resources/static/bidder-params/thetradedesk.json +++ b/src/main/resources/static/bidder-params/thetradedesk.json @@ -6,10 +6,14 @@ "properties": { "publisherId": { "type": "string", + "minLength": 1, "description": "An ID which identifies the publisher" + }, + "supplySourceId": { + "type":"string", + "minLength": 1, + "description": "An ID provided by TheTradeDesk used to determine which endpoint to use" } }, - "required": [ - "publisherId" - ] + "required": ["publisherId"] } diff --git a/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java b/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java index 4fee1810ad3..021bca5617a 100644 --- a/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java @@ -95,14 +95,12 @@ public void makeHttpRequestsShouldReturnExpectedHeaders() { .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) - .isEqualTo(APPLICATION_JSON_VALUE)) - .satisfies(headers -> assertThat(headers.get("x-integration-type")) - .isEqualTo("1")); + .isEqualTo(APPLICATION_JSON_VALUE)); assertThat(result.getErrors()).isEmpty(); } @Test - public void makeHttpRequestsShouldUseCorrectUri() { + public void makeHttpRequestsShouldUseConfiguredSupplyIdWhenImpExtSupplyIdIsNotProvided() { // given final BidRequest bidRequest = givenBidRequest(identity(), identity()); @@ -116,6 +114,42 @@ public void makeHttpRequestsShouldUseCorrectUri() { .containsExactly("https://test.endpoint.com/supplyid"); } + @Test + public void makeHttpRequestsShouldUseImpExtSupplyIdWhenProvided() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + imp -> imp.ext(impExt("publisher", "supplySourceId"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/supplySourceId"); + } + + @Test + public void makeHttpRequestsShouldUseFirstFoundSupplySourceId() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + imp -> imp.ext(impExt("publisher", null)), + imp -> imp.ext(impExt("publisher", "supplySourceId1")), + imp -> imp.ext(impExt("publisher", "supplySourceId2"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/supplySourceId1"); + } + @Test public void makeHttpRequestsShouldHaveImpIds() { // given @@ -259,7 +293,7 @@ public void makeHttpRequestsShouldReturnAppWithExtImpPublisherWhenAppWithoutPubl } @Test - public void makeHttpRequestsShouldReturnAppWithPublisherOfTheFirsrExtImp() { + public void makeHttpRequestsShouldReturnAppWithPublisherOfTheFirstExtImp() { final BidRequest bidRequest = givenBidRequest( request -> request.app(App.builder().build()), imp -> imp.ext(impExt("newPublisher")), @@ -453,7 +487,11 @@ private static Imp givenImp(UnaryOperator impCustomizer) { } private static ObjectNode impExt(String publisherId) { - return mapper.valueToTree(ExtPrebid.of(null, ExtImpTheTradeDesk.of(publisherId))); + return impExt(publisherId, null); + } + + private static ObjectNode impExt(String publisherId, String supplySourceId) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpTheTradeDesk.of(publisherId, supplySourceId))); } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json index 2f1bdf270d1..e3fba9cb248 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json @@ -9,7 +9,8 @@ }, "ext": { "thetradedesk": { - "publisherId": "publisherId" + "publisherId": "publisherId", + "supplySourceId": "somesupplyid" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json index fde2176c406..cdcce478087 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json @@ -11,7 +11,8 @@ "ext": { "tid": "${json-unit.any-string}", "bidder": { - "publisherId": "publisherId" + "publisherId": "publisherId", + "supplySourceId": "somesupplyid" } } } diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index a88d3f10216..450ae419cb5 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -507,7 +507,6 @@ adapters.tradplus.enabled=true adapters.tradplus.endpoint=http://{{ZoneID}}localhost:8090/{{AccountID}}/tradplus-exchange adapters.thetradedesk.enabled=true adapters.thetradedesk.endpoint=http://localhost:8090/thetradedesk-exchange/{{SupplyId}} -adapters.thetradedesk.extra-info.supply-id=somesupplyid adapters.triplelift.enabled=true adapters.triplelift.endpoint=http://localhost:8090/triplelift-exchange adapters.tripleliftnative.enabled=true From 21f0923f1814b456e302113979aaac2c0674a990 Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Fri, 16 May 2025 18:39:41 +0300 Subject: [PATCH 6/7] InvibesBidder: fix parameter parsing issue (#3956) --- .../server/bidder/invibes/InvibesBidder.java | 2 +- .../bidder/invibes/InvibesBidderTest.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java index 4b00b970d09..cc4e4bfcaf8 100644 --- a/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java +++ b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java @@ -247,7 +247,7 @@ private static String resolveWidth(Device device) { } private static String resolveHost(Integer domainId) { - if (domainId == 0 || domainId == 1 || domainId == 1001) { + if (domainId == null || domainId == 0 || domainId == 1 || domainId == 1001) { return "bid"; } else if (domainId < 1002) { return "bid" + domainId; diff --git a/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java index abb60ff1edc..5ac2828716c 100644 --- a/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java @@ -85,6 +85,23 @@ public void makeHttpRequestsShouldCreateCorrectURLFor1003Zone() { assertThat(result.getValue().getFirst().getUri()).isEqualTo("https://bid3.videostep.com/bid/"); } + @Test + public void makeHttpRequestsShouldCreateCorrectURLForNullZone() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + impBuilder -> impBuilder.banner(Banner.builder().h(BANNER_H).w(BANNER_W).build()), + ExtImpInvibes.of("12", null, InvibesDebug.of("test", true))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().getFirst().getUri()).isEqualTo("https://bid.videostep.com/bid/"); + } + @Test public void makeHttpRequestsShouldCreateCorrectURLFor0Zone() { // given From 2f1571f633483b78b57ff06426048350bc448a77 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Fri, 16 May 2025 17:40:06 +0200 Subject: [PATCH 7/7] Revert "Core: Pad GPP consent string sections" (#3963) --- .../auction/gpp/model/GppModelWrapper.java | 28 +--- .../privacy/GppSyncUserActivitiesSpec.groovy | 68 ---------- .../GppTransmitEidsActivitiesSpec.groovy | 89 ------------- .../GppTransmitUfpdActivitiesSpec.groovy | 123 +----------------- .../tests/privacy/PrivacyBaseSpec.groovy | 1 - .../gpp/model/GppContextCreatorTest.java | 2 +- .../gpp/model/GppModelWrapperTest.java | 73 +---------- 7 files changed, 9 insertions(+), 375 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/gpp/model/GppModelWrapper.java b/src/main/java/org/prebid/server/auction/gpp/model/GppModelWrapper.java index 1bde141d72f..e8f7d4e008b 100644 --- a/src/main/java/org/prebid/server/auction/gpp/model/GppModelWrapper.java +++ b/src/main/java/org/prebid/server/auction/gpp/model/GppModelWrapper.java @@ -17,33 +17,7 @@ public class GppModelWrapper extends GppModel { private IntObjectMap sectionIdToEncodedString; public GppModelWrapper(String encodedString) throws DecodingException { - super(padSections(encodedString)); - } - - private static String padSections(String gpp) { - final StringBuilder gppBuilder = new StringBuilder(gpp); - - int subsectionStart = 0; - int offset = 0; - for (int i = 1; i < gpp.length(); i++) { - final char currentChar = gpp.charAt(i); - - if (currentChar == '~' || currentChar == '.') { - if ((i - subsectionStart) % 4 != 0 && gpp.charAt(i - 1) != '=') { - gppBuilder.insert(i + offset, "A"); - offset++; - } - - subsectionStart = i + 1; - } - } - - final int lastSubsectionLength = gpp.length() - subsectionStart; - if (lastSubsectionLength > 0 && lastSubsectionLength % 4 != 0 && !gpp.endsWith("=")) { - gppBuilder.append("A"); - } - - return gppBuilder.toString(); + super(encodedString); } private void init() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy index 657e0fef1e5..a6d9d6e7aad 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy @@ -483,39 +483,6 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { ] } - def "PBS cookie sync call when privacy module contain invalid GPP string should exclude bidders URLs"() { - given: "Cookie sync request with link to account" - def accountId = PBSUtils.randomString - def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { - it.gppSid = US_NAT_V1.value - it.account = accountId - it.gpp = INVALID_GPP_STRING - } - - and: "Activities set for cookie sync with allowing privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [IAB_US_GENERAL] - } - - def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) - - and: "Account gpp configuration" - def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) - - and: "Existed account with cookie sync and privacy regulation setup" - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) - accountDao.save(account) - - when: "PBS processes cookie sync request" - def response = activityPbsService.sendCookieSyncRequest(cookieSyncRequest) - - then: "Response should not contain any URLs for bidders" - assert !response.bidderStatus.userSync.url - - and: "Response should not contain any warning" - assert !response.warnings - } - def "PBS cookie sync call when request have different gpp consent but match and rejecting should exclude bidders URLs"() { given: "Cookie sync request with link to account" def accountId = PBSUtils.randomString @@ -1359,41 +1326,6 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { ] } - def "PBS setuid request when privacy module contain invalid GPP string should reject bidders with status code invalidStatusCode"() { - given: "Cookie sync SetuidRequest with accountId" - def accountId = PBSUtils.randomString - def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { - it.account = accountId - it.gppSid = US_NAT_V1.value - it.gpp = INVALID_GPP_STRING - } - - and: "UIDS Cookie" - def uidsCookie = UidsCookie.defaultUidsCookie - - and: "Activities set for cookie sync with allowing privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [IAB_US_GENERAL] - } - - def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) - - and: "Account gpp configuration" - def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) - - and: "Existed account with cookie sync and allow activities setup" - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) - accountDao.save(account) - - when: "PBS processes cookie sync request" - activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) - - then: "Request should fail with error" - def exception = thrown(PrebidServerException) - assert exception.statusCode == INVALID_STATUS_CODE - assert exception.responseBody == INVALID_STATUS_MESSAGE - } - def "PBS setuid request when request have different gpp consent but match and rejecting should reject bidders with status code invalidStatusCode"() { given: "Cookie sync SetuidRequest with accountId" def accountId = PBSUtils.randomString diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy index edd720700ca..73dfda85a23 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy @@ -77,7 +77,6 @@ import static org.prebid.server.functional.model.request.auction.PrivacyModule.I import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_CUSTOM_LOGIC import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_GENERAL import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE -import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.model.State.ALABAMA import static org.prebid.server.functional.util.privacy.model.State.ONTARIO @@ -843,45 +842,6 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { ] } - def "PBS auction call when privacy module contain invalid GPP string should remove EIDS fields in request"() { - given: "Default Generic BidRequests with EIDS fields and account id" - def accountId = PBSUtils.randomNumber as String - def bidRequest = getBidRequestWithPersonalData(accountId).tap { - regs.gppSid = [US_NAT_V1.intValue] - regs.gpp = INVALID_GPP_STRING - } - - and: "Activities set for transmitEIDS with rejecting privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [IAB_US_GENERAL] - } - - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) - - and: "Account gpp configuration" - def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) - - and: "Existed account with privacy regulation setup" - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) - accountDao.save(account) - - when: "PBS processes auction requests" - def response = activityPbsService.sendAuctionRequest(bidRequest) - - then: "Generic bidder request should have empty EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) - verifyAll { - !genericBidderRequest.user.eids - !genericBidderRequest.user?.ext?.eids - } - - and: "Response should not contain any warnings" - assert !response.ext.warnings - - and: "Response should not contain any errors" - assert !response.ext.errors - } - def "PBS auction call when request have different gpp consent but match and rejecting should remove EIDS fields in request"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String @@ -1870,55 +1830,6 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { ] } - def "PBS amp call when privacy module contain invalid GPP string should remove EIDS fields in request"() { - given: "Default Generic BidRequest with EIDS fields field and account id" - def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = getBidRequestWithPersonalData(accountId) - - and: "amp request with link to account" - def ampRequest = AmpRequest.defaultAmpRequest.tap { - it.account = accountId - it.gppSid = US_NAT_V1.value - it.consentString = INVALID_GPP_STRING - it.consentType = GPP - } - - and: "Activities set for transmitEIDS with allowing privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [IAB_US_GENERAL] - } - - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) - - and: "Account gpp configuration" - def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) - - and: "Existed account with privacy regulation setup" - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) - accountDao.save(account) - - and: "Stored request in DB" - def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) - storedRequestDao.save(storedRequest) - - when: "PBS processes amp request" - def response = activityPbsService.sendAmpRequest(ampRequest) - - then: "Generic bidder request should have empty EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { - !genericBidderRequest.user.eids - !genericBidderRequest.user?.ext?.eids - } - - and: "Response should not contain any warnings" - assert !response.ext.warnings - - and: "Response should contain amp error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == ["Amp request parameter consent_string has invalid format: $INVALID_GPP_STRING"] - } - def "PBS amp call when request have different gpp consent but match and rejecting should remove EIDS fields in request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy index 0eaafecc6f0..920196ea576 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy @@ -15,10 +15,16 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.Activity import org.prebid.server.functional.model.request.auction.ActivityRule import org.prebid.server.functional.model.request.auction.AllowActivities +import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Condition +import org.prebid.server.functional.model.request.auction.Data import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Eid import org.prebid.server.functional.model.request.auction.Geo import org.prebid.server.functional.model.request.auction.RegsExt +import org.prebid.server.functional.model.request.auction.User +import org.prebid.server.functional.model.request.auction.UserExt +import org.prebid.server.functional.model.request.auction.UserExtData import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent @@ -81,7 +87,6 @@ import static org.prebid.server.functional.model.request.auction.PrivacyModule.I import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_CUSTOM_LOGIC import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_GENERAL import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE -import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.model.State.ALABAMA import static org.prebid.server.functional.util.privacy.model.State.ONTARIO @@ -1109,59 +1114,6 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { ] } - def "PBS auction call when privacy module contain invalid GPP string should remove UFPD fields in request"() { - given: "Default Generic BidRequests with UFPD fields and account id" - def accountId = PBSUtils.randomNumber as String - def bidRequest = getBidRequestWithPersonalData(accountId).tap { - regs.gppSid = [US_NAT_V1.intValue] - regs.gpp = INVALID_GPP_STRING - } - - and: "Activities set for transmitUfpd with rejecting privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [IAB_US_GENERAL] - } - - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) - - and: "Account gpp configuration" - def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) - - and: "Existed account with privacy regulation setup" - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) - accountDao.save(account) - - when: "PBS processes auction requests" - def response= activityPbsService.sendAuctionRequest(bidRequest) - - then: "Generic bidder request should have empty UFPD fields" - def bidderRequest = bidder.getBidderRequest(bidRequest.id) - verifyAll { - !bidderRequest.device.didsha1 - !bidderRequest.device.didmd5 - !bidderRequest.device.dpidsha1 - !bidderRequest.device.ifa - !bidderRequest.device.macsha1 - !bidderRequest.device.macmd5 - !bidderRequest.device.dpidmd5 - !bidderRequest.user.id - !bidderRequest.user.buyeruid - !bidderRequest.user.yob - !bidderRequest.user.gender - !bidderRequest.user.data - !bidderRequest.user.ext - } - - and: "Generic bidder request should have data in EIDS fields" - assert bidderRequest.user.eids == bidRequest.user.eids - - and: "Response should not contain any warnings" - assert !response.ext.warnings - - and: "Response should not contain any errors" - assert !response.ext.errors - } - def "PBS auction call when request have different gpp consent but match and rejecting should remove UFPD fields in request"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String @@ -2435,69 +2387,6 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { ] } - def "PBS amp call when privacy module contain invalid GPP string should remove UFPD fields in request"() { - given: "Default Generic BidRequest with UFPD fields field and account id" - def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = getBidRequestWithPersonalData(accountId) - - and: "amp request with link to account" - def ampRequest = AmpRequest.defaultAmpRequest.tap { - it.account = accountId - it.gppSid = US_NAT_V1.value - it.consentString = INVALID_GPP_STRING - it.consentType = GPP - } - - and: "Activities set for transmitUfpd with allowing privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [IAB_US_GENERAL] - } - - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) - - and: "Account gpp configuration" - def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) - - and: "Existed account with privacy regulation setup" - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) - accountDao.save(account) - - and: "Stored request in DB" - def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) - storedRequestDao.save(storedRequest) - - when: "PBS processes amp request" - def response = activityPbsService.sendAmpRequest(ampRequest) - - then: "Generic bidder request should have empty UFPD fields" - def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { - !bidderRequest.device.didsha1 - !bidderRequest.device.didmd5 - !bidderRequest.device.dpidsha1 - !bidderRequest.device.ifa - !bidderRequest.device.macsha1 - !bidderRequest.device.macmd5 - !bidderRequest.device.dpidmd5 - !bidderRequest.user.id - !bidderRequest.user.buyeruid - !bidderRequest.user.yob - !bidderRequest.user.gender - !bidderRequest.user.data - !bidderRequest.user.ext - } - - and: "Generic bidder request should have data in EIDS fields" - assert bidderRequest.user.eids == ampStoredRequest.user.eids - - and: "Response should not contain any warnings" - assert !response.ext.warnings - - and: "Response should contain amp error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == ["Amp request parameter consent_string has invalid format: $INVALID_GPP_STRING"] - } - def "PBS amp call when request have different gpp consent but match and rejecting should remove UFPD fields in request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy index aff17c4d49b..ed633ec5316 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy @@ -95,7 +95,6 @@ abstract class PrivacyBaseSpec extends BaseSpec { private static final Map GDPR_EEA_COUNTRY = ["gdpr.eea-countries": "$BULGARIA.ISOAlpha2, SK, VK" as String] protected static final String VENDOR_LIST_PATH = "/app/prebid-server/data/vendorlist-v{VendorVersion}/{VendorVersion}.json" - protected static final String INVALID_GPP_STRING = "DBABLA~BVQqAAAAAg.YA" // TODO replace BVQqAAAAAg with ${PBSUtils.getRandomString(7)} when proper fix is ready protected static final String VALID_VALUE_FOR_GPC_HEADER = "1" protected static final GppConsent SIMPLE_GPC_DISALLOW_LOGIC = new UsNatV1Consent.Builder().setGpc(true).build() protected static final VendorList vendorListResponse = new VendorList(networkServiceContainer) diff --git a/src/test/java/org/prebid/server/auction/gpp/model/GppContextCreatorTest.java b/src/test/java/org/prebid/server/auction/gpp/model/GppContextCreatorTest.java index 2d977ac65e7..dd5efdbe4c2 100644 --- a/src/test/java/org/prebid/server/auction/gpp/model/GppContextCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/gpp/model/GppContextCreatorTest.java @@ -37,7 +37,7 @@ public void fromShouldReturnGppContextWrapperWithErrorOnInvalidGpp() { assertThat(gppContext.regions()).isEqualTo(GppContext.Regions.builder().build()); }); assertThat(gppContextWrapper.getErrors()) - .containsExactly("GPP string invalid: Unable to decode 'invalidA'"); + .containsExactly("GPP string invalid: Unable to decode 'invalid'"); } @Test diff --git a/src/test/java/org/prebid/server/auction/gpp/model/GppModelWrapperTest.java b/src/test/java/org/prebid/server/auction/gpp/model/GppModelWrapperTest.java index 4f921fae0c3..587721151d5 100644 --- a/src/test/java/org/prebid/server/auction/gpp/model/GppModelWrapperTest.java +++ b/src/test/java/org/prebid/server/auction/gpp/model/GppModelWrapperTest.java @@ -5,16 +5,12 @@ import com.iab.gpp.encoder.error.EncodingException; import com.iab.gpp.encoder.section.HeaderV1; import com.iab.gpp.encoder.section.TcfEuV2; -import com.iab.gpp.encoder.section.UsNat; import com.iab.gpp.encoder.section.UspV1; import org.junit.jupiter.api.Test; import java.util.Comparator; -import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatNoException; public class GppModelWrapperTest { @@ -39,7 +35,7 @@ public class GppModelWrapperTest { + "1YN-"; @Test - public void wrapperShouldStoreSomeOfOriginalSections() throws DecodingException, EncodingException { + public void test() throws DecodingException, EncodingException { // given and when final GppModel originalGpp = new GppModel(GPP_STRING); final GppModel wrappedGpp = new GppModelWrapper(GPP_STRING); @@ -52,73 +48,6 @@ public void wrapperShouldStoreSomeOfOriginalSections() throws DecodingException, assertThat(wrappedGpp.encodeSection(UspV1.ID)).isEqualTo(originalGpp.encodeSection(UspV1.ID)); } - @Test - public void wrapperShouldPadSectionsIfNeeded() { - // given - final List samples = List.of( - "DBABLA~BVQqAAAAAg", - "DBABLA~BVVqCAAACg", - "DBABLA~BVVVBAAABg", - "DBABLA~BVVqCACACg", - "DBABLA~BVQVAAAAAg", - "DBABLA~BVVVBABABg"); - - for (String sample : samples) { - // when - final GppModel originalGpp = new GppModel(sample); - final GppModel wrappedGpp = new GppModelWrapper(sample); - - // then - assertThatExceptionOfType(DecodingException.class) - .isThrownBy(() -> originalGpp.getUsNatSection().getMspaCoveredTransaction()); - assertThatNoException() - .isThrownBy(() -> wrappedGpp.getUsNatSection().getMspaCoveredTransaction()); - } - } - - @Test - public void wrapperShouldNotModifyValidBase64SubsectionsWithPadChars() { - // given - final String gpp = "DBABLA~BVVVQAAARlA=.QA=="; - - // when - final GppModel wrappedGpp = new GppModelWrapper(gpp); - - // then - assertThat(wrappedGpp.encodeSection(UsNat.ID)).isEqualTo("BVVVQAAARlA=.QA=="); - } - - @Test - public void wrapperShouldNotModifyValidBase64SubsectionsWithoutPadChars() { - // given - final String gpp = "DBABLA~CqqqgAAAAIJo.YA=="; - - // when - final GppModel wrappedGpp = new GppModelWrapper(gpp); - - // then - assertThat(wrappedGpp.encodeSection(UsNat.ID)).isEqualTo("CqqqgAAAAIJo.YA=="); - assertThatNoException() - .isThrownBy(() -> wrappedGpp.getUsNatSection().getMspaCoveredTransaction()); - } - - @Test - public void wrapperShouldPadSubsections() { - // given - final String gpp = "DBABLA~BVVVQAAARl.Q"; - - // when - final GppModel originalGpp = new GppModel(gpp); - final GppModel wrappedGpp = new GppModelWrapper(gpp); - - // then - assertThat(wrappedGpp.encodeSection(UsNat.ID)).isEqualTo("BVVVQAAARlA.QA"); - assertThatExceptionOfType(DecodingException.class) - .isThrownBy(() -> originalGpp.getUsNatSection().getMspaCoveredTransaction()); - assertThatNoException() - .isThrownBy(() -> wrappedGpp.getUsNatSection().getMspaCoveredTransaction()); - } - public static String normalizeEncodedTcfEuV2Section(String encodedSection) { try { final GppModel normalizer = new GppModel();