diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360Bidder.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360Bidder.java new file mode 100644 index 00000000000..180f545bf47 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360Bidder.java @@ -0,0 +1,176 @@ +package org.prebid.server.bidder.nexx360; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; +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.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.nexx360.ExtImpNexx360; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; + +import java.net.URISyntaxException; +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 Nexx360Bidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String BIDDER_NAME = "nexx360"; + + private final String endpointUrl; + private final JacksonMapper mapper; + private final PrebidVersionProvider prebidVersionProvider; + + public Nexx360Bidder(String endpointUrl, JacksonMapper mapper, PrebidVersionProvider prebidVersionProvider) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List imps = request.getImp(); + final List modifiedImps = new ArrayList<>(); + + final ExtImpNexx360 firstExtImp; + try { + firstExtImp = parseImpExt(imps.getFirst()); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + for (final Imp imp : imps) { + modifiedImps.add(modifyImp(imp)); + } + + final BidRequest modifiedRequest = makeRequest(request, modifiedImps); + final String url = makeUrl(firstExtImp.getTagId(), firstExtImp.getPlacement()); + return Result.withValue(BidderUtil.defaultRequest(modifiedRequest, url, mapper)); + } + + private ExtImpNexx360 parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp) { + return imp.toBuilder() + .ext(mapper.mapper().createObjectNode().set(BIDDER_NAME, imp.getExt().get("bidder"))) + .build(); + } + + private BidRequest makeRequest(BidRequest request, List imps) { + final ExtRequest extRequest = ExtRequest.empty(); + extRequest.addProperty(BIDDER_NAME, mapper.mapper().valueToTree( + Nexx360ExtRequest.of(Nexx360ExtRequestCaller.of(prebidVersionProvider.getNameVersionRecord())))); + + return request.toBuilder() + .imp(imps) + .ext(extRequest) + .build(); + } + + private String makeUrl(String tagId, String placement) { + final URIBuilder uriBuilder; + try { + uriBuilder = new URIBuilder(endpointUrl); + } catch (URISyntaxException e) { + throw new PreBidException("Invalid url: %s, error: %s".formatted(endpointUrl, e.getMessage())); + } + + if (StringUtils.isNotBlank(placement)) { + uriBuilder.addParameter("placement", placement); + } + if (StringUtils.isNotBlank(tagId)) { + uriBuilder.addParameter("tag_id", tagId); + } + + return uriBuilder.toString(); + } + + @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 bidsFromResponse(bidResponse, errors); + } + + private 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 BidderBid makeBid(Bid bid, String currency, List errors) { + try { + return BidderBid.of(bid, getBidType(bid), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private BidType getBidType(Bid bid) { + final String bidType; + try { + bidType = mapper.mapper() + .convertValue(bid.getExt(), Nexx360ExtBid.class) + .getBidType(); + } catch (IllegalArgumentException e) { + throw new PreBidException( + "unable to fetch mediaType in multi-format: " + bid.getImpid()); + } + + return switch (bidType) { + case "banner" -> BidType.banner; + case "video" -> BidType.video; + case "audio" -> BidType.audio; + case "native" -> BidType.xNative; + default -> throw new PreBidException( + "unable to fetch mediaType in multi-format: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtBid.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtBid.java new file mode 100644 index 00000000000..3e4ecaf4cda --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtBid.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.nexx360; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class Nexx360ExtBid { + + @JsonProperty("bidType") + String bidType; +} diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequest.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequest.java new file mode 100644 index 00000000000..1ff6bc3ead1 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequest.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.nexx360; + +import lombok.Value; + +import java.util.Collections; +import java.util.List; + +@Value(staticConstructor = "of") +public class Nexx360ExtRequest { + + List caller; + + public static Nexx360ExtRequest of(Nexx360ExtRequestCaller caller) { + return of(Collections.singletonList(caller)); + } +} diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequestCaller.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequestCaller.java new file mode 100644 index 00000000000..72da1af310a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequestCaller.java @@ -0,0 +1,15 @@ +package org.prebid.server.bidder.nexx360; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Nexx360ExtRequestCaller { + + String name; + + String version; + + public static Nexx360ExtRequestCaller of(String version) { + return Nexx360ExtRequestCaller.of("Prebid-Server", version); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/nexx360/ExtImpNexx360.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nexx360/ExtImpNexx360.java new file mode 100644 index 00000000000..5a44dfd868f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nexx360/ExtImpNexx360.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.nexx360; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpNexx360 { + + @JsonProperty("tagId") + String tagId; + + String placement; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/Nexx360Configuration.java b/src/main/java/org/prebid/server/spring/config/bidder/Nexx360Configuration.java new file mode 100644 index 00000000000..2eccf05c68c --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/Nexx360Configuration.java @@ -0,0 +1,46 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.nexx360.Nexx360Bidder; +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.prebid.server.version.PrebidVersionProvider; +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/nexx360.yaml", factory = YamlPropertySourceFactory.class) +public class Nexx360Configuration { + + private static final String BIDDER_NAME = "nexx360"; + + @Bean("nexx360ConfigurationProperties") + @ConfigurationProperties("adapters.nexx360") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps nexx360BidderDeps(BidderConfigurationProperties nexx360ConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(nexx360ConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new Nexx360Bidder( + config.getEndpoint(), + mapper, + prebidVersionProvider)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/nexx360.yaml b/src/main/resources/bidder-config/nexx360.yaml new file mode 100644 index 00000000000..443ca7f5ab4 --- /dev/null +++ b/src/main/resources/bidder-config/nexx360.yaml @@ -0,0 +1,22 @@ +adapters: + nexx360: + endpoint: http://fast.nexx360.io/prebid-server + endpoint-compression: gzip + aliases: + 1accord: ~ + easybid: ~ + prismassp: ~ + meta-info: + maintainer-email: tech@nexx360.io + app-media-types: + - banner + - video + - native + - audio + site-media-types: + - banner + - video + - native + - audio + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/static/bidder-params/nexx360.json b/src/main/resources/static/bidder-params/nexx360.json new file mode 100644 index 00000000000..32e06db5297 --- /dev/null +++ b/src/main/resources/static/bidder-params/nexx360.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Nexx360 Adapter Params", + "description": "A schema which validates params accepted by the Nexx360 adapter", + "type": "object", + "properties": { + "tagId": { + "type": "string", + "minLength": 1, + "description": "TagId" + }, + "placement": { + "type": "string", + "minLength": 1, + "description": "Placement" + } + }, + "anyOf": [ + { + "required": [ + "tagId" + ] + }, + { + "required": [ + "placement" + ] + } + ] +} diff --git a/src/test/java/org/prebid/server/bidder/nexx360/Nexx360BidderTest.java b/src/test/java/org/prebid/server/bidder/nexx360/Nexx360BidderTest.java new file mode 100644 index 00000000000..09c50589e19 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/nexx360/Nexx360BidderTest.java @@ -0,0 +1,286 @@ +package org.prebid.server.bidder.nexx360; + +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.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.nexx360.ExtImpNexx360; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.version.PrebidVersionProvider; + +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 java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +public class Nexx360BidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + private static final String PBS_VERSION = "pbs-java/1.0"; + + @Mock(strictness = LENIENT) + private PrebidVersionProvider prebidVersionProvider; + + private Nexx360Bidder target; + + @BeforeEach + public void setUp() { + target = new Nexx360Bidder(ENDPOINT_URL, jacksonMapper, prebidVersionProvider); + given(prebidVersionProvider.getNameVersionRecord()).willReturn(PBS_VERSION); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new Nexx360Bidder("invalid_url", jacksonMapper, prebidVersionProvider)); + } + + @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("Cannot deserialize value of type"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldCreateSingleRequest() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("imp1"), + imp -> imp.id("imp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + } + + @Test + public void makeHttpRequestsShouldSetCorrectUrl() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(givenImpExt("tag", "placement"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com?placement=placement&tag_id=tag"); + } + + @Test + public void makeHttpRequestsShouldModifyImpExt() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("imp1").ext(givenImpExt("tag1", "p1")), + imp -> imp.id("imp2").ext(givenImpExt("tag2", "p2"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ObjectNode expectedExt1 = mapper.createObjectNode() + .set("nexx360", mapper.valueToTree(ExtImpNexx360.of("tag1", "p1"))); + final ObjectNode expectedExt2 = mapper.createObjectNode() + .set("nexx360", mapper.valueToTree(ExtImpNexx360.of("tag2", "p2"))); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(expectedExt1, expectedExt2); + } + + @Test + public void makeHttpRequestsShouldModifyRequestExt() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ExtRequest expectedExtRequest = ExtRequest.empty(); + expectedExtRequest.addProperty("nexx360", mapper.valueToTree( + Nexx360ExtRequest.of(Nexx360ExtRequestCaller.of("pbs-java/1.0")))); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(req -> req.getPayload().getExt()) + .containsExactly(expectedExtRequest); + } + + @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 makeBidsShouldReturnErrorWhenBidTypeIsUnknown() throws JsonProcessingException { + // given + final Bid bid = givenBid(builder -> builder.ext(givenBidExt("unknown"))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("unable to fetch mediaType in multi-format: impId")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidTypeIfPresentInBidExt() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(builder -> builder.ext(givenBidExt("banner"))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(bannerBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidTypeIfPresentInBidExt() throws JsonProcessingException { + // given + final Bid videoBid = givenBid(builder -> builder.ext(givenBidExt("video"))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(videoBid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBidTypeIfPresentInBidExt() throws JsonProcessingException { + // given + final Bid audioBid = givenBid(builder -> builder.ext(givenBidExt("audio"))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(audioBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(audioBid, BidType.audio, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidTypeIfPresentInBidExt() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(builder -> builder.ext(givenBidExt("native"))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(nativeBid, BidType.xNative, "USD")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + final List imps = Arrays.stream(impCustomizers) + .map(Nexx360BidderTest::givenImp) + .toList(); + return BidRequest.builder().imp(imps).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(givenImpExt("tagId", "placementId"))) + .build(); + } + + private static ObjectNode givenImpExt(String tagId, String placement) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpNexx360.of(tagId, placement))); + } + + private static Bid givenBid(UnaryOperator bidCustomizer) { + return bidCustomizer.apply(Bid.builder().id("bidId").impid("impId").price(BigDecimal.ONE)).build(); + } + + private static ObjectNode givenBidExt(String bidType) { + return mapper.valueToTree(Nexx360ExtBid.of(bidType)); + } + + 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/it/EasybidTest.java b/src/test/java/org/prebid/server/it/EasybidTest.java new file mode 100644 index 00000000000..3993578f48b --- /dev/null +++ b/src/test/java/org/prebid/server/it/EasybidTest.java @@ -0,0 +1,34 @@ +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.equalTo; +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 EasybidTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromEasybid() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/easybid-exchange")) + .withQueryParam("placement", equalTo("placement")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/easybid/test-easybid-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/easybid/test-easybid-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/easybid/test-auction-easybid-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/easybid/test-auction-easybid-response.json", response, singletonList("easybid")); + } +} diff --git a/src/test/java/org/prebid/server/it/Nexx360Test.java b/src/test/java/org/prebid/server/it/Nexx360Test.java new file mode 100644 index 00000000000..e32a6a93918 --- /dev/null +++ b/src/test/java/org/prebid/server/it/Nexx360Test.java @@ -0,0 +1,34 @@ +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.equalTo; +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 Nexx360Test extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromNexx360() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/nexx360-exchange")) + .withQueryParam("placement", equalTo("placement")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/nexx360/test-nexx360-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/nexx360/test-nexx360-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/nexx360/test-auction-nexx360-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/nexx360/test-auction-nexx360-response.json", response, singletonList("nexx360")); + } +} diff --git a/src/test/java/org/prebid/server/it/OneAccordTest.java b/src/test/java/org/prebid/server/it/OneAccordTest.java new file mode 100644 index 00000000000..bce3a853e14 --- /dev/null +++ b/src/test/java/org/prebid/server/it/OneAccordTest.java @@ -0,0 +1,34 @@ +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.equalTo; +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 OneAccordTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromOneaccord() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/1accord-exchange")) + .withQueryParam("placement", equalTo("placement")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/oneaccord/test-1accord-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/oneaccord/test-1accord-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/oneaccord/test-auction-1accord-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/oneaccord/test-auction-1accord-response.json", response, singletonList("1accord")); + } +} diff --git a/src/test/java/org/prebid/server/it/PrismaSspTest.java b/src/test/java/org/prebid/server/it/PrismaSspTest.java new file mode 100644 index 00000000000..1fedb5a790c --- /dev/null +++ b/src/test/java/org/prebid/server/it/PrismaSspTest.java @@ -0,0 +1,37 @@ +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.equalTo; +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 PrismaSspTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromPrismassp() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/prismassp-exchange")) + .withQueryParam("placement", equalTo("placement")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/prismassp/test-prismassp-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/prismassp/test-prismassp-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/prismassp/test-auction-prismassp-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals( + "openrtb2/prismassp/test-auction-prismassp-response.json", + response, + singletonList("prismassp")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-request.json new file mode 100644 index 00000000000..e1be3a1f864 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "easybid": { + "placement": "placement" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-response.json new file mode 100644 index 00000000000..41a9fc23d1c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-response.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 5.78, + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "easybid" + } + }, + "bidType": "banner" + } + } + ], + "seat": "easybid", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "easybid": "{{ easybid.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-request.json new file mode 100644 index 00000000000..a6a87d4012c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "nexx360": { + "placement": "placement" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "nexx360": { + "caller": [ + { + "name": "Prebid-Server", + "version": "${json-unit.any-string}" + } + ] + } + } +} + diff --git a/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-response.json new file mode 100644 index 00000000000..1e43f621496 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-response.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "bidType": "banner" + } + } + ], + "seat": "seatId00", + "group": 0 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-request.json b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-request.json new file mode 100644 index 00000000000..015bb3967c8 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "nexx360": { + "placement": "placement" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-response.json new file mode 100644 index 00000000000..4c22dfd3154 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-response.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 5.78, + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "nexx360" + } + }, + "bidType": "banner" + } + } + ], + "seat": "nexx360", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "nexx360": "{{ nexx360.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-request.json new file mode 100644 index 00000000000..a6a87d4012c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "nexx360": { + "placement": "placement" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "nexx360": { + "caller": [ + { + "name": "Prebid-Server", + "version": "${json-unit.any-string}" + } + ] + } + } +} + diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-response.json new file mode 100644 index 00000000000..1e43f621496 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-response.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "bidType": "banner" + } + } + ], + "seat": "seatId00", + "group": 0 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-request.json new file mode 100644 index 00000000000..a6a87d4012c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "nexx360": { + "placement": "placement" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "nexx360": { + "caller": [ + { + "name": "Prebid-Server", + "version": "${json-unit.any-string}" + } + ] + } + } +} + diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-response.json new file mode 100644 index 00000000000..1e43f621496 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-response.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "bidType": "banner" + } + } + ], + "seat": "seatId00", + "group": 0 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-request.json b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-request.json new file mode 100644 index 00000000000..87a3265574b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "1accord": { + "placement": "placement" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-response.json new file mode 100644 index 00000000000..e4ea52c0c85 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-response.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 5.78, + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "1accord" + } + }, + "bidType": "banner" + } + } + ], + "seat": "1accord", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "1accord": "{{ 1accord.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-request.json b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-request.json new file mode 100644 index 00000000000..647a01b2a29 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "prismassp": { + "placement": "placement" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-response.json new file mode 100644 index 00000000000..83245ca2001 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-response.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 5.78, + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "prismassp" + } + }, + "bidType": "banner" + } + } + ], + "seat": "prismassp", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "prismassp": "{{ prismassp.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-request.json new file mode 100644 index 00000000000..a6a87d4012c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "nexx360": { + "placement": "placement" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "nexx360": { + "caller": [ + { + "name": "Prebid-Server", + "version": "${json-unit.any-string}" + } + ] + } + } +} + diff --git a/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-response.json new file mode 100644 index 00000000000..1e43f621496 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-response.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "bidType": "banner" + } + } + ], + "seat": "seatId00", + "group": 0 + } + ] +} 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 a37068f2c93..efedc419022 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -371,6 +371,14 @@ adapters.motorik.endpoint=http://localhost:8090/motorik-exchange?k={{AccountID}} adapters.nextmillennium.enabled=true adapters.nextmillennium.endpoint=http://localhost:8090/nextmillennium-exchange adapters.nextmillennium.extra-info.nmmFlags=1 +adapters.nexx360.enabled=true +adapters.nexx360.endpoint=http://localhost:8090/nexx360-exchange +adapters.nexx360.aliases.1accord.enabled=true +adapters.nexx360.aliases.1accord.endpoint=http://localhost:8090/1accord-exchange +adapters.nexx360.aliases.prismassp.enabled=true +adapters.nexx360.aliases.prismassp.endpoint=http://localhost:8090/prismassp-exchange +adapters.nexx360.aliases.easybid.enabled=true +adapters.nexx360.aliases.easybid.endpoint=http://localhost:8090/easybid-exchange adapters.nobid.enabled=true adapters.nobid.endpoint=http://localhost:8090/nobid-exchange?pubid= adapters.ogury.enabled=true