diff --git a/Dockerfile b/Dockerfile index 92029011aba..7de0126d535 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM amazoncorretto:21.0.6-al2023 +FROM amazoncorretto:21.0.8-al2023 WORKDIR /app/prebid-server diff --git a/Dockerfile-modules b/Dockerfile-modules index aaa636e9d3c..1626999164a 100644 --- a/Dockerfile-modules +++ b/Dockerfile-modules @@ -1,4 +1,4 @@ -FROM amazoncorretto:21.0.6-al2023 +FROM amazoncorretto:21.0.8-al2023 WORKDIR /app/prebid-server diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 32c8ef2b187..a9b6fcc0990 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -65,6 +65,11 @@ wurfl-devicedetection ${project.version} + + org.prebid.server.hooks.modules + live-intent-omni-channel-identity + ${project.version} + diff --git a/extra/modules/live-intent-omni-channel-identity/README.md b/extra/modules/live-intent-omni-channel-identity/README.md new file mode 100644 index 00000000000..be5ad801ec1 --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/README.md @@ -0,0 +1,46 @@ +# Overview + +This module enriches bid requests with user EIDs. + +The user EIDs to be enriched are configured per partner as part of the LiveIntent HIRO onboarding process. As part of this onboarding process, partners will also be provided with the `identity-resolution-endpoint` URL as well as with the `auth-token`. + +`treatment-rate` is a value between 0.0 and 1.0 (including 0.0 and 1.0) and defines the percentage of requests for which identity enrichment should be performed. This value can be freely picked. We recommend a value between 0.9 and 0.95 + +## Configuration + +To start using the LiveIntent Omni Channel Identity module you have to enable it and add configuration: + +```yaml +hooks: + liveintent-omni-channel-identity: + enabled: true + host-execution-plan: > + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "processed-auction-request": { + "groups": [ + { + "timeout": 100, + "hook-sequence": [ + { + "module-code": "liveintent-omni-channel-identity", + "hook-impl-code": "liveintent-omni-channel-identity-enrichment-hook" + } + ] + } + ] + } + } + } + } + } + modules: + liveintent-omni-channel-identity: + request-timeout-ms: 2000 + identity-resolution-endpoint: "https://liveintent.com/idx" + auth-token: "secret-token" + treatment-rate: 0.9 +``` + diff --git a/extra/modules/live-intent-omni-channel-identity/pom.xml b/extra/modules/live-intent-omni-channel-identity/pom.xml new file mode 100644 index 00000000000..8b64db27a7d --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + org.prebid.server.hooks.modules + all-modules + 3.31.0-SNAPSHOT + + + live-intent-omni-channel-identity + + live-intent-omni-channel-identity + LiveIntent Omni-Channel Identity + + + + diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/config/LiveIntentOmniChannelIdentityConfiguration.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/config/LiveIntentOmniChannelIdentityConfiguration.java new file mode 100644 index 00000000000..e3f286e0120 --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/config/LiveIntentOmniChannelIdentityConfiguration.java @@ -0,0 +1,42 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.config; + +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.LiveIntentOmniChannelProperties; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.LiveIntentOmniChannelIdentityModule; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.hooks.LiveIntentOmniChannelIdentityProcessedAuctionRequestHook; +import org.prebid.server.hooks.v1.Module; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Collections; + +@Configuration +@ConditionalOnProperty( + prefix = "hooks." + LiveIntentOmniChannelIdentityModule.CODE, + name = "enabled", + havingValue = "true") +public class LiveIntentOmniChannelIdentityConfiguration { + + @Bean + @ConfigurationProperties(prefix = "hooks.modules." + LiveIntentOmniChannelIdentityModule.CODE) + LiveIntentOmniChannelProperties properties() { + return new LiveIntentOmniChannelProperties(); + } + + @Bean + Module liveIntentOmniChannelIdentityModule(LiveIntentOmniChannelProperties properties, + JacksonMapper mapper, + HttpClient httpClient, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + final LiveIntentOmniChannelIdentityProcessedAuctionRequestHook hook = + new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook( + properties, mapper, httpClient, logSamplingRate); + + return new LiveIntentOmniChannelIdentityModule(Collections.singleton(hook)); + } +} diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/IdResResponse.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/IdResResponse.java new file mode 100644 index 00000000000..35b22adca0d --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/IdResResponse.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model; + +import com.iab.openrtb.request.Eid; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor(staticName = "of") +public class IdResResponse { + + List eids; +} diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/config/LiveIntentOmniChannelProperties.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/config/LiveIntentOmniChannelProperties.java new file mode 100644 index 00000000000..b6a61b0ca8f --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/config/LiveIntentOmniChannelProperties.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config; + +import lombok.Data; + +@Data +public final class LiveIntentOmniChannelProperties { + + long requestTimeoutMs; + + String identityResolutionEndpoint; + + String authToken; + + float treatmentRate; +} diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityModule.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityModule.java new file mode 100644 index 00000000000..0ffdce8b436 --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityModule.java @@ -0,0 +1,23 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1; + +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; + +public record LiveIntentOmniChannelIdentityModule( + Collection> hooks) implements Module { + + public static final String CODE = "liveintent-omni-channel-identity"; + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..5c3e43f9952 --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java @@ -0,0 +1,116 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.hooks; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.User; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.ListUtils; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.IdResResponse; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.LiveIntentOmniChannelProperties; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.ListUtil; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; + +public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(LoggerFactory.getLogger( + LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.class)); + + private static final String CODE = "liveintent-omni-channel-identity-enrichment-hook"; + + private final LiveIntentOmniChannelProperties config; + private final JacksonMapper mapper; + private final HttpClient httpClient; + private final double logSamplingRate; + + public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(LiveIntentOmniChannelProperties config, + JacksonMapper mapper, + HttpClient httpClient, + double logSamplingRate) { + + this.config = Objects.requireNonNull(config); + HttpUtil.validateUrlSyntax(config.getIdentityResolutionEndpoint()); + this.mapper = Objects.requireNonNull(mapper); + this.httpClient = Objects.requireNonNull(httpClient); + this.logSamplingRate = logSamplingRate; + } + + @Override + public Future> call(AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + + return config.getTreatmentRate() > ThreadLocalRandom.current().nextFloat() + ? requestIdentities(auctionRequestPayload.bidRequest()) + .>map(this::update) + .onFailure(throwable -> conditionalLogger.error( + "Failed enrichment: %s".formatted(throwable.getMessage()), logSamplingRate)) + : noAction(); + } + + private Future requestIdentities(BidRequest bidRequest) { + return httpClient.post( + config.getIdentityResolutionEndpoint(), + headers(), + mapper.encodeToString(bidRequest), + config.getRequestTimeoutMs()) + .map(this::processResponse); + } + + private MultiMap headers() { + return MultiMap.caseInsensitiveMultiMap() + .add(HttpUtil.AUTHORIZATION_HEADER, "Bearer " + config.getAuthToken()); + } + + private IdResResponse processResponse(HttpClientResponse response) { + return mapper.decodeValue(response.getBody(), IdResResponse.class); + } + + private static Future> noAction() { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + private InvocationResultImpl update(IdResResponse resolutionResult) { + return InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(payload -> updatedPayload(payload, resolutionResult.getEids())) + .build(); + } + + private AuctionRequestPayload updatedPayload(AuctionRequestPayload requestPayload, List resolvedEids) { + final List eids = ListUtils.emptyIfNull(resolvedEids); + final BidRequest bidRequest = requestPayload.bidRequest(); + final User updatedUser = Optional.ofNullable(bidRequest.getUser()) + .map(user -> user.toBuilder().eids(ListUtil.union(ListUtils.emptyIfNull(user.getEids()), eids))) + .orElseGet(() -> User.builder().eids(eids)) + .build(); + + return AuctionRequestPayloadImpl.of(bidRequest.toBuilder().user(updatedUser).build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java b/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java new file mode 100644 index 00000000000..9dc53916980 --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java @@ -0,0 +1,202 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; +import io.vertx.core.Future; +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.hooks.execution.v1.auction.AuctionInvocationContextImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.IdResResponse; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.LiveIntentOmniChannelProperties; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.hooks.LiveIntentOmniChannelIdentityProcessedAuctionRequestHook; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.util.List; +import java.util.concurrent.TimeoutException; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest { + + private static final JacksonMapper MAPPER = new JacksonMapper(ObjectMapperProvider.mapper()); + + @Mock + private HttpClient httpClient; + + @Mock(strictness = LENIENT) + private LiveIntentOmniChannelProperties properties; + + private LiveIntentOmniChannelIdentityProcessedAuctionRequestHook target; + + @BeforeEach + public void setUp() { + given(properties.getRequestTimeoutMs()).willReturn(5L); + given(properties.getIdentityResolutionEndpoint()).willReturn("https://test.com/idres"); + given(properties.getAuthToken()).willReturn("auth_token"); + given(properties.getTreatmentRate()).willReturn(1.0f); + + target = new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook( + properties, MAPPER, httpClient, 0.01d); + } + + @Test + public void creationShouldFailOnInvalidIdentityUrl() { + given(properties.getIdentityResolutionEndpoint()).willReturn("invalid_url"); + assertThatIllegalArgumentException().isThrownBy(() -> + new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook( + properties, MAPPER, httpClient, 0.01d)); + } + + @Test + public void callShouldEnrichUserEidsWithRequestedEids() { + // given + final Uid givenUid = Uid.builder().id("id1").atype(2).build(); + final Eid givenEid = Eid.builder().source("some.source.com").uids(singletonList(givenUid)).build(); + final User givenUser = User.builder().eids(singletonList(givenEid)).build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").user(givenUser).build(); + + final Eid expectedEid = Eid.builder() + .source("liveintent.com") + .uids(singletonList(Uid.builder().id("id2").atype(3).build())) + .build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + final AuctionInvocationContext auctionInvocationContext = AuctionInvocationContextImpl.of( + null, null, false, null, null); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.payloadUpdate().apply(AuctionRequestPayloadImpl.of(givenBidRequest))) + .extracting(AuctionRequestPayload::bidRequest) + .extracting(BidRequest::getUser) + .extracting(User::getEids) + .isEqualTo(List.of(givenEid, expectedEid)); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(givenBidRequest)), + eq(5L)); + } + + @Test + public void callShouldCreateUserAndUseRequestedEidsWhenUserIsAbsent() { + // given + final BidRequest givenBidRequest = BidRequest.builder().id("request").user(null).build(); + + final Eid expectedEid = Eid.builder() + .source("liveintent.com") + .uids(singletonList(Uid.builder().id("id2").atype(3).build())) + .build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + final AuctionInvocationContext auctionInvocationContext = AuctionInvocationContextImpl.of( + null, null, false, null, null); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.payloadUpdate().apply(AuctionRequestPayloadImpl.of(givenBidRequest))) + .extracting(AuctionRequestPayload::bidRequest) + .extracting(BidRequest::getUser) + .extracting(User::getEids) + .isEqualTo(List.of(expectedEid)); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(givenBidRequest)), + eq(5L)); + } + + @Test + public void callShouldReturnNoActionSuccessfullyWhenTreatmentRateIsLowerThanThreshold() { + // given + final Uid givenUid = Uid.builder().id("id1").atype(2).build(); + final Eid givebEid = Eid.builder().source("some.source.com").uids(singletonList(givenUid)).build(); + final User givenUser = User.builder().eids(singletonList(givebEid)).build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").user(givenUser).build(); + + final AuctionInvocationContext auctionInvocationContext = AuctionInvocationContextImpl.of( + null, null, false, null, null); + + given(properties.getTreatmentRate()).willReturn(0.0f); + + // when + final InvocationResult result = target.call( + AuctionRequestPayloadImpl.of(givenBidRequest), + auctionInvocationContext) + .result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result.payloadUpdate()).isNull(); + } + + @Test + public void callShouldReturnFailureWhenRequestingEidsIsFailed() { + // given + final Uid givenUid = Uid.builder().id("id1").atype(2).build(); + final Eid givebEid = Eid.builder().source("some.source.com").uids(singletonList(givenUid)).build(); + final User givenUser = User.builder().eids(singletonList(givebEid)).build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").user(givenUser).build(); + + final AuctionInvocationContext auctionInvocationContext = AuctionInvocationContextImpl.of( + null, null, false, null, null); + + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.failedFuture(new TimeoutException("Timeout exceeded"))); + + // when + final Future> result = target.call( + AuctionRequestPayloadImpl.of(givenBidRequest), + auctionInvocationContext); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause()).isInstanceOf(TimeoutException.class); + assertThat(result.cause()) + .isInstanceOf(TimeoutException.class) + .hasMessage("Timeout exceeded"); + } +} diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index bb71bcd798b..00c3294b1a9 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -26,6 +26,7 @@ pb-request-correction optable-targeting wurfl-devicedetection + live-intent-omni-channel-identity diff --git a/src/main/java/org/prebid/server/bidder/blis/BlisBidder.java b/src/main/java/org/prebid/server/bidder/blis/BlisBidder.java new file mode 100644 index 00000000000..10e9ee547b4 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/blis/BlisBidder.java @@ -0,0 +1,138 @@ +package org.prebid.server.bidder.blis; + +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 io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +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.Result; +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.blis.ExtImpBlis; +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; +import java.util.stream.Collectors; + +public class BlisBidder implements Bidder { + + private static final TypeReference> BLIS_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String AUCTION_PRICE_MACRO = "${AUCTION_PRICE}"; + private static final String SUPPLY_ID_MACRO = "{{SupplyId}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public BlisBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final String supplyId; + try { + supplyId = parseImpExt(request.getImp().getFirst()).getSupplyId(); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + return Result.withValue(BidderUtil.defaultRequest(request, makeHeaders(supplyId), makeUrl(supplyId), mapper)); + } + + private ExtImpBlis parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), BLIS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing imp.ext: " + e.getMessage()); + } + } + + private static MultiMap makeHeaders(String supplyId) { + return HttpUtil.headers().add("X-Supply-Partner-Id", supplyId); + } + + private String makeUrl(String supplyId) { + return endpointUrl.replace(SUPPLY_ID_MACRO, HttpUtil.encodeUrl(supplyId)); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException 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 bidsFromResponse(bidResponse, errors); + } + + private static List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType bidType = getBidType(bid, errors); + return bidType != null + ? BidderBid.of(resolveMacros(bid), bidType, currency) + : null; + } + + private static Bid resolveMacros(Bid bid) { + final BigDecimal price = bid.getPrice(); + final String priceAsString = price != null ? price.toPlainString() : "0"; + + return bid.toBuilder() + .nurl(StringUtils.replace(bid.getNurl(), AUCTION_PRICE_MACRO, priceAsString)) + .adm(StringUtils.replace(bid.getAdm(), AUCTION_PRICE_MACRO, priceAsString)) + .burl(StringUtils.replace(bid.getBurl(), AUCTION_PRICE_MACRO, priceAsString)) + .build(); + } + + private static BidType getBidType(Bid bid, List errors) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> { + errors.add(BidderError.badServerResponse( + "Failed to parse media type of impression ID " + bid.getImpid())); + yield null; + } + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/connatix/ConnatixBidder.java b/src/main/java/org/prebid/server/bidder/connatix/ConnatixBidder.java index f6adfff4867..b319b04c932 100644 --- a/src/main/java/org/prebid/server/bidder/connatix/ConnatixBidder.java +++ b/src/main/java/org/prebid/server/bidder/connatix/ConnatixBidder.java @@ -53,6 +53,7 @@ public class ConnatixBidder implements Bidder { private static final String BIDDER_CURRENCY = "USD"; private static final String FORMATTING = "%s-%s"; + private static final String GPID_KEY = "gpid"; private final String endpointUrl; private final JacksonMapper mapper; @@ -173,6 +174,11 @@ private Imp modifyImp(Imp imp, ExtImpConnatix extImpConnatix, String displayMana final ObjectNode impExt = mapper.mapper() .createObjectNode().set("connatix", mapper.mapper().valueToTree(extImpConnatix)); + Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get(GPID_KEY)) + .filter(JsonNode::isTextual) + .ifPresent(gpidNode -> impExt.set(GPID_KEY, gpidNode)); + return imp.toBuilder() .ext(impExt) .banner(modifyImpBanner(imp.getBanner())) diff --git a/src/main/java/org/prebid/server/bidder/exco/ExcoBidder.java b/src/main/java/org/prebid/server/bidder/exco/ExcoBidder.java new file mode 100644 index 00000000000..41e067def03 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/exco/ExcoBidder.java @@ -0,0 +1,153 @@ +package org.prebid.server.bidder.exco; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +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.Result; +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.exco.ExtImpExco; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class ExcoBidder implements Bidder { + + private static final TypeReference> EXCO_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public ExcoBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(); + + String publisherId = null; + + for (Imp imp : request.getImp()) { + try { + final ExtImpExco extImp = parseImpExt(imp); + modifiedImps.add(imp.toBuilder().tagid(extImp.getTagId()).build()); + publisherId = extImp.getPublisherId(); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, publisherId); + return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)); + } + + private ExtImpExco parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), EXCO_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Invalid imp.ext for impression %s. Error Information: %s" + .formatted(imp.getId(), e.getMessage())); + } + } + + private BidRequest modifyRequest(BidRequest request, List imps, String publisherId) { + final Site site = request.getSite(); + final App app = request.getApp(); + + return request.toBuilder() + .imp(imps) + .site(site != null ? modifySite(site, publisherId) : null) + .app(app != null ? modifyApp(app, publisherId) : null) + .build(); + } + + private static Site modifySite(Site site, String publisherId) { + return site.toBuilder().publisher(modifyPublisher(site.getPublisher(), publisherId)).build(); + } + + private static App modifyApp(App app, String publisherId) { + return app.toBuilder().publisher(modifyPublisher(app.getPublisher(), publisherId)).build(); + } + + private static Publisher modifyPublisher(Publisher publisher, String publisherId) { + return Optional.ofNullable(publisher) + .map(Publisher::toBuilder) + .orElseGet(Publisher::builder) + .id(publisherId) + .build(); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException 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 bidsFromResponse(bidResponse, errors); + } + + private static List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, List errors) { + final BidType bidType = getBidType(bid, errors); + return bidType != null + ? BidderBid.of(bid, bidType, currency) + : null; + } + + private static BidType getBidType(Bid bid, List errors) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case null, default -> { + errors.add(BidderError.badServerResponse( + "unrecognized bid_ad_type in response from exco: " + bid.getMtype())); + yield null; + } + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java index ffd6c08d184..7b8be79fa7d 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java @@ -13,9 +13,12 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; +import org.prebid.server.auction.aliases.AlternateBidder; +import org.prebid.server.auction.aliases.AlternateBidderCodesConfig; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -25,6 +28,7 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.pubmatic.model.request.PubmaticBidderImpExt; import org.prebid.server.bidder.pubmatic.model.request.PubmaticExtDataAdServer; +import org.prebid.server.bidder.pubmatic.model.request.PubmaticMarketplace; import org.prebid.server.bidder.pubmatic.model.request.PubmaticWrapper; import org.prebid.server.bidder.pubmatic.model.response.PubmaticBidExt; import org.prebid.server.bidder.pubmatic.model.response.PubmaticBidResponse; @@ -59,6 +63,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -70,15 +75,19 @@ public class PubmaticBidder implements Bidder { private static final String IMP_EXT_AD_UNIT_KEY = "dfp_ad_unit_code"; private static final String AD_SERVER_GAM = "gam"; private static final String PREBID = "prebid"; + private static final String MARKETPLACE_EXT_REQUEST = "marketplace"; private static final String ACAT_EXT_REQUEST = "acat"; private static final String WRAPPER_EXT_REQUEST = "wrapper"; private static final String BIDDER_NAME = "pubmatic"; private static final String AE = "ae"; private static final String GP_ID = "gpid"; + private static final String SKADN = "skadn"; private static final String IMP_EXT_PBADSLOT = "pbadslot"; private static final String IMP_EXT_ADSERVER = "adserver"; private static final List IMP_EXT_DATA_RESERVED_FIELD = List.of(IMP_EXT_PBADSLOT, IMP_EXT_ADSERVER); private static final String DCTR_VALUE_FORMAT = "%s=%s"; + private static final String WILDCARD = "*"; + private static final String WILDCARD_ALL = "all"; private final String endpointUrl; private final JacksonMapper mapper; @@ -97,10 +106,13 @@ public Result>> makeHttpRequests(BidRequest request PubmaticWrapper wrapper; final List acat; final Pair displayManagerFields; + final List allowedBidders; try { - acat = extractAcat(request); - wrapper = extractWrapper(request); + final JsonNode bidderparams = getExtRequestPrebidBidderparams(request); + acat = extractAcat(bidderparams); + wrapper = extractWrapper(bidderparams); + allowedBidders = extractAllowedBidders(request); displayManagerFields = extractDisplayManagerFields(request.getApp()); } catch (IllegalArgumentException e) { return Result.withError(BidderError.badInput(e.getMessage())); @@ -129,12 +141,43 @@ public Result>> makeHttpRequests(BidRequest request return Result.withErrors(errors); } - final BidRequest modifiedBidRequest = modifyBidRequest(request, validImps, publisherId, wrapper, acat); + final BidRequest modifiedBidRequest = modifyBidRequest( + request, validImps, publisherId, wrapper, acat, allowedBidders); return Result.of(Collections.singletonList(makeHttpRequest(modifiedBidRequest)), errors); } - private List extractAcat(BidRequest request) { - final JsonNode bidderParams = getExtRequestPrebidBidderparams(request); + private List extractAllowedBidders(BidRequest request) { + final AlternateBidderCodesConfig alternateBidderCodes = Optional.ofNullable(request.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAlternateBidderCodes) + .orElse(null); + + if (alternateBidderCodes == null) { + return null; + } + + if (BooleanUtils.isNotTrue(alternateBidderCodes.getEnabled())) { + return Collections.singletonList(BIDDER_NAME); + } + + final AlternateBidder alternateBidder = Optional.ofNullable(alternateBidderCodes.getBidders()) + .map(bidders -> bidders.get(BIDDER_NAME)) + .filter(bidder -> BooleanUtils.isTrue(bidder.getEnabled())) + .orElse(null); + + if (alternateBidder == null) { + return Collections.singletonList(BIDDER_NAME); + } + + final Set allowedBidderCodes = alternateBidder.getAllowedBidderCodes(); + if (allowedBidderCodes == null || allowedBidderCodes.contains(WILDCARD)) { + return Collections.singletonList(WILDCARD_ALL); + } + + return Stream.concat(Stream.of(BIDDER_NAME), allowedBidderCodes.stream()).toList(); + } + + private List extractAcat(JsonNode bidderParams) { final JsonNode acatNode = bidderParams != null ? bidderParams.get(ACAT_EXT_REQUEST) : null; return acatNode != null && acatNode.isArray() @@ -144,9 +187,8 @@ private List extractAcat(BidRequest request) { : null; } - private PubmaticWrapper extractWrapper(BidRequest request) { - final JsonNode pubmatic = getExtRequestPrebidBidderparams(request); - final JsonNode wrapperNode = pubmatic != null ? pubmatic.get(WRAPPER_EXT_REQUEST) : null; + private PubmaticWrapper extractWrapper(JsonNode bidderParams) { + final JsonNode wrapperNode = bidderParams != null ? bidderParams.get(WRAPPER_EXT_REQUEST) : null; return wrapperNode != null && wrapperNode.isObject() ? mapper.mapper().convertValue(wrapperNode, PubmaticWrapper.class) @@ -294,6 +336,9 @@ private ObjectNode makeKeywords(PubmaticBidderImpExt impExt) { if (impExt.getGpId() != null) { keywordsNode.put(GP_ID, impExt.getGpId()); } + if (impExt.getSkadn() != null) { + keywordsNode.set(SKADN, impExt.getSkadn()); + } return keywordsNode; } @@ -433,13 +478,14 @@ private BidRequest modifyBidRequest(BidRequest request, List imps, String publisherId, PubmaticWrapper wrapper, - List acat) { + List acat, + List allowedBidders) { return request.toBuilder() .imp(imps) .site(modifySite(request.getSite(), publisherId)) .app(modifyApp(request.getApp(), publisherId)) - .ext(modifyExtRequest(wrapper, acat)) + .ext(modifyExtRequest(wrapper, acat, allowedBidders)) .build(); } @@ -465,7 +511,7 @@ private static Publisher modifyPublisher(Publisher publisher, String publisherId : Publisher.builder().id(publisherId).build(); } - private ExtRequest modifyExtRequest(PubmaticWrapper wrapper, List acat) { + private ExtRequest modifyExtRequest(PubmaticWrapper wrapper, List acat, List allowedBidders) { final ObjectNode extNode = mapper.mapper().createObjectNode(); if (wrapper != null) { @@ -476,6 +522,10 @@ private ExtRequest modifyExtRequest(PubmaticWrapper wrapper, List acat) extNode.putPOJO(ACAT_EXT_REQUEST, acat); } + if (allowedBidders != null) { + extNode.putPOJO(MARKETPLACE_EXT_REQUEST, PubmaticMarketplace.of(allowedBidders)); + } + final ExtRequest newExtRequest = ExtRequest.empty(); return extNode.isEmpty() ? newExtRequest diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticBidderImpExt.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticBidderImpExt.java index bdfd08a9dae..0a248ff8a5b 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticBidderImpExt.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticBidderImpExt.java @@ -16,4 +16,6 @@ public class PubmaticBidderImpExt { @JsonProperty("gpid") String gpId; + + ObjectNode skadn; } diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticMarketplace.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticMarketplace.java new file mode 100644 index 00000000000..8c4f593dbd3 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticMarketplace.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.pubmatic.model.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class PubmaticMarketplace { + + @JsonProperty("allowedbidders") + List allowedBidders; +} diff --git a/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java b/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java index d6dd53b75d7..e2d353fb003 100644 --- a/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java +++ b/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java @@ -49,22 +49,30 @@ public SmartadserverBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - final List> result = new ArrayList<>(); final List errors = new ArrayList<>(); + final List imps = new ArrayList<>(); + ExtImpSmartadserver extImp = null; for (Imp imp : request.getImp()) { try { - final ExtImpSmartadserver extImpSmartadserver = parseImpExt(imp); - final BidRequest updatedRequest = request.toBuilder() - .imp(Collections.singletonList(imp)) - .site(modifySite(request.getSite(), extImpSmartadserver.getNetworkId())) - .build(); - result.add(createSingleRequest(updatedRequest)); + extImp = parseImpExt(imp); + imps.add(imp); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } } - return Result.of(result, errors); + + if (imps.isEmpty()) { + return Result.withErrors(errors); + } + + final BidRequest outgoingRequest = request.toBuilder() + .imp(imps) + .site(modifySite(request.getSite(), extImp.getNetworkId())) + .build(); + + final HttpRequest httpRequest = BidderUtil.defaultRequest(outgoingRequest, makeUrl(), mapper); + return Result.of(Collections.singletonList(httpRequest), errors); } private ExtImpSmartadserver parseImpExt(Imp imp) { @@ -75,24 +83,6 @@ private ExtImpSmartadserver parseImpExt(Imp imp) { } } - private HttpRequest createSingleRequest(BidRequest request) { - - return BidderUtil.defaultRequest(request, getUri(), mapper); - } - - private String getUri() { - final URI uri; - try { - uri = new URI(endpointUrl); - } catch (URISyntaxException e) { - throw new PreBidException("Malformed URL: %s.".formatted(endpointUrl)); - } - return new URIBuilder(uri) - .setPath(StringUtils.removeEnd(uri.getPath(), "/") + "/api/bid") - .addParameter("callerId", "5") - .toString(); - } - private static Site modifySite(Site site, Integer networkId) { final Site.SiteBuilder siteBuilder = site != null ? site.toBuilder() : Site.builder(); final Publisher sitePublisher = site != null ? site.getPublisher() : null; @@ -108,6 +98,19 @@ private static Publisher modifyPublisher(Publisher publisher, Integer networkId) return publisherBuilder.id(String.valueOf(networkId)).build(); } + private String makeUrl() { + final URI uri; + try { + uri = new URI(endpointUrl); + } catch (URISyntaxException e) { + throw new PreBidException("Malformed URL: %s.".formatted(endpointUrl)); + } + return new URIBuilder(uri) + .setPath(StringUtils.removeEnd(uri.getPath(), "/") + "/api/bid") + .addParameter("callerId", "5") + .toString(); + } + @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { @@ -135,7 +138,6 @@ private Result> extractBids(BidResponse bidResponse) { private static BidType getBidTypeFromMarkupType(Integer markupType) { return switch (markupType) { - case 1 -> BidType.banner; case 2 -> BidType.video; case 3 -> BidType.audio; case 4 -> BidType.xNative; diff --git a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java new file mode 100644 index 00000000000..6beba78b3dc --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java @@ -0,0 +1,212 @@ +package org.prebid.server.bidder.sparteo; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +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.Result; +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.ExtPublisher; +import org.prebid.server.proto.openrtb.ext.request.sparteo.ExtImpSparteo; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class SparteoBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = + new TypeReference<>() { }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public SparteoBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List modifiedImps = new ArrayList<>(); + String siteNetworkId = null; + + for (Imp imp : request.getImp()) { + if (siteNetworkId == null) { + try { + siteNetworkId = parseExtImp(imp).getNetworkId(); + } catch (PreBidException e) { + errors.add(BidderError.badInput( + "ignoring imp id=%s, error processing ext: %s".formatted( + imp.getId(), e.getMessage()))); + } + } + + final ObjectNode modifiedExt = modifyImpExt(imp); + modifiedImps.add(imp.toBuilder().ext(modifiedExt).build()); + } + + if (modifiedImps.isEmpty()) { + return Result.withErrors(errors); + } + + final BidRequest outgoingRequest = request.toBuilder() + .imp(modifiedImps) + .site(modifySite(request.getSite(), siteNetworkId, mapper)) + .build(); + + final HttpRequest call = BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + + return Result.of(Collections.singletonList(call), errors); + } + + private ExtImpSparteo parseExtImp(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("invalid imp.ext"); + } + } + + private static ObjectNode modifyImpExt(Imp imp) { + final ObjectNode modifiedImpExt = imp.getExt().deepCopy(); + final ObjectNode sparteoNode = modifiedImpExt.putObject("sparteo"); + final JsonNode bidderJsonNode = modifiedImpExt.remove("bidder"); + sparteoNode.set("params", bidderJsonNode); + + return modifiedImpExt; + } + + private Site modifySite(Site site, String siteNetworkId, JacksonMapper mapper) { + if (site == null || site.getPublisher() == null || siteNetworkId == null) { + return site; + } + + final Publisher originalPublisher = site.getPublisher(); + final ExtPublisher originalExt = originalPublisher.getExt(); + + final ExtPublisher modifiedExt = originalExt != null + ? ExtPublisher.of(originalExt.getPrebid()) + : ExtPublisher.empty(); + + if (originalExt != null) { + mapper.fillExtension(modifiedExt, originalExt); + } + + final JsonNode paramsProperty = modifiedExt.getProperty("params"); + final ObjectNode paramsNode; + + if (paramsProperty != null && paramsProperty.isObject()) { + paramsNode = (ObjectNode) paramsProperty; + } else { + paramsNode = mapper.mapper().createObjectNode(); + modifiedExt.addProperty("params", paramsNode); + } + + paramsNode.put("networkId", siteNetworkId); + + final Publisher modifiedPublisher = originalPublisher.toBuilder() + .ext(modifiedExt) + .build(); + + return site.toBuilder() + .publisher(modifiedPublisher) + .build(); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private 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 -> toBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid toBidderBid(Bid bid, String currency, List errors) { + try { + final BidType bidType = getBidType(bid); + + final Integer mtype = switch (bidType) { + case banner -> 1; + case video -> 2; + case xNative -> 4; + default -> null; + }; + + final Bid bidWithMtype = mtype != null ? bid.toBuilder().mtype(mtype).build() : bid; + + return BidderBid.of(bidWithMtype, bidType, currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private BidType getBidType(Bid bid) throws PreBidException { + final BidType bidType = Optional.ofNullable(bid.getExt()) + .map(ext -> ext.get("prebid")) + .filter(JsonNode::isObject) + .map(this::parseExtBidPrebid) + .map(ExtBidPrebid::getType) + .orElseThrow(() -> new PreBidException( + "Failed to parse bid mediatype for impression \"%s\"".formatted(bid.getImpid()))); + + if (bidType == BidType.audio) { + throw new PreBidException( + "Audio bid type not supported by this adapter for impression id: %s".formatted(bid.getImpid())); + } + + return bidType; + } + + private ExtBidPrebid parseExtBidPrebid(JsonNode prebidNode) { + try { + return mapper.mapper().treeToValue(prebidNode, ExtBidPrebid.class); + } catch (JsonProcessingException e) { + return null; + } + } +} 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 d83274e4852..a2853b26d72 100644 --- a/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java +++ b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java @@ -231,6 +231,7 @@ private static Bid resolvePriceMacros(Bid bid) { return bid.toBuilder() .nurl(StringUtils.replace(bid.getNurl(), PRICE_MACRO, priceAsString)) .adm(StringUtils.replace(bid.getAdm(), PRICE_MACRO, priceAsString)) + .burl(StringUtils.replace(bid.getBurl(), PRICE_MACRO, priceAsString)) .build(); } } diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java index 8c06a3794fa..47f6cd44523 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java @@ -464,6 +464,7 @@ private BidderBid makeBid(BidRequest bidRequest, } final Format adsize = resolveAdSize(yieldlabBid.getAdSize()); + final String advertiser = yieldlabBid.getAdvertiser(); final Bid bid = Bid.builder() .id(adSlotId) .price(BigDecimal.valueOf(yieldlabBid.getPrice() / 100)) @@ -476,6 +477,7 @@ private BidderBid makeBid(BidRequest bidRequest, : makeBanner(bidRequest, extImp, yieldlabBid)) .w(adsize.getW()) .h(adsize.getH()) + .adomain(advertiser != null ? Collections.singletonList(advertiser) : null) .ext(resolveBidExt(yieldlabBid, errors)) .build(); diff --git a/src/main/java/org/prebid/server/bidder/zentotem/ZentotemBidder.java b/src/main/java/org/prebid/server/bidder/zentotem/ZentotemBidder.java index c665b06650c..1aac754f04a 100644 --- a/src/main/java/org/prebid/server/bidder/zentotem/ZentotemBidder.java +++ b/src/main/java/org/prebid/server/bidder/zentotem/ZentotemBidder.java @@ -61,7 +61,7 @@ public Result> makeBids(BidderCall httpCall, BidRequ final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); final List errors = new ArrayList<>(); return Result.of(extractBids(bidResponse, errors), errors); - } catch (DecodeException | PreBidException e) { + } catch (DecodeException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/blis/ExtImpBlis.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blis/ExtImpBlis.java new file mode 100644 index 00000000000..0b0807ecf3e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blis/ExtImpBlis.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.blis; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpBlis { + + @JsonProperty("spid") + String supplyId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/exco/ExtImpExco.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/exco/ExtImpExco.java new file mode 100644 index 00000000000..91bef799cbd --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/exco/ExtImpExco.java @@ -0,0 +1,17 @@ +package org.prebid.server.proto.openrtb.ext.request.exco; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpExco { + + @JsonProperty("accountId") + String accountId; + + @JsonProperty("publisherId") + String publisherId; + + @JsonProperty("tagId") + String tagId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/sparteo/ExtImpSparteo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sparteo/ExtImpSparteo.java new file mode 100644 index 00000000000..cb9bda90558 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sparteo/ExtImpSparteo.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.sparteo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpSparteo { + + @JsonProperty("networkId") + String networkId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BlisBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BlisBidderConfiguration.java new file mode 100644 index 00000000000..47d2f796f40 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BlisBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.blis.BlisBidder; +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/blis.yaml", factory = YamlPropertySourceFactory.class) +public class BlisBidderConfiguration { + + private static final String BIDDER_NAME = "blis"; + + @Bean("blisConfigurationProperties") + @ConfigurationProperties("adapters.blis") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps blisBidderDeps(BidderConfigurationProperties blisConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(blisConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BlisBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ExcoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ExcoConfiguration.java new file mode 100644 index 00000000000..9e079821d2e --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ExcoConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.exco.ExcoBidder; +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 javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/exco.yaml", factory = YamlPropertySourceFactory.class) +public class ExcoConfiguration { + + private static final String BIDDER_NAME = "exco"; + + @Bean("excoConfigurationProperties") + @ConfigurationProperties("adapters.exco") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps excoBidderDeps(BidderConfigurationProperties excoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(excoConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ExcoBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SparteoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SparteoConfiguration.java new file mode 100644 index 00000000000..5934319d2db --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/SparteoConfiguration.java @@ -0,0 +1,42 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.sparteo.SparteoBidder; +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/sparteo.yaml", + factory = YamlPropertySourceFactory.class) +public class SparteoConfiguration { + + private static final String BIDDER_NAME = "sparteo"; + + @Bean("sparteoConfigurationProperties") + @ConfigurationProperties("adapters.sparteo") + public BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + public BidderDeps sparteoBidderDeps(BidderConfigurationProperties sparteoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(sparteoConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new SparteoBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/blis.yaml b/src/main/resources/bidder-config/blis.yaml new file mode 100644 index 00000000000..6d9d50dad69 --- /dev/null +++ b/src/main/resources/bidder-config/blis.yaml @@ -0,0 +1,24 @@ +adapters: + blis: + endpoint: https://prebid.lb.infinity.blismedia.com/rtb/213/{{SupplyId}} + modifying-vast-xml-allowed: true + endpoint-compression: gzip + ortb-version: "2.6" + meta-info: + maintainer-email: prebid-support@blis.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 94 + usersync: + cookie-family-name: blis + redirect: + url: https://tr.blismedia.com/v1/api/sync/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r={{redirect_url}} + support-cors: false + uid-macro: '%%BLIS_USER_TOKEN%%' diff --git a/src/main/resources/bidder-config/exco.yaml b/src/main/resources/bidder-config/exco.yaml new file mode 100644 index 00000000000..df2a4dac0a1 --- /dev/null +++ b/src/main/resources/bidder-config/exco.yaml @@ -0,0 +1,19 @@ +adapters: + exco: + endpoint: https://v.ex.co/se/openrtb/hb/pbs + meta-info: + maintainer-email: itadmin@ex.co + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 444 + usersync: + cookie-family-name: exco + redirect: + url: https://sync.ex.co/v1/user_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/sparteo.yaml b/src/main/resources/bidder-config/sparteo.yaml new file mode 100644 index 00000000000..44cbe16bd86 --- /dev/null +++ b/src/main/resources/bidder-config/sparteo.yaml @@ -0,0 +1,20 @@ +adapters: + sparteo: + endpoint: https://bid.sparteo.com/s2s-auction + meta-info: + maintainer-email: prebid@sparteo.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 1028 + usersync: + cookie-family-name: sparteo + iframe: + url: "https://sync.sparteo.com/s2s_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect_url={{redirect_url}}" + support-cors: true diff --git a/src/main/resources/bidder-config/vidazoo.yaml b/src/main/resources/bidder-config/vidazoo.yaml index 13ff0644788..9ac04fbb2dc 100644 --- a/src/main/resources/bidder-config/vidazoo.yaml +++ b/src/main/resources/bidder-config/vidazoo.yaml @@ -15,6 +15,26 @@ adapters: url: https://sync.programmaticx.ai/api/user/html/685297194d85991a5e6e36dd?pbs=true&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}} support-cors: false uid-macro: '${userId}' + omnidex: + endpoint: https://exchange.omni-dex.io/openrtb/ + usersync: + enabled: true + cookie-family-name: omnidex + iframe: + url: https://sync.omni-dex.io/api/user/html/6810d0c7f163277130f3d7a9?pbs=true&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}} + support-cors: false + uid-macro: '${userId}' + tagoras: + endpoint: https://exchange.tagoras.io/openrtb/ + meta-info: + maintainer-email: prebid@tagoras.io + usersync: + enabled: true + cookie-family-name: tagoras + iframe: + url: https://sync.tagoras.io/api/user/html/6819bdc3e6bb44545c55f843?pbs=true&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}} + support-cors: false + uid-macro: '${userId}' endpoint-compression: gzip ortb-version: "2.6" meta-info: diff --git a/src/main/resources/static/bidder-params/blis.json b/src/main/resources/static/bidder-params/blis.json new file mode 100644 index 00000000000..34d97b8a0e0 --- /dev/null +++ b/src/main/resources/static/bidder-params/blis.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Blis Adapter Params", + "description": "A schema which validates params accepted by the Blis adapter", + "type": "object", + "properties": { + "spid": { + "type": "string", + "minLength": 1, + "description": "Unique supply partner ID provided by Blis" + } + }, + "required": [ + "spid" + ] +} diff --git a/src/main/resources/static/bidder-params/exco.json b/src/main/resources/static/bidder-params/exco.json new file mode 100644 index 00000000000..e60e43aa8d2 --- /dev/null +++ b/src/main/resources/static/bidder-params/exco.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "The Exco Adapter Params", + "description": "A schema which validates params accepted by Exco adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "minLength": 1, + "description": "A unique account identifier provided by EX.CO." + }, + "publisherId": { + "type": "string", + "minLength": 1, + "description": "Publisher ID provided by EX.CO." + }, + "tagId": { + "type": "string", + "minLength": 1, + "description": "A unique Tag ID (supply id) identifier provided by EX.CO." + } + }, + "required": [ + "accountId", + "publisherId", + "tagId" + ] +} diff --git a/src/main/resources/static/bidder-params/sparteo.json b/src/main/resources/static/bidder-params/sparteo.json new file mode 100644 index 00000000000..ca8c072ae77 --- /dev/null +++ b/src/main/resources/static/bidder-params/sparteo.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Sparteo Params", + "type": "object", + "properties": { + "networkId": { + "type": "string", + "description": "Sparteo network ID. This information will be given to you by the Sparteo team." + }, + "custom1": { + "type": "string", + "description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore." + }, + "custom2": { + "type": "string", + "description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore." + }, + "custom3": { + "type": "string", + "description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore." + }, + "custom4": { + "type": "string", + "description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore." + }, + "custom5": { + "type": "string", + "description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore." + } + }, + "required": [ + "networkId" + ] +} \ No newline at end of file diff --git a/src/test/java/org/prebid/server/bidder/blis/BlisBidderTest.java b/src/test/java/org/prebid/server/bidder/blis/BlisBidderTest.java new file mode 100644 index 00000000000..83e5fa1224e --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/blis/BlisBidderTest.java @@ -0,0 +1,254 @@ +package org.prebid.server.bidder.blis; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +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.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.blis.ExtImpBlis; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.bidder.model.BidderError.badServerResponse; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +public class BlisBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://endpoint.com/?spid={{SupplyId}}"; + + private final BlisBidder target = new BlisBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new BlisBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Error parsing imp.ext:"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseFirstImpExtToResolveUrl() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(givenImpExt("spid1")), + imp -> imp.ext(givenImpExt("spid2"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getUri) + .isEqualTo("https://endpoint.com/?spid=spid1"); + } + + @Test + public void makeHttpRequestsShouldUseFirstImpExtToSetHeaders() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(givenImpExt("spid1")), + imp -> imp.ext(givenImpExt("spid2"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> { + assertThat(headers.get("X-Supply-Partner-Id")).isEqualTo("spid1"); + assertThat(headers.get(CONTENT_TYPE_HEADER)).isEqualTo(APPLICATION_JSON_CONTENT_TYPE); + assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE); + }); + } + + @Test + public void makeHttpRequestsShouldSetPayloadAndBody() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(givenImpExt("spid1")), + imp -> imp.ext(givenImpExt("spid2"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .satisfies(httpRequest -> { + assertThat(httpRequest.getPayload()).isEqualTo(bidRequest); + assertThat(httpRequest.getBody()).isEqualTo(jacksonMapper.encodeToBytes(bidRequest)); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid_json"); + + // 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 'invalid_json'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidWithMacrosResolved() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(1, BigDecimal.valueOf(1.23)); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + final Bid expectedBid = bannerBid.toBuilder() + .adm("adm_1.23") + .nurl("nurl_1.23") + .burl("burl_1.23") + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(expectedBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidWithMacrosResolved() throws JsonProcessingException { + // given + final Bid videoBid = givenBid(2, BigDecimal.valueOf(2.34)); + final BidderCall httpCall = givenHttpCall(givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + final Bid expectedBid = videoBid.toBuilder() + .adm("adm_2.34") + .nurl("nurl_2.34") + .burl("burl_2.34") + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(expectedBid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidWithMacrosResolved() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(4, BigDecimal.valueOf(3.45)); + final BidderCall httpCall = givenHttpCall(givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + final Bid expectedBid = nativeBid.toBuilder() + .adm("adm_3.45") + .nurl("nurl_3.45") + .burl("burl_3.45") + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(expectedBid, BidType.xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorIfMtypeIsUnsupported() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(givenBid(3, BigDecimal.ONE))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("Failed to parse media type of impression ID impId")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + final List imps = Arrays.stream(impCustomizers) + .map(BlisBidderTest::givenImp) + .toList(); + return BidRequest.builder().imp(imps).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("impId")).build(); + } + + private static ObjectNode givenImpExt(String spid) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpBlis.of(spid))); + } + + private static Bid givenBid(Integer mtype, BigDecimal price) { + return Bid.builder() + .id("bidId") + .impid("impId") + .price(price) + .mtype(mtype) + .adm("adm_${AUCTION_PRICE}") + .nurl("nurl_${AUCTION_PRICE}") + .burl("burl_${AUCTION_PRICE}") + .build(); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/connatix/ConnatixBidderTest.java b/src/test/java/org/prebid/server/bidder/connatix/ConnatixBidderTest.java index 4440063c436..f24dec142fb 100644 --- a/src/test/java/org/prebid/server/bidder/connatix/ConnatixBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/connatix/ConnatixBidderTest.java @@ -349,6 +349,33 @@ public void makeHttpRequestsShouldExcludeDataCenterWhenUserIdIsMissing() { assertThat(result.getValue()).extracting(HttpRequest::getUri).containsOnly(CONNATIX_ENDPOINT); } + @Test + public void makeHttpRequestsShouldIncludeGpidWhenPresent() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.valueToTree(ExtImpConnatix.of("placementId", null))); + impExt.put("gpid", "test-gpid"); + + final BidRequest bidRequest = givenBidRequest( + UnaryOperator.identity(), + givenImp(impBuilder -> impBuilder.ext(impExt))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ObjectNode expectedExt = mapper.createObjectNode(); + expectedExt.set("connatix", mapper.valueToTree(ExtImpConnatix.of("placementId", null))); + expectedExt.put("gpid", "test-gpid"); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(expectedExt); + } + @Test public void makeBidsShouldErrorIfResponseBodyCannotBeParsed() { // given diff --git a/src/test/java/org/prebid/server/bidder/exco/ExcoBidderTest.java b/src/test/java/org/prebid/server/bidder/exco/ExcoBidderTest.java new file mode 100644 index 00000000000..9efee9b11ca --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/exco/ExcoBidderTest.java @@ -0,0 +1,259 @@ +package org.prebid.server.bidder.exco; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +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.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.exco.ExtImpExco; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.List; +import java.util.function.UnaryOperator; + +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.prebid.server.bidder.model.BidderError.badServerResponse; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class ExcoBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + + private final ExcoBidder target = new ExcoBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new ExcoBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Invalid imp.ext for impression impId"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldModifyImpCorrectly() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.tagid("originalTagId").ext(givenImpExt("pub", "tag"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getTagid) + .containsOnly("tag"); + } + + @Test + public void makeHttpRequestsShouldModifyAppAndSiteCorrectly() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(givenImpExt("pub", "tag"))) + .toBuilder() + .site(Site.builder().publisher(Publisher.builder().id("origPub").build()).build()) + .app(App.builder().publisher(Publisher.builder().id("origPub").build()).build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getApp, BidRequest::getSite) + .containsOnly(tuple( + App.builder().publisher(Publisher.builder().id("pub").build()).build(), + Site.builder().publisher(Publisher.builder().id("pub").build()).build())); + } + + @Test + public void makeHttpRequestsShouldModifyAppAndSiteCorrectlyWhenPublisherIsAbsent() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(givenImpExt("pub", "tag"))) + .toBuilder() + .site(Site.builder().build()) + .app(App.builder().build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getApp, BidRequest::getSite) + .containsOnly(tuple( + App.builder().publisher(Publisher.builder().id("pub").build()).build(), + Site.builder().publisher(Publisher.builder().id("pub").build()).build())); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedUrl() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getUri) + .isEqualTo(ENDPOINT_URL); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid_json"); + + // 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 'invalid_json'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnVideoBidSuccessfully() throws JsonProcessingException { + // given + final Bid videoBid = givenBid(2); + final BidderCall httpCall = givenHttpCall(givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsOnly(BidderBid.of(videoBid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBidSuccessfully() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(1); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsOnly(BidderBid.of(bannerBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorsForUnsupportedMtypes() throws JsonProcessingException { + // given + final Bid validBid = givenBid(1); + final Bid invalidBid = givenBid(3); + final BidderCall httpCall = givenHttpCall(givenBidResponse(validBid, invalidBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("unrecognized bid_ad_type in response from exco: 3")); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(validBid, BidType.banner, "USD")); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return BidRequest.builder().imp(singletonList(givenImp(impCustomizer))).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("impId").ext(givenImpExt("pub", "tag"))).build(); + } + + private static ObjectNode givenImpExt(String publisherId, String tagId) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpExco.of(null, publisherId, tagId))); + } + + private static Bid givenBid(Integer mtype) { + return Bid.builder().id("bidId").impid("impId").price(BigDecimal.ONE).mtype(mtype).build(); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java b/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java index 054d2f2d13e..bc4568396c6 100644 --- a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java @@ -1,6 +1,8 @@ package org.prebid.server.bidder.pubmatic; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; @@ -17,6 +19,7 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.prebid.server.VertxTest; import org.prebid.server.bidder.model.BidderBid; @@ -38,6 +41,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; 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.ExtRequestPrebidAlternateBidderCodes; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAlternateBidderCodesBidder; import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmatic; import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmaticKeyVal; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; @@ -50,6 +55,7 @@ import java.math.BigDecimal; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.UnaryOperator; import static java.util.Arrays.asList; @@ -214,6 +220,175 @@ public void makeHttpRequestsShouldReturnBidRequestExtIfAcatFieldIsValidAndTrimWh .containsExactly(expectedExtRequest); } + @Test + public void makeHttpRequestsShouldReturnAllowedBidderCodeWithPubmaticAdded() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic", + ExtRequestPrebidAlternateBidderCodesBidder.of(true, Set.of("bidder1", "bidder2"))))) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("pubmatic", "bidder1", "bidder2"); + } + + @Test + public void makeHttpRequestsShouldReturnOnlyPubmaticWhenPubmaticCodesAreDisabled() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic", + ExtRequestPrebidAlternateBidderCodesBidder.of(false, Set.of("bidder1", "bidder2"))))) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("pubmatic"); + } + + @Test + public void makeHttpRequestsShouldReturnOnlyAllWhenPubmaticCodesAreAbsent() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic", + ExtRequestPrebidAlternateBidderCodesBidder.of(true, null)))) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("all"); + } + + @Test + public void makeHttpRequestsShouldReturnOnlyAllWhenPubmaticCodesHasWildcard() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic", + ExtRequestPrebidAlternateBidderCodesBidder.of(true, Set.of("*"))))) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("all"); + } + + @Test + public void makeHttpRequestsShouldReturnOnlyPubmaticWhenPubmaticCodesAreAbsent() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of())) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("pubmatic"); + } + + @Test + public void makeHttpRequestsShouldReturnOnlyPubmaticWhenAlternateBidderCodesAreDisabled() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(false, Map.of())) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders")) + .asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class)) + .extracting(JsonNode::asText) + .containsOnly("pubmatic"); + } + + @Test + public void makeHttpRequestsShouldReturnNothingWhenAlternateBidderCodeIsAbsent() { + // given + final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder() + .alternateBidderCodes(null) + .build()); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(ext -> ext.getProperty("marketplace")) + .isNull(); + } + @Test public void makeHttpRequestsShouldMergeWrappersFromImpAndBidRequestExt() { // given @@ -544,6 +719,7 @@ public void makeHttpRequestsShouldAddImpExtAddUnitKeyKeyWordFromDataAdSlotIfAdSe ExtImpPubmatic.builder().build(), extData, null, + null, null ))) .build())) @@ -577,6 +753,7 @@ public void makeHttpRequestsShouldAddImpExtAddUnitKeyKeyWordFromAdServerAdSlotIf ExtImpPubmatic.builder().build(), extData, null, + null, null ))) .build())) @@ -611,6 +788,7 @@ public void makeHttpRequestsShouldAddImpExtWithKeyValWithDctrAndExtDataExceptFor ExtImpPubmatic.builder().dctr("dctr").build(), extData, null, + null, null ))) .build())) @@ -648,6 +826,7 @@ public void makeHttpRequestsShouldAddImpExtWithKeyValWithExtDataWhenDctrIsAbsent ExtImpPubmatic.builder().dctr(null).build(), extData, null, + null, null ))) .build())) @@ -675,7 +854,7 @@ public void makeHttpRequestsShouldAddImpExtAddAE() { .id("123") .banner(Banner.builder().build()) .ext(mapper.valueToTree(PubmaticBidderImpExt.of( - ExtImpPubmatic.builder().build(), null, 1, null))) + ExtImpPubmatic.builder().build(), null, 1, null, null))) .build())) .build(); @@ -700,7 +879,7 @@ public void makeHttpRequestsShouldAddImpExtAddGpId() { .id("123") .banner(Banner.builder().build()) .ext(mapper.valueToTree(PubmaticBidderImpExt.of( - ExtImpPubmatic.builder().build(), null, null, "gpId"))) + ExtImpPubmatic.builder().build(), null, null, "gpId", null))) .build())) .build(); @@ -717,6 +896,34 @@ public void makeHttpRequestsShouldAddImpExtAddGpId() { .containsExactly(expectedImpExt); } + @Test + public void makeHttpRequestsShouldAddImpExtAddSkadn() { + // given + final ObjectNode skadn = mapper.createObjectNode() + .put("field1", 1) + .put("field2", "value"); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("123") + .banner(Banner.builder().build()) + .ext(mapper.valueToTree(PubmaticBidderImpExt.of( + ExtImpPubmatic.builder().build(), null, null, null, skadn))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ObjectNode expectedImpExt = mapper.createObjectNode().set("skadn", skadn); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(expectedImpExt); + } + @Test public void makeHttpRequestsShouldSetImpExtFromKeywordsSkippingKeysWithEmptyValues() { // given diff --git a/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java b/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java index d31213136fb..4565c7581de 100644 --- a/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java @@ -108,12 +108,12 @@ public void makeHttpRequestsShouldUpdateSiteObjectIfPresent() { } @Test - public void makeHttpRequestsShouldCreateRequestForEveryValidImp() { + public void makeHttpRequestsShouldCreateSingleRequest() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(Arrays.asList(givenImp(identity()), - givenImp(impBuilder -> impBuilder.id("456")) - )) + .imp(Arrays.asList( + givenImp(impBuilder -> impBuilder.id("123")), + givenImp(impBuilder -> impBuilder.id("456")))) .build(); // when @@ -121,23 +121,22 @@ public void makeHttpRequestsShouldCreateRequestForEveryValidImp() { // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) - .flatExtracting(Imp::getId) + .extracting(Imp::getId) .containsExactly("123", "456"); } @Test - public void makeHttpRequestsShouldCreateRequestForValidImpAndSaveErrorForInvalid() { + public void makeHttpRequestsShouldCreateSingleRequestWithValidImpsOnly() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(Arrays.asList(givenImp(impBuilder -> impBuilder.id("456")), + .imp(Arrays.asList(givenImp(impBuilder -> impBuilder.id("123")), Imp.builder() .id("invalidImp") .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) - .build() - )) + .build())) .build(); // when @@ -146,11 +145,11 @@ public void makeHttpRequestsShouldCreateRequestForValidImpAndSaveErrorForInvalid // then assertThat(result.getErrors()) .containsExactly(BidderError.badInput("Error parsing smartadserverExt parameters")); - assertThat(result.getValue()) + assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) - .flatExtracting(Imp::getId) - .containsExactly("456"); + .extracting(Imp::getId) + .containsExactly("123"); } @Test diff --git a/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java new file mode 100644 index 00000000000..2920f94adbd --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java @@ -0,0 +1,919 @@ +package org.prebid.server.bidder.sparteo; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.junit.jupiter.api.Test; +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.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +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; + +public class SparteoBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.sparteo.com/endpoint"; + private final SparteoBidder sparteoBidder = new SparteoBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + // when and then + assertThatIllegalArgumentException() + .isThrownBy(() -> new SparteoBidder("invalid_url", jacksonMapper)) + .withMessage("URL supplied is not valid: invalid_url"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpExtIsInvalid() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, "invalid"))))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> assertThat(error.getMessage()) + .startsWith("ignoring imp id=impId, error processing ext: invalid imp.ext")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenAllImpsAreInvalid() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp1").ext(mapper.valueToTree(ExtPrebid.of(null, "invalid"))))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + } + + @Test + public void makeHttpRequestsShouldReturnPartialResultWhenSomeImpsAreInvalid() { + // given + final ObjectNode validExt = mapper.createObjectNode(); + validExt.set("bidder", mapper.createObjectNode().put("key", "value")); + + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp1").ext(mapper.valueToTree(ExtPrebid.of(null, "invalid")))), + givenImp(imp -> imp.id("imp2").ext(validExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> assertThat(error.getMessage()).startsWith("ignoring imp id=imp1")); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .extracting((JsonNode ext) -> ext.at("/sparteo/params/key").asText()) + .containsExactly("", "value"); + } + + @Test + public void makeHttpRequestsShouldSetNetworkIdOnSitePublisherExtWhenPresentInImp() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode() + .put("networkId", "testNetworkId") + .put("customParam", "customValue")); + + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().publisher(Publisher.builder().build()).build()), + givenImp(imp -> imp.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getMethod, HttpRequest::getUri) + .containsExactly(tuple(HttpMethod.POST, ENDPOINT_URL)); + + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .allSatisfy(ext -> { + assertThat(ext.at("/sparteo/params/networkId").asText()).isEqualTo("testNetworkId"); + assertThat(ext.at("/sparteo/params/customParam").asText()).isEqualTo("customValue"); + assertThat(ext.has("bidder")).isFalse(); + }); + + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .extracting(Publisher::getExt) + .extracting(ext -> ((ExtPublisher) ext).getProperties().get("params").get("networkId").asText()) + .containsExactly("testNetworkId"); + } + + @Test + public void makeHttpRequestsShouldUseFirstNetworkIdWhenMultipleImpsDefineIt() { + // given + final ObjectNode impExt1 = mapper.createObjectNode(); + impExt1.set("bidder", mapper.createObjectNode().put("networkId", "id1")); + final ObjectNode impExt2 = mapper.createObjectNode(); + impExt2.set("bidder", mapper.createObjectNode().put("networkId", "id2")); + + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().publisher(Publisher.builder().build()).build()), + givenImp(imp -> imp.id("imp1").ext(impExt1)), + givenImp(imp -> imp.id("imp2").ext(impExt2))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .extracting(Publisher::getExt) + .extracting(ext -> ((ExtPublisher) ext).getProperties().get("params").get("networkId").asText()) + .containsExactly("id1"); + } + + @Test + public void makeHttpRequestsShouldOverwriteSparteoParamsWithBidderParamsOnConflict() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("conflictingParam", "bidderValue")); + final ObjectNode sparteoNode = impExt.putObject("sparteo"); + sparteoNode.putObject("params").put("conflictingParam", "sparteoValue"); + + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .extracting((JsonNode ext) -> ext.at("/sparteo/params/conflictingParam").asText()) + .containsExactly("bidderValue"); + } + + @Test + public void makeHttpRequestsShouldHandleRequestWithoutSiteOrPublisher() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); + + final BidRequest bidRequestNoSite = givenBidRequest(request -> request.site(null), + givenImp(imp -> imp.ext(impExt))); + + final BidRequest bidRequestNoPublisher = givenBidRequest( + request -> request.site(Site.builder().publisher(null).build()), + givenImp(imp -> imp.ext(impExt))); + + // when + final Result>> resultNoSite = sparteoBidder.makeHttpRequests(bidRequestNoSite); + final Result>> resultNoPublisher = + sparteoBidder.makeHttpRequests(bidRequestNoPublisher); + + // then + assertThat(resultNoSite.getErrors()).isEmpty(); + assertThat(resultNoSite.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .containsNull(); + + assertThat(resultNoPublisher.getErrors()).isEmpty(); + assertThat(resultNoPublisher.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .containsNull(); + } + + @Test + public void makeHttpRequestsShouldMergeNetworkIdIntoExistingPublisherExtParams() throws JsonProcessingException { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); + + final ObjectNode publisherExtNode = mapper.createObjectNode(); + publisherExtNode.putObject("params").put("existingParam", "existingValue"); + final ExtPublisher extPublisher = mapper.convertValue(publisherExtNode, ExtPublisher.class); + + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().publisher( + Publisher.builder().ext(extPublisher).build()).build()), + givenImp(imp -> imp.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .extracting(Publisher::getExt) + .extracting(ext -> ((ExtPublisher) ext).getProperties().get("params")) + .allSatisfy(params -> { + assertThat(params.get("networkId").asText()).isEqualTo("testNetworkId"); + assertThat(params.get("existingParam").asText()).isEqualTo("existingValue"); + }); + } + + @Test + public void makeHttpRequestsShouldAddParamsToPublisherExtWhenExtExistsWithoutParams() + throws JsonProcessingException { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); + + final ObjectNode publisherExtJson = mapper.createObjectNode(); + publisherExtJson.put("otherField", "otherValue"); + final ExtPublisher extPublisher = mapper.convertValue(publisherExtJson, ExtPublisher.class); + + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().publisher( + Publisher.builder().ext(extPublisher).build()).build()), + givenImp(imp -> imp.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .extracting(Publisher::getExt) + .extracting(ext -> ((ExtPublisher) ext).getProperties()) + .allSatisfy(properties -> { + assertThat(properties.get("params").get("networkId").asText()).isEqualTo("testNetworkId"); + assertThat(properties.get("otherField").asText()).isEqualTo("otherValue"); + }); + } + + @Test + public void makeHttpRequestsShouldCreateEmptyParamsWhenBidderExtIsEmpty() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode()); + impExt.putObject("sparteo"); + + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .allSatisfy(ext -> { + assertThat(ext.at("/sparteo")).isInstanceOf(ObjectNode.class); + assertThat(ext.at("/sparteo/params")).isInstanceOf(ObjectNode.class); + assertThat(ext.at("/sparteo/params").size()).isZero(); + }); + } + + @Test + public void makeHttpRequestsShouldNotAddParamsWhenNetworkIdIsMissing() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("otherParam", "value")); + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().publisher(Publisher.builder().id("pub1").build()).build()), + givenImp(imp -> imp.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .allSatisfy(publisher -> { + final ExtPublisher publisherExt = (ExtPublisher) publisher.getExt(); + if (publisherExt == null || publisherExt.getProperties() == null) { + assertThat(publisherExt).isNull(); + } else { + assertThat(publisherExt.getProperties()).doesNotContainKey("params"); + } + }); + } + + @Test + public void makeHttpRequestsShouldNotAddNetworkIdWhenItIsNullInExt() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + final ObjectNode bidderNode = mapper.createObjectNode(); + bidderNode.putNull("networkId"); + impExt.set("bidder", bidderNode); + + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().publisher(Publisher.builder().id("pub1").build()).build()), + givenImp(imp -> imp.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .allSatisfy(publisher -> { + final ExtPublisher publisherExt = (ExtPublisher) publisher.getExt(); + if (publisherExt != null && publisherExt.getProperties() != null + && publisherExt.getProperties().containsKey("params")) { + assertThat(publisherExt.getProperties().get("params")).isInstanceOf(ObjectNode.class); + assertThat(((ObjectNode) + publisherExt.getProperties().get("params")).has("networkId")).isFalse(); + } + }); + } + + @Test + public void makeHttpRequestsShouldNotModifyPublisherExtWhenNetworkIdIsMissing() throws JsonProcessingException { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("someOtherParam", "someValue")); + + final ObjectNode publisherExtJson = mapper.createObjectNode(); + publisherExtJson.put("existing", "value"); + final ExtPublisher originalPublisherExt = mapper.convertValue(publisherExtJson, ExtPublisher.class); + + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().publisher( + Publisher.builder().ext(originalPublisherExt).build()).build()), + givenImp(imp -> imp.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .extracting(Publisher::getExt) + .extracting(ext -> ((ExtPublisher) ext).getProperties()) + .allSatisfy(properties -> { + assertThat(properties.get("existing").asText()).isEqualTo("value"); + assertThat(properties).doesNotContainKey("params"); + }); + } + + @Test + public void makeHttpRequestsShouldOverwriteInvalidSparteoExtWhenBidderExtIsValid() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("param", "value")); + impExt.put("sparteo", "this_is_a_string"); + + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .allSatisfy(ext -> { + assertThat(ext.at("/sparteo")).isInstanceOf(ObjectNode.class); + assertThat(ext.at("/sparteo/params/param").asText()).isEqualTo("value"); + }); + } + + @Test + public void makeHttpRequestsShouldReturnEmptyResultWhenRequestHasNoImps() { + // given + final BidRequest bidRequest = givenBidRequest(request -> request.imp(Collections.emptyList())); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseStatusIs204() { + // given + final BidderCall httpCall = BidderCall.succeededHttp( + HttpRequest.builder().payload(givenBidRequest()).build(), + HttpResponse.of(204, null, ""), + null); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()) + .startsWith("Failed to decode: No content to map due to end-of-input"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseStatusIsNot200Or204() { + // given + final BidderCall httpCall = BidderCall.succeededHttp( + HttpRequest.builder().payload(givenBidRequest()).build(), + HttpResponse.of(400, null, "Bad Request"), + null); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getValue()).isEmpty(); + 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 'Bad'"); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyIsInvalidJson() { + // given + final BidderCall httpCall = BidderCall.succeededHttp( + HttpRequest.builder().payload(givenBidRequest()).build(), + HttpResponse.of(200, null, "invalid_json"), + null); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getValue()).isEmpty(); + 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 'invalid_json'"); + }); + } + + @Test + public void makeBidsShouldReturnEmptyResultWhenBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = BidderCall.succeededHttp( + HttpRequest.builder().payload(givenBidRequest()).build(), + HttpResponse.of(400, null, "null"), + null); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyResultWhenBidResponseHasNoSeatBids() + throws JsonProcessingException { + // given + final BidResponse bidResponse = BidResponse.builder().seatbid(Collections.emptyList()).build(); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidWhenMediaTypeIsBanner() throws JsonProcessingException { + // given + final Bid bid = givenBid(builder -> builder.impid("imp1").price(BigDecimal.valueOf(1.23)).adm("adm-banner"), + BidType.banner.getName()); + final BidResponse bidResponse = givenBidResponse(bid, "EUR"); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(bid.toBuilder().mtype(1).build(), BidType.banner, "EUR")); + } + + @Test + public void makeBidsShouldReturnVideoBidWhenMediaTypeIsVideo() throws JsonProcessingException { + // given + final Bid bid = givenBid(builder -> + builder.impid("imp2").price(BigDecimal.valueOf(2.34)).adm("adm-video"), + BidType.video.getName()); + final BidResponse bidResponse = givenBidResponse(bid, "EUR"); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(bid.toBuilder().mtype(2).build(), BidType.video, "EUR")); + } + + @Test + public void makeBidsShouldReturnNativeBidWhenMediaTypeIsNative() throws JsonProcessingException { + // given + final Bid bid = givenBid(builder -> + builder.impid("imp3").price(BigDecimal.valueOf(3.45)).adm("adm-native"), + BidType.xNative.getName()); + final BidResponse bidResponse = givenBidResponse(bid, "EUR"); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(bid.toBuilder().mtype(4).build(), BidType.xNative, "EUR")); + } + + @Test + public void makeBidsShouldReturnErrorForUnsupportedMediaTypeAndProcessOthers() throws JsonProcessingException { + // given + final Bid audioBid = givenBid(builder -> + builder.impid("impAudio").price(BigDecimal.ONE), + BidType.audio.getName()); + final Bid bannerBid = givenBid(builder -> + builder.impid("impBanner").price(BigDecimal.valueOf(2.0)), + BidType.banner.getName()); + final BidResponse bidResponse = givenBidResponse(List.of(audioBid, bannerBid), "EUR"); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Audio bid type not supported by this adapter for impression id: impAudio"); + + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .containsExactly(bannerBid.toBuilder().mtype(1).build()); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidExtIsNull() throws JsonProcessingException { + // given + final Bid bid = givenBid(builder -> builder.impid("imp1").ext(null), null); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); + } + + @Test + public void makeBidsShouldReturnErrorWhenPrebidIsMissingInBidExt() throws JsonProcessingException { + // given + final Bid bid = givenBid(builder -> builder.impid("imp1").ext(mapper.createObjectNode()), null); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); + } + + @Test + public void makeBidsShouldReturnErrorWhenPrebidTypeIsMissingInBidExt() throws JsonProcessingException { + // given + final Bid bid = givenBid(builder -> builder.impid("imp1").ext(createBidExtWithEmptyPrebid()), null); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); + } + + @Test + public void makeBidsShouldReturnErrorWhenPrebidCannotBeParsed() throws JsonProcessingException { + // given + final ObjectNode malformedExt = mapper.createObjectNode(); + malformedExt.putArray("prebid"); + final Bid bid = givenBid(builder -> builder.impid("imp1").ext(malformedExt), null); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); + } + + @Test + public void makeBidsShouldReturnErrorWhenPrebidTypeIsUnsupported() throws JsonProcessingException { + // given + final Bid bid = givenBid(builder -> builder.impid("imp1"), "unknown-type"); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); + } + + @Test + public void makeBidsShouldProcessValidBidsWhenSeatBidContainsNulls() throws JsonProcessingException { + // given + final Bid validBid = givenBid(builder -> + builder.impid("validImp").price(BigDecimal.ONE), + BidType.banner.getName()); + final List bids = new ArrayList<>(); + bids.add(null); + bids.add(validBid); + + final BidResponse bidResponse = givenBidResponse(bids, "USD"); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .containsExactly(validBid.toBuilder().mtype(1).build()); + } + + @Test + public void makeBidsShouldCorrectlyProcessMultipleBidsAndSeatBids() throws JsonProcessingException { + // given + final Bid bid1 = givenBid(builder -> + builder.impid("imp1").price(BigDecimal.valueOf(1.0)), + BidType.banner.getName()); + final Bid bid2 = givenBid(builder -> + builder.impid("imp2").price(BigDecimal.valueOf(2.0)), + BidType.video.getName()); + final Bid bid3 = givenBid(builder -> + builder.impid("imp3").price(BigDecimal.valueOf(3.0)), + BidType.xNative.getName()); + + final SeatBid seatBid1 = SeatBid.builder().bid(asList(bid1, bid2)).build(); + final SeatBid seatBid2 = SeatBid.builder().bid(singletonList(bid3)).build(); + + final BidResponse bidResponse = BidResponse.builder().cur("USD").seatbid(asList(seatBid1, seatBid2)).build(); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .hasSize(3) + .extracting((BidderBid bidderBid) -> bidderBid.getBid().getImpid(), BidderBid::getType) + .containsExactlyInAnyOrder( + tuple("imp1", BidType.banner), + tuple("imp2", BidType.video), + tuple("imp3", BidType.xNative)); + } + + @Test + public void makeBidsShouldReturnErrorWhenPrebidExtIsNullNode() throws JsonProcessingException { + // given + final ObjectNode bidExtWithNullPrebid = mapper.createObjectNode(); + bidExtWithNullPrebid.set("prebid", NullNode.getInstance()); + + final Bid bid = givenBid(builder -> builder.impid("imp1").ext(bidExtWithNullPrebid), null); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); + } + + @Test + public void makeBidsShouldProcessValidSeatBidsWhenResponseContainsNulls() throws JsonProcessingException { + // given + final Bid validBid1 = givenBid(builder -> + builder.impid("validImp1").price(BigDecimal.TEN), + BidType.banner.getName()); + final Bid validBid2 = givenBid(builder -> + builder.impid("validImp2").price(BigDecimal.ONE), + BidType.banner.getName()); + + final SeatBid validSeatBid1 = SeatBid.builder().bid(singletonList(validBid1)).build(); + final SeatBid validSeatBid2 = SeatBid.builder().bid(singletonList(validBid2)).build(); + + final List seatBidsWithNull = new ArrayList<>(); + seatBidsWithNull.add(validSeatBid1); + seatBidsWithNull.add(null); + seatBidsWithNull.add(validSeatBid2); + + final BidResponse bidResponse = BidResponse.builder().cur("USD").seatbid(seatBidsWithNull).build(); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getImpid) + .containsExactlyInAnyOrder("validImp1", "validImp2"); + } + + @Test + public void makeBidsShouldReturnEmptyResultWhenSeatBidHasNullBidList() throws JsonProcessingException { + // given + final SeatBid seatBidWithNullBids = SeatBid.builder().bid(null).build(); + final BidResponse bidResponse = + BidResponse.builder().cur("USD").seatbid(singletonList(seatBidWithNullBids)).build(); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyResultWhenSeatBidHasEmptyBidList() throws JsonProcessingException { + // given + final SeatBid seatBidWithEmptyBids = SeatBid.builder().bid(Collections.emptyList()).build(); + final BidResponse bidResponse = + BidResponse.builder().cur("USD").seatbid(singletonList(seatBidWithEmptyBids)).build(); + final BidderCall httpCall = + givenHttpCall(givenBidRequest(), bidResponse); + + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + private BidRequest givenBidRequest(UnaryOperator customizer, Imp... imps) { + return customizer.apply(BidRequest.builder().imp(asList(imps))).build(); + } + + private BidRequest givenBidRequest(Imp... imps) { + return givenBidRequest(identity(), imps); + } + + private Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("impId")).build(); + } + + private Bid givenBid(UnaryOperator bidCustomizer, String mediaType) { + final Bid.BidBuilder builder = Bid.builder(); + bidCustomizer.apply(builder); + + if (builder.build().getExt() == null && mediaType != null) { + builder.ext(createBidExtWithType(mediaType)); + } + + return builder.build(); + } + + private BidResponse givenBidResponse(List bids, String currency) { + return BidResponse.builder() + .cur(currency) + .seatbid(singletonList(SeatBid.builder().bid(bids).build())) + .build(); + } + + private BidResponse givenBidResponse(Bid bid, String currency) { + return givenBidResponse(singletonList(bid), currency); + } + + private BidderCall givenHttpCall(BidRequest bidRequest, BidResponse bidResponse) { + try { + final String body = mapper.writeValueAsString(bidResponse); + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize BidResponse in test setup", e); + } + } + + private ObjectNode createBidExtWithType(String bidType) { + final ObjectNode bidExt = mapper.createObjectNode(); + final ObjectNode prebidNode = mapper.createObjectNode(); + prebidNode.put("type", bidType); + bidExt.set("prebid", prebidNode); + return bidExt; + } + + private ObjectNode createBidExtWithEmptyPrebid() { + final ObjectNode bidExt = mapper.createObjectNode(); + bidExt.set("prebid", mapper.createObjectNode()); + return bidExt; + } +} 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 8c167590e4f..033b8236d34 100644 --- a/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java @@ -512,7 +512,8 @@ public void makeBidsShouldReplacePriceMacroInNurlAndAdmWithBidPrice() throws Jso .impid("123") .price(BigDecimal.valueOf(1.23)) .nurl("http://example.com/nurl?price=${AUCTION_PRICE}") - .adm("
Price: ${AUCTION_PRICE}
"))); + .adm("
Price: ${AUCTION_PRICE}
") + .burl("https://adsrvr.org/feedback/xxx?wp=${AUCTION_PRICE}¶m2=xyz"))); // when final Result> result = target.makeBids(httpCall, null); @@ -521,8 +522,9 @@ public void makeBidsShouldReplacePriceMacroInNurlAndAdmWithBidPrice() throws Jso assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(BidderBid::getBid) - .extracting(Bid::getNurl, Bid::getAdm, Bid::getPrice) - .containsOnly(tuple("http://example.com/nurl?price=1.23", "
Price: 1.23
", BigDecimal.valueOf(1.23))); + .extracting(Bid::getNurl, Bid::getAdm, Bid::getBurl, Bid::getPrice) + .containsOnly(tuple("http://example.com/nurl?price=1.23", "
Price: 1.23
", + "https://adsrvr.org/feedback/xxx?wp=1.23¶m2=xyz", BigDecimal.valueOf(1.23))); } @Test @@ -534,7 +536,8 @@ public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsNull() throws J .impid("123") .price(null) .nurl("http://example.com/nurl?price=${AUCTION_PRICE}") - .adm("
Price: ${AUCTION_PRICE}
"))); + .adm("
Price: ${AUCTION_PRICE}
") + .burl("https://adsrvr.org/feedback/xxx?wp=${AUCTION_PRICE}¶m2=xyz"))); // when final Result> result = target.makeBids(httpCall, null); @@ -543,8 +546,9 @@ public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsNull() throws J assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(BidderBid::getBid) - .extracting(Bid::getNurl, Bid::getAdm) - .containsOnly(tuple("http://example.com/nurl?price=0", "
Price: 0
")); + .extracting(Bid::getNurl, Bid::getAdm, Bid::getBurl) + .containsOnly(tuple("http://example.com/nurl?price=0", "
Price: 0
", + "https://adsrvr.org/feedback/xxx?wp=0¶m2=xyz")); } @Test @@ -556,7 +560,8 @@ public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsZero() throws J .impid("123") .price(BigDecimal.ZERO) .nurl("http://example.com/nurl?price=${AUCTION_PRICE}") - .adm("
Price: ${AUCTION_PRICE}
"))); + .adm("
Price: ${AUCTION_PRICE}
") + .burl("https://adsrvr.org/feedback/xxx?wp=${AUCTION_PRICE}¶m2=xyz"))); // when final Result> result = target.makeBids(httpCall, null); @@ -565,8 +570,9 @@ public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsZero() throws J assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(BidderBid::getBid) - .extracting(Bid::getNurl, Bid::getAdm) - .containsOnly(tuple("http://example.com/nurl?price=0", "
Price: 0
")); + .extracting(Bid::getNurl, Bid::getAdm, Bid::getBurl) + .containsOnly(tuple("http://example.com/nurl?price=0", "
Price: 0
", + "https://adsrvr.org/feedback/xxx?wp=0¶m2=xyz")); } @Test @@ -578,7 +584,8 @@ public void makeBidsShouldReplacePriceMacroInNurlOnlyWhenAdmDoesNotContainMacro( .impid("123") .price(BigDecimal.valueOf(5.67)) .nurl("http://example.com/nurl?price=${AUCTION_PRICE}") - .adm("
No macro here
"))); + .adm("
No macro here
") + .burl("http://example.com/burl"))); // when final Result> result = target.makeBids(httpCall, null); @@ -587,8 +594,9 @@ public void makeBidsShouldReplacePriceMacroInNurlOnlyWhenAdmDoesNotContainMacro( assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(BidderBid::getBid) - .extracting(Bid::getNurl, Bid::getAdm) - .containsOnly(tuple("http://example.com/nurl?price=5.67", "
No macro here
")); + .extracting(Bid::getNurl, Bid::getAdm, Bid::getBurl) + .containsOnly(tuple("http://example.com/nurl?price=5.67", "
No macro here
", + "http://example.com/burl")); } @Test @@ -600,7 +608,8 @@ public void makeBidsShouldReplacePriceMacroInAdmOnlyWhenNurlDoesNotContainMacro( .impid("123") .price(BigDecimal.valueOf(8.90)) .nurl("http://example.com/nurl") - .adm("
Price: ${AUCTION_PRICE}
"))); + .adm("
Price: ${AUCTION_PRICE}
") + .burl("http://example.com/burl"))); // when final Result> result = target.makeBids(httpCall, null); @@ -609,8 +618,9 @@ public void makeBidsShouldReplacePriceMacroInAdmOnlyWhenNurlDoesNotContainMacro( assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(BidderBid::getBid) - .extracting(Bid::getNurl, Bid::getAdm) - .containsOnly(tuple("http://example.com/nurl", "
Price: 8.9
")); + .extracting(Bid::getNurl, Bid::getAdm, Bid::getBurl) + .containsOnly(tuple("http://example.com/nurl", "
Price: 8.9
", + "http://example.com/burl")); } @Test @@ -622,7 +632,8 @@ public void makeBidsShouldNotReplacePriceMacroWhenNurlAndAdmDoNotContainMacro() .impid("123") .price(BigDecimal.valueOf(12.34)) .nurl("http://example.com/nurl") - .adm("
No macro
"))); + .adm("
No macro
") + .burl("http://example.com/burl"))); // when final Result> result = target.makeBids(httpCall, null); @@ -631,8 +642,9 @@ public void makeBidsShouldNotReplacePriceMacroWhenNurlAndAdmDoNotContainMacro() assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(BidderBid::getBid) - .extracting(Bid::getNurl, Bid::getAdm) - .containsOnly(tuple("http://example.com/nurl", "
No macro
")); + .extracting(Bid::getNurl, Bid::getAdm, Bid::getBurl) + .containsOnly(tuple("http://example.com/nurl", "
No macro
", + "http://example.com/burl")); } @Test @@ -644,7 +656,8 @@ public void makeBidsShouldHandleNullNurlAndAdm() throws JsonProcessingException .impid("123") .price(BigDecimal.valueOf(15.00)) .nurl(null) - .adm(null))); + .adm(null) + .burl(null))); // when final Result> result = target.makeBids(httpCall, null); @@ -653,8 +666,8 @@ public void makeBidsShouldHandleNullNurlAndAdm() throws JsonProcessingException assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(BidderBid::getBid) - .extracting(Bid::getNurl, Bid::getAdm) - .containsOnly(tuple(null, null)); + .extracting(Bid::getNurl, Bid::getAdm, Bid::getBurl) + .containsOnly(tuple(null, null, null)); } @Test @@ -666,7 +679,8 @@ public void makeBidsShouldReplaceMultiplePriceMacrosInSameField() throws JsonPro .impid("123") .price(BigDecimal.valueOf(9.99)) .nurl("http://example.com/nurl?price=${AUCTION_PRICE}&backup_price=${AUCTION_PRICE}") - .adm("
Price: ${AUCTION_PRICE}, Fallback: ${AUCTION_PRICE}
"))); + .adm("
Price: ${AUCTION_PRICE}, Fallback: ${AUCTION_PRICE}
") + .burl("https://adsrvr.org/feedback/xxx?wp=${AUCTION_PRICE}&backup_wp=${AUCTION_PRICE}¶m2=xyz"))); // when final Result> result = target.makeBids(httpCall, null); @@ -675,8 +689,11 @@ public void makeBidsShouldReplaceMultiplePriceMacrosInSameField() throws JsonPro assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(BidderBid::getBid) - .extracting(Bid::getNurl, Bid::getAdm) - .containsOnly(tuple("http://example.com/nurl?price=9.99&backup_price=9.99", "
Price: 9.99, Fallback: 9.99
")); + .extracting(Bid::getNurl, Bid::getAdm, Bid::getBurl) + .containsOnly(tuple( + "http://example.com/nurl?price=9.99&backup_price=9.99", + "
Price: 9.99, Fallback: 9.99
", + "https://adsrvr.org/feedback/xxx?wp=9.99&backup_wp=9.99¶m2=xyz")); } @Test @@ -688,7 +705,8 @@ public void makeBidsShouldHandleLargeDecimalPrices() throws JsonProcessingExcept .impid("123") .price(new BigDecimal("123456789.123456789")) .nurl("http://example.com/nurl?price=${AUCTION_PRICE}") - .adm("
Price: ${AUCTION_PRICE}
"))); + .adm("
Price: ${AUCTION_PRICE}
") + .burl("https://adsrvr.org/feedback/xxx?wp=${AUCTION_PRICE}¶m2=xyz"))); // when final Result> result = target.makeBids(httpCall, null); @@ -697,8 +715,37 @@ public void makeBidsShouldHandleLargeDecimalPrices() throws JsonProcessingExcept assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(BidderBid::getBid) - .extracting(Bid::getNurl, Bid::getAdm) - .containsOnly(tuple("http://example.com/nurl?price=123456789.123456789", "
Price: 123456789.123456789
")); + .extracting(Bid::getNurl, Bid::getAdm, Bid::getBurl) + .containsOnly(tuple( + "http://example.com/nurl?price=123456789.123456789", + "
Price: 123456789.123456789
", + "https://adsrvr.org/feedback/xxx?wp=123456789.123456789¶m2=xyz")); + } + + @Test + public void makeBidsShouldReplacePriceMacroInBurlIfNurlAndAdmDoNotContainMacro() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder + .mtype(1) + .impid("123") + .price(BigDecimal.valueOf(7.77)) + .nurl("http://example.com/nurl") + .adm("
No macro
") + .burl("https://adsrvr.org/feedback/xxx?wp=${AUCTION_PRICE}¶m2=xyz"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm, Bid::getBurl) + .containsOnly(tuple( + "http://example.com/nurl", + "
No macro
", + "https://adsrvr.org/feedback/xxx?wp=7.77¶m2=xyz")); } private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { diff --git a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java index c230df8f9ca..c33b0c9835b 100644 --- a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java @@ -278,6 +278,7 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio .w(728) .h(90) .adm(adm) + .adomain(singletonList("yieldlab")) .build(), BidType.banner, "EUR"); diff --git a/src/test/java/org/prebid/server/it/BlisTest.java b/src/test/java/org/prebid/server/it/BlisTest.java new file mode 100644 index 00000000000..4b7403a4d51 --- /dev/null +++ b/src/test/java/org/prebid/server/it/BlisTest.java @@ -0,0 +1,32 @@ +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 BlisTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheBlisBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/blis-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/blis/test-blis-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/blis/test-blis-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/blis/test-auction-blis-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/blis/test-auction-blis-response.json", response, singletonList("blis")); + } +} diff --git a/src/test/java/org/prebid/server/it/ExcoTest.java b/src/test/java/org/prebid/server/it/ExcoTest.java new file mode 100644 index 00000000000..0515bef4e07 --- /dev/null +++ b/src/test/java/org/prebid/server/it/ExcoTest.java @@ -0,0 +1,32 @@ +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 ExcoTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromExco() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/exco-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/exco/test-exco-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/exco/test-exco-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/exco/test-auction-exco-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/exco/test-auction-exco-response.json", response, singletonList("exco")); + } +} diff --git a/src/test/java/org/prebid/server/it/OmnidexTest.java b/src/test/java/org/prebid/server/it/OmnidexTest.java new file mode 100644 index 00000000000..8280a8f7e91 --- /dev/null +++ b/src/test/java/org/prebid/server/it/OmnidexTest.java @@ -0,0 +1,35 @@ +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 java.util.List; + +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; + +public class OmnidexTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromOmnidex() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/omnidex-exchange/connectionId")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/omnidex/test-omnidex-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/omnidex/test-omnidex-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/omnidex/test-auction-omnidex-request.json", + Endpoint.openrtb2_auction + ); + + // then + assertJsonEquals("openrtb2/omnidex/test-auction-omnidex-response.json", response, List.of("omnidex")); + } + +} diff --git a/src/test/java/org/prebid/server/it/SparteoTest.java b/src/test/java/org/prebid/server/it/SparteoTest.java new file mode 100644 index 00000000000..f169a486e50 --- /dev/null +++ b/src/test/java/org/prebid/server/it/SparteoTest.java @@ -0,0 +1,32 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +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 SparteoTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromSparteoBanner() throws Exception { + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/sparteo-exchange")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/sparteo/test-sparteo-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/sparteo/test-sparteo-bid-response.json")))); + + final Response response = responseFor( + "openrtb2/sparteo/test-auction-sparteo-request.json", + Endpoint.openrtb2_auction); + + assertJsonEquals( + "openrtb2/sparteo/test-auction-sparteo-response.json", + response, + singletonList("sparteo")); + } +} diff --git a/src/test/java/org/prebid/server/it/TagorasTest.java b/src/test/java/org/prebid/server/it/TagorasTest.java new file mode 100644 index 00000000000..70daa9f10fe --- /dev/null +++ b/src/test/java/org/prebid/server/it/TagorasTest.java @@ -0,0 +1,35 @@ +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 java.util.List; + +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; + +public class TagorasTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTagoras() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/tagoras-exchange/connectionId")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/tagoras/test-tagoras-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/tagoras/test-tagoras-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/tagoras/test-auction-tagoras-request.json", + Endpoint.openrtb2_auction + ); + + // then + assertJsonEquals("openrtb2/tagoras/test-auction-tagoras-response.json", response, List.of("tagoras")); + } + +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/blis/test-auction-blis-request.json b/src/test/resources/org/prebid/server/it/openrtb2/blis/test-auction-blis-request.json new file mode 100644 index 00000000000..c4dfc836e6e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/blis/test-auction-blis-request.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "blis": { + "spid": "supplyId" + } + } + } + ], + "tmax": 5000, + "regs": { + "gdpr": 0 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/blis/test-auction-blis-response.json b/src/test/resources/org/prebid/server/it/openrtb2/blis/test-auction-blis-response.json new file mode 100644 index 00000000000..6ff80a298f7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/blis/test-auction-blis-response.json @@ -0,0 +1,41 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 3.33, + "crid": "creativeId", + "adm": "adm_3.33", + "nurl": "nurl_3.33", + "burl": "burl_3.33", + "mtype": 1, + "ext": { + "origbidcpm": 3.33, + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "blis" + } + } + } + } + ], + "seat": "blis", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "blis": "{{ blis.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/blis/test-blis-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/blis/test-blis-bid-request.json new file mode 100644 index 00000000000..25dbc44bd30 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/blis/test-blis-bid-request.json @@ -0,0 +1,54 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "spid": "supplyId" + } + } + } + ], + "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": { + "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/blis/test-blis-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/blis/test-blis-bid-response.json new file mode 100644 index 00000000000..bff9fcc577a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/blis/test-blis-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId", + "adm": "adm_${AUCTION_PRICE}", + "nurl": "nurl_${AUCTION_PRICE}", + "burl": "burl_${AUCTION_PRICE}", + "mtype": 1 + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/connatix/test-banner-auction-connatix-request.json b/src/test/resources/org/prebid/server/it/openrtb2/connatix/test-banner-auction-connatix-request.json index 22d8754250d..e828461db45 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/connatix/test-banner-auction-connatix-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/connatix/test-banner-auction-connatix-request.json @@ -17,7 +17,8 @@ "ext": { "connatix": { "placementId": "some-placement-id" - } + }, + "gpid": "test-gpid" } } ], diff --git a/src/test/resources/org/prebid/server/it/openrtb2/connatix/test-banner-connatix-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/connatix/test-banner-connatix-bid-request.json index 616934ea2c5..c9007f58508 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/connatix/test-banner-connatix-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/connatix/test-banner-connatix-bid-request.json @@ -11,7 +11,8 @@ "ext": { "connatix": { "placementId": "some-placement-id" - } + }, + "gpid": "test-gpid" } } ], diff --git a/src/test/resources/org/prebid/server/it/openrtb2/exco/test-auction-exco-request.json b/src/test/resources/org/prebid/server/it/openrtb2/exco/test-auction-exco-request.json new file mode 100644 index 00000000000..9c0a339a740 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/exco/test-auction-exco-request.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "exco": { + "accountId": "accountId", + "tagId": "tagId", + "publisherId": "publisherId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/exco/test-auction-exco-response.json b/src/test/resources/org/prebid/server/it/openrtb2/exco/test-auction-exco-response.json new file mode 100644 index 00000000000..2214e823635 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/exco/test-auction-exco-response.json @@ -0,0 +1,43 @@ +{ + "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": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "exco" + } + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "exco", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "exco": "{{ exco.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/exco/test-exco-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/exco/test-exco-bid-request.json new file mode 100644 index 00000000000..bdaf3abcee0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/exco/test-exco-bid-request.json @@ -0,0 +1,60 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "tagId", + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "accountId": "accountId", + "tagId": "tagId", + "publisherId": "publisherId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "publisherId", + "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/exco/test-exco-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/exco/test-exco-bid-response.json new file mode 100644 index 00000000000..2769168e6ed --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/exco/test-exco-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", + "mtype": 1, + "h": 250, + "w": 300 + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/omnidex/test-auction-omnidex-request.json b/src/test/resources/org/prebid/server/it/openrtb2/omnidex/test-auction-omnidex-request.json new file mode 100644 index 00000000000..5691113dcef --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/omnidex/test-auction-omnidex-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "omnidex": { + "cId": "connectionId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/omnidex/test-auction-omnidex-response.json b/src/test/resources/org/prebid/server/it/openrtb2/omnidex/test-auction-omnidex-response.json new file mode 100644 index 00000000000..a3764e73d83 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/omnidex/test-auction-omnidex-response.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "omnidex" + } + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "omnidex", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "omnidex": "{{ omnidex.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/omnidex/test-omnidex-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/omnidex/test-omnidex-bid-request.json new file mode 100644 index 00000000000..eb67f5687e5 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/omnidex/test-omnidex-bid-request.json @@ -0,0 +1,54 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "cId": "connectionId" + } + } + } + ], + "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": { + "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/omnidex/test-omnidex-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/omnidex/test-omnidex-bid-response.json new file mode 100644 index 00000000000..47d4f8718ea --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/omnidex/test-omnidex-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "8048", + "mtype": 1 + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-auction-sparteo-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-auction-sparteo-request.json new file mode 100644 index 00000000000..b53ec1d1693 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-auction-sparteo-request.json @@ -0,0 +1,50 @@ +{ + "id": "request-id", + "imp": [ + { + "id": "div-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "topframe": 1 + }, + "ext": { + "tid": "df097866-eb0e-4fc1-89c8-bb4c38ec8c2e", + "sparteo": { + "networkId": "networkId" + } + } + } + ], + "site": { + "domain": "dev.sparteo.com", + "page": "https://dev.sparteo.com/page/test/", + "ref": "https://dev.sparteo.com/page/test/", + "publisher": { + "id": "sparteo", + "domain": "dev.sparteo.com", + "ext": { + "params": { + "networkId": "networkId" + } + } + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-auction-sparteo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-auction-sparteo-response.json new file mode 100644 index 00000000000..b6876245848 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-auction-sparteo-response.json @@ -0,0 +1,46 @@ +{ + "id": "request-id", + "seatbid": [ + { + "seat": "sparteo", + "group": 0, + "bid": [ + { + "id": "137dd2bd-3015-407a-ab50-384aa2c706d7", + "impid": "div-banner-id", + "price": 0.10797, + "crid": "banner_creative_test", + "mtype": 1, + "nurl": "https://dev.sparteo.com/event/sparteo/0a98964d-1cf9-4802-93e9-c59c18f497fa?d=kyGjrlVurDF3FlG8d7juZXMzNlGoZA06b75fJDG9dyF3LTd9LzPxLRHzMzFzMfwfdlifNfF9L7AlLzreYS96MTH9JTP7NRdtNRU4LS8lYTPwZTMlLRPyYTUfJDGjdEPfNfFvdzGzJWA8Y6Qob75fJDG8YSF3FirvJWe9rEHtY7xoZW49JzKuLSFsFmBiFgnfZCV7JmMwYXG9ZW1uY70tNgP9MRPfJDGwrSF3Fle9rEBzGTMBGTGCGTGCZCV7JmMwYXG9ZW1uY70tGTMBMRP9MDUyQmBeZ7UjLiZzZXG7ZXFtrC1td7VyrlVyJWGoZCQobldjLiZzZXG7ZXGUb8MjdmZjdiGoZCQoblrDYW4uZXFfJDGfdmFfNfFfJDGuhWPfNfGjYlY4ZTi6ZD8iMCMeJTQeLRKtNWL9YS8iLzK8LTPwMzL7ZRYfJDGwhWPfNfGjYlY4ZTi6ZD8iMCMeJTQeLRKtNWL9YS8iLzK8LTPwMzL7ZRYfJDGib6LfNfGtYWMOUyFsFlQfFgnfrW4qbl06bfFsFmVib75fNfG8bltub6ruFfwfrWQ9FgnfZCVzh6QvdDFsFlAfrDF3WyGzd6HtY70tYl1tbW0uZXj9hXojdj0eXzUwFfwfd6MwJWMvbWGvJXB8YlZ8rEVyZV0jbXB9kV18LDFsFlrsb7GebDFsFmBfdy8fhWPtdlA9ZV0OPV1xLRHwFfwfdlV7ZW48ZV0BXzKwLDFsFlGoZD8vdEQaT6B9hW8KXzP8Fj9sFmByZWQzFgo2Flrsb7GebKxjrlVPdlVPdlViFgnxJDGobXBLZXZjbAByZVByZWPfNgKsFlMyZWAadCVyX7jtdDF3LS58MzYsFlGobCxeYlxjX6Bjdj0obXHfNgKsFlVzrCjtYXQjZA0yZXZjbmVjFgnwJgF5NSwfrljvrXMskV0yZXZjbmVadlA9hW1fNgKsFmrobj0yYXQjFgnwJgHyMSwfbWAyZ7juX6ByZWPfNgHuLRH8JDGfhWQadEGoY7UfNgHuLX9sFlGohSF3FlQorf8fYW4uZXFfJDGwbELfNjsfLzHwkRF8LDGrJDGwbXPfNjsfYlAublVyFj9sFlVsdyF3WyFzLRB5LgUwFfwfLzHwkRKwLDFsFgLwLEcxLgUfJDFzLRB5LTUwFj9sFlVtrDF3WyGfYW4uZXFfXSwfYlQzFgobFlAikW08bCjqZSFsFlAwdC4jkEVzFfwfZ6GoZDFsFl0lrC8jZCjeFfwfb74jrCAmFfwfb6BjbmcfJDGyhXMjFfwfdmVfhWMvbfFsFmMtYXG9YWQzZXG7ZXFfJDGzbWjsZXrebmQjZDFsFmVudmVskSGrJDGtYfF3ZlAsd7UsFlZ9dyF3WyGPYXMzVCeyb6VmhKZvdl8erDGrJDGydyF3LD55MSwfYlifNfFxLzriZRGfZD9zLRK8JTPwM7KtYWF8LD9zNRQeYTGgMzH7ZRdfJDGfdDF3LD5xJDGlrDF3FjBed6MUhEGvrWrnQl0ybWA9Fm9=", + "adm": "", + "adomain": [ + "dev.sparteo.com" + ], + "w": 300, + "h": 250, + "exp": 300, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "sparteo" + } + }, + "origbidcpm": 0.10797, + "origbidcur": "USD" + } + } + ] + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "sparteo": "{{ sparteo.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-sparteo-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-sparteo-bid-request.json new file mode 100644 index 00000000000..f1dd0392632 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-sparteo-bid-request.json @@ -0,0 +1,73 @@ +{ + "id": "request-id", + "imp": [ + { + "id": "div-banner-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "topframe": 1 + }, + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "sparteo": { + "params": { + "networkId": "networkId" + } + } + } + } + ], + "site": { + "domain": "dev.sparteo.com", + "page": "https://dev.sparteo.com/page/test/", + "ref": "https://dev.sparteo.com/page/test/", + "publisher": { + "id": "sparteo", + "domain": "dev.sparteo.com", + "ext": { + "params": { + "networkId": "networkId" + } + } + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "channel": { + "name": "web" + }, + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-sparteo-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-sparteo-bid-response.json new file mode 100644 index 00000000000..2b6d330c63a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sparteo/test-sparteo-bid-response.json @@ -0,0 +1,28 @@ +{ + "id": "request-id", + "seatbid": [ + { + "bid": [ + { + "id": "137dd2bd-3015-407a-ab50-384aa2c706d7", + "impid": "div-banner-id", + "price": 0.10797, + "crid": "banner_creative_test", + "mtype": 1, + "nurl": "https://dev.sparteo.com/event/sparteo/0a98964d-1cf9-4802-93e9-c59c18f497fa?d=kyGjrlVurDF3FlG8d7juZXMzNlGoZA06b75fJDG9dyF3LTd9LzPxLRHzMzFzMfwfdlifNfF9L7AlLzreYS96MTH9JTP7NRdtNRU4LS8lYTPwZTMlLRPyYTUfJDGjdEPfNfFvdzGzJWA8Y6Qob75fJDG8YSF3FirvJWe9rEHtY7xoZW49JzKuLSFsFmBiFgnfZCV7JmMwYXG9ZW1uY70tNgP9MRPfJDGwrSF3Fle9rEBzGTMBGTGCGTGCZCV7JmMwYXG9ZW1uY70tGTMBMRP9MDUyQmBeZ7UjLiZzZXG7ZXFtrC1td7VyrlVyJWGoZCQobldjLiZzZXG7ZXGUb8MjdmZjdiGoZCQoblrDYW4uZXFfJDGfdmFfNfFfJDGuhWPfNfGjYlY4ZTi6ZD8iMCMeJTQeLRKtNWL9YS8iLzK8LTPwMzL7ZRYfJDGwhWPfNfGjYlY4ZTi6ZD8iMCMeJTQeLRKtNWL9YS8iLzK8LTPwMzL7ZRYfJDGib6LfNfGtYWMOUyFsFlQfFgnfrW4qbl06bfFsFmVib75fNfG8bltub6ruFfwfrWQ9FgnfZCVzh6QvdDFsFlAfrDF3WyGzd6HtY70tYl1tbW0uZXj9hXojdj0eXzUwFfwfd6MwJWMvbWGvJXB8YlZ8rEVyZV0jbXB9kV18LDFsFlrsb7GebDFsFmBfdy8fhWPtdlA9ZV0OPV1xLRHwFfwfdlV7ZW48ZV0BXzKwLDFsFlGoZD8vdEQaT6B9hW8KXzP8Fj9sFmByZWQzFgo2Flrsb7GebKxjrlVPdlVPdlViFgnxJDGobXBLZXZjbAByZVByZWPfNgKsFlMyZWAadCVyX7jtdDF3LS58MzYsFlGobCxeYlxjX6Bjdj0obXHfNgKsFlVzrCjtYXQjZA0yZXZjbmVjFgnwJgF5NSwfrljvrXMskV0yZXZjbmVadlA9hW1fNgKsFmrobj0yYXQjFgnwJgHyMSwfbWAyZ7juX6ByZWPfNgHuLRH8JDGfhWQadEGoY7UfNgHuLX9sFlGohSF3FlQorf8fYW4uZXFfJDGwbELfNjsfLzHwkRF8LDGrJDGwbXPfNjsfYlAublVyFj9sFlVsdyF3WyFzLRB5LgUwFfwfLzHwkRKwLDFsFgLwLEcxLgUfJDFzLRB5LTUwFj9sFlVtrDF3WyGfYW4uZXFfXSwfYlQzFgobFlAikW08bCjqZSFsFlAwdC4jkEVzFfwfZ6GoZDFsFl0lrC8jZCjeFfwfb74jrCAmFfwfb6BjbmcfJDGyhXMjFfwfdmVfhWMvbfFsFmMtYXG9YWQzZXG7ZXFfJDGzbWjsZXrebmQjZDFsFmVudmVskSGrJDGtYfF3ZlAsd7UsFlZ9dyF3WyGPYXMzVCeyb6VmhKZvdl8erDGrJDGydyF3LD55MSwfYlifNfFxLzriZRGfZD9zLRK8JTPwM7KtYWF8LD9zNRQeYTGgMzH7ZRdfJDGfdDF3LD5xJDGlrDF3FjBed6MUhEGvrWrnQl0ybWA9Fm9=", + "adm": "", + "adomain": [ + "dev.sparteo.com" + ], + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }] + } + ], + "cur": "USD" +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tagoras/test-auction-tagoras-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tagoras/test-auction-tagoras-request.json new file mode 100644 index 00000000000..5a76aadfc95 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tagoras/test-auction-tagoras-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "tagoras": { + "cId": "connectionId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tagoras/test-auction-tagoras-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tagoras/test-auction-tagoras-response.json new file mode 100644 index 00000000000..091bbb9db5c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tagoras/test-auction-tagoras-response.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "tagoras" + } + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "tagoras", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "tagoras": "{{ tagoras.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tagoras/test-tagoras-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tagoras/test-tagoras-bid-request.json new file mode 100644 index 00000000000..eb67f5687e5 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tagoras/test-tagoras-bid-request.json @@ -0,0 +1,54 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "cId": "connectionId" + } + } + } + ], + "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": { + "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/tagoras/test-tagoras-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tagoras/test-tagoras-bid-response.json new file mode 100644 index 00000000000..47d4f8718ea --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tagoras/test-tagoras-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "8048", + "mtype": 1 + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json index bb940fbcddd..9d402d76635 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json @@ -13,6 +13,9 @@ "w": 400, "h": 300, "adm": "", + "adomain": [ + "yieldlab" + ], "ext": { "origbidcpm": 2.01, "origbidcur": "EUR", 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 3851b37e2d4..51958ab075b 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -169,6 +169,8 @@ adapters.blasto.enabled=true adapters.blasto.endpoint=http://localhost:8090/blasto-exchange?source={{SourceId}}&account={{AccountID}} adapters.bliink.enabled=true adapters.bliink.endpoint=http://localhost:8090/bliink-exchange +adapters.blis.enabled=true +adapters.blis.endpoint=http://localhost:8090/blis-exchange adapters.bluesea.enabled=true adapters.bluesea.endpoint=http://localhost:8090/bluesea-exchange adapters.bmtm.enabled=true @@ -236,6 +238,8 @@ adapters.escalax.enabled=true adapters.escalax.endpoint=http://localhost:8090/escalax-exchange?k={{AccountID}}&name={{SourceId}} adapters.evolution.enabled=true adapters.evolution.endpoint=http://localhost:8090/evolution-exchange +adapters.exco.enabled=true +adapters.exco.endpoint=http://localhost:8090/exco-exchange adapters.evtech.enabled=true adapters.feedad.enabled=true adapters.feedad.endpoint=http://localhost:8090/feedad-exchange @@ -526,6 +530,8 @@ adapters.sovrn.enabled=true adapters.sovrn.endpoint=http://localhost:8090/sovrn-exchange adapters.sovrnXsp.enabled=true adapters.sovrnXsp.endpoint=http://localhost:8090/sovrnxsp-exchange +adapters.sparteo.enabled=true +adapters.sparteo.endpoint=http://localhost:8090/sparteo-exchange adapters.sspbc.enabled=true adapters.sspbc.endpoint=http://localhost:8090/sspbc-exchange adapters.sharethrough.enabled=true @@ -600,6 +606,10 @@ adapters.vidazoo.enabled=true adapters.vidazoo.endpoint=http://localhost:8090/vidazoo-exchange/ adapters.vidazoo.aliases.progx.enabled=true adapters.vidazoo.aliases.progx.endpoint=http://localhost:8090/progx-exchange/ +adapters.vidazoo.aliases.omnidex.enabled=true +adapters.vidazoo.aliases.omnidex.endpoint=http://localhost:8090/omnidex-exchange/ +adapters.vidazoo.aliases.tagoras.enabled=true +adapters.vidazoo.aliases.tagoras.endpoint=http://localhost:8090/tagoras-exchange/ adapters.videobyte.enabled=true adapters.videobyte.endpoint=http://localhost:8090/videobyte-exchange adapters.videoheroes.enabled=true