diff --git a/src/main/java/org/prebid/server/bidder/mgid/MgidBidder.java b/src/main/java/org/prebid/server/bidder/mgid/MgidBidder.java new file mode 100644 index 00000000000..5331dbeec9d --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mgid/MgidBidder.java @@ -0,0 +1,181 @@ +package org.prebid.server.bidder.mgid; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Imp.ImpBuilder; +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 io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.BidderUtil; +import org.prebid.server.bidder.mgid.model.ExtBidMgid; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.request.mgid.ExtImpMgid; +import org.prebid.server.proto.openrtb.ext.response.BidType; +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.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class MgidBidder implements Bidder { + + private static final String DEFAULT_BID_CURRENCY = "USD"; + private final String endpointUrl; + + public MgidBidder(String endpoint) { + endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + final List imps = new ArrayList<>(); + String accountId = null; + for (Imp imp : bidRequest.getImp()) { + try { + final ExtImpMgid impExt = parseImpExt(imp); + + if (StringUtils.isBlank(accountId) && StringUtils.isNotBlank(impExt.getAccountId())) { + accountId = impExt.getAccountId(); + } + + final Imp modifiedImp = modifyImp(imp, impExt); + imps.add(modifiedImp); + } catch (PreBidException e) { + return Result.emptyWithError(BidderError.badInput(e.getMessage())); + } + } + + if (StringUtils.isBlank(accountId)) { + return Result.emptyWithError(BidderError.badInput("accountId is not set")); + } + + final BidRequest outgoingRequest = bidRequest.toBuilder() + .tmax(bidRequest.getTmax()) + .imp(imps) + .build(); + + final String body = Json.encode(outgoingRequest); + + return Result.of(Collections.singletonList(HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl + accountId) + .body(body) + .headers(BidderUtil.headers()) + .payload(outgoingRequest) + .build()), Collections.emptyList()); + } + + private static ExtImpMgid parseImpExt(Imp imp) { + try { + return Json.mapper.convertValue(imp.getExt().get("bidder"), ExtImpMgid.class); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage(), e); + } + } + + private static Imp modifyImp(Imp imp, ExtImpMgid impExt) throws PreBidException { + final ImpBuilder impBuilder = imp.toBuilder(); + + final String cur = getCur(impExt); + if (StringUtils.isNotBlank(cur)) { + impBuilder.bidfloorcur(cur); + } + + final BigDecimal bidFloor = getBidFloor(impExt); + if (bidFloor != null) { + impBuilder.bidfloor(bidFloor); + } + + return impBuilder + .tagid(impExt.getPlacementId()) + .build(); + } + + private static String getCur(ExtImpMgid impMgid) { + return ObjectUtils.defaultIfNull( + currencyValueOrNull(impMgid.getCurrency()), currencyValueOrNull(impMgid.getCur())); + } + + private static BigDecimal getBidFloor(ExtImpMgid impMgid) { + return ObjectUtils.defaultIfNull( + bidFloorValueOrNull(impMgid.getBidfloor()), bidFloorValueOrNull(impMgid.getBidFloorSecond())); + } + + private static String currencyValueOrNull(String value) { + return StringUtils.isNotBlank(value) && !value.equals("USD") ? value : null; + } + + private static BigDecimal bidFloorValueOrNull(BigDecimal value) { + return value != null && value.compareTo(BigDecimal.ZERO) > 0 ? value : null; + } + + @Override + public final Result> makeBids(HttpCall httpCall, + BidRequest bidRequest) { + try { + final BidResponse bidResponse = Json + .decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse), + Collections.emptyList()); + } catch (DecodeException | PreBidException e) { + return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || bidResponse.getSeatbid() == null) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse); + } + + private static List bidsFromResponse(BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getBidType(bid), DEFAULT_BID_CURRENCY)) + .collect(Collectors.toList()); + } + + private static BidType getBidType(Bid bid) { + final ExtBidMgid bidExt = getBidExt(bid); + if (bidExt == null) { + return BidType.banner; + } + + final BidType crtype = bidExt.getCrtype(); + return crtype == null ? BidType.banner : crtype; + } + + private static ExtBidMgid getBidExt(Bid bid) { + try { + return Json.mapper.convertValue(bid.getExt(), ExtBidMgid.class); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public final Map extractTargeting(ObjectNode ext) { + return Collections.emptyMap(); + } +} diff --git a/src/main/java/org/prebid/server/bidder/mgid/model/ExtBidMgid.java b/src/main/java/org/prebid/server/bidder/mgid/model/ExtBidMgid.java new file mode 100644 index 00000000000..bcc9f57c099 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mgid/model/ExtBidMgid.java @@ -0,0 +1,12 @@ +package org.prebid.server.bidder.mgid.model; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +@Value +@AllArgsConstructor(staticName = "of") +public class ExtBidMgid { + + BidType crtype; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mgid/ExtImpMgid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mgid/ExtImpMgid.java new file mode 100644 index 00000000000..f93718c8457 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mgid/ExtImpMgid.java @@ -0,0 +1,31 @@ +package org.prebid.server.proto.openrtb.ext.request.mgid; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.math.BigDecimal; + +/** + * Defines the contract for bidRequest.imp[i].ext.mgid + */ +@AllArgsConstructor( + staticName = "of" +) +@Value +public class ExtImpMgid { + @JsonProperty("accountId") + String accountId; + + @JsonProperty("placementId") + String placementId; + + String cur; + + String currency; + + BigDecimal bidfloor; + + @JsonProperty("bidFloor") + BigDecimal bidFloorSecond; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MgidConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MgidConfiguration.java new file mode 100644 index 00000000000..eec2011228d --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MgidConfiguration.java @@ -0,0 +1,49 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.mgid.MgidBidder; +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.BidderInfoCreator; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +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/mgid.yaml", factory = YamlPropertySourceFactory.class) +public class MgidConfiguration { + + private static final String BIDDER_NAME = "mgid"; + + @Value("${external-url}") + @NotBlank + private String externalUrl; + + @Autowired + @Qualifier("mgidConfigurationProperties") + private BidderConfigurationProperties configProperties; + + @Bean("mgidConfigurationProperties") + @ConfigurationProperties("adapters.mgid") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps mgidBidderDeps() { + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(configProperties) + .bidderInfo(BidderInfoCreator.create(configProperties)) + .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl)) + .bidderCreator(() -> new MgidBidder(configProperties.getEndpoint())) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/mgid.yaml b/src/main/resources/bidder-config/mgid.yaml new file mode 100644 index 00000000000..b7ad9b45efe --- /dev/null +++ b/src/main/resources/bidder-config/mgid.yaml @@ -0,0 +1,23 @@ +adapters: + mgid: + enabled: false + endpoint: https://prebid.mgid.com/prebid/ + pbs-enforces-gdpr: true + deprecated-names: + aliases: + meta-info: + maintainer-email: prebid@mgid.com + app-media-types: + - banner + - native + site-media-types: + - banner + - native + supported-vendors: + vendor-id: 358 + usersync: + url: https://cm.mgid.com/m?cdsp=363893&adu= + redirect-url: /setuid?bidder=mgid&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&uid={muidn} + cookie-family-name: mgid + type: redirect + support-cors: false \ No newline at end of file diff --git a/src/main/resources/static/bidder-params/mgid.json b/src/main/resources/static/bidder-params/mgid.json new file mode 100644 index 00000000000..be77dc8e138 --- /dev/null +++ b/src/main/resources/static/bidder-params/mgid.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Mgid Params", + "description": "A schema which validates params accepted by the Mgid", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Internal Mgid account ID" + }, + "placementId": { + "type": "string", + "description": "Internal Mgid Placement ID" + }, + "cur": { + "type": "string", + "description": "optional bidfloor currency" + }, + "currency": { + "type": "string", + "description": "optional bidfloor currency" + }, + "bidfloor": { + "type": "number", + "description": "optional minimum acceptable bid, in CPM, USD by default" + }, + "bidFloor": { + "type": "number", + "description": "optional minimum acceptable bid, in CPM, USD by default" + } + }, + "required": ["accountId","placementId"] +} diff --git a/src/test/java/org/prebid/server/bidder/mgid/MgidBidderTest.java b/src/test/java/org/prebid/server/bidder/mgid/MgidBidderTest.java new file mode 100644 index 00000000000..21a91188c61 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/mgid/MgidBidderTest.java @@ -0,0 +1,322 @@ +package org.prebid.server.bidder.mgid; + +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 io.vertx.core.json.Json; +import org.junit.Before; +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +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.mgid.ExtImpMgid; + +import java.math.BigDecimal; +import java.util.List; +import java.util.function.Function; + +import static java.util.Collections.emptyMap; +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.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; + +public class MgidBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/mgid-exchange/"; + + private MgidBidder mgidBidder; + + private static BidResponse givenBidResponse(Function bidCustomizer) { + return BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static HttpCall givenHttpCall(BidRequest bidRequest, String body) { + return HttpCall.success( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } + + @Before + public void setUp() { + mgidBidder = new MgidBidder(ENDPOINT_URL); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new MgidBidder("invalid_url")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) + .build())) + .id("request_id") + .build(); + + // when + final Result>> result = mgidBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance"); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtAccIdIsBlank() { + // given + final String currency = "GRP"; + final BigDecimal bidFloor = new BigDecimal(10.3); + final String placementId = "placID"; + final String accId = ""; + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpMgid.of(accId, placementId, currency, currency, bidFloor, bidFloor)))) + .build())) + .id("reqID") + .build(); + + // when + final Result>> result = mgidBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()).isEqualToIgnoringCase("accountId is not set"); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldSetBidFloorCurAndBidFloorToIncomingRequestWhenImpExtHasNotBlankCurAndBidfloor() { + // given + final String currency = "GRP"; + final BigDecimal bidFloor = new BigDecimal(10.3); + final String placementId = "placID"; + final String accId = "accId"; + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .bidfloor(new BigDecimal(3)) + .bidfloorcur("be replaced") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpMgid.of(accId, placementId, currency, null, bidFloor, null)))) + .build())) + .id("reqID") + .build(); + + // when + final Result>> result = mgidBidder.makeHttpRequests(bidRequest); + + // then + final BidRequest expected = BidRequest.builder() + .imp(singletonList(Imp.builder() + .bidfloor(bidFloor) + .bidfloorcur(currency) + .tagid(placementId) + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpMgid.of("accId", placementId, currency, null, bidFloor, null)))) + .build())) + .id("reqID") + .build(); + + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getBody) + .containsOnly(Json.encode(expected)); + } + + @Test + public void makeHttpRequestsShouldSetBidFloorCurAndBidFloorToIncomingRequestWhenImpExtHasNotBlankCurencyAndBidFloor() { + // given + final String currency = "GRP"; + final BigDecimal bidFloor = new BigDecimal(10.3); + final String placementId = "placID"; + final String accId = "accId"; + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .bidfloor(new BigDecimal(3)) + .bidfloorcur("be replaced") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpMgid.of(accId, placementId, null, currency, null, bidFloor)))) + .build())) + .id("reqID") + .build(); + + // when + final Result>> result = mgidBidder.makeHttpRequests(bidRequest); + + // then + final BidRequest expected = BidRequest.builder() + .imp(singletonList(Imp.builder() + .bidfloor(bidFloor) + .bidfloorcur(currency) + .tagid(placementId) + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpMgid.of("accId", placementId, null, currency, null, bidFloor)))) + .build())) + .id("reqID") + .build(); + + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getBody) + .containsOnly(Json.encode(expected)); + } + + @Test + public void makeHttpRequestsShouldNotModifyIncomingRequestWhenImpExtNotContainsParamters() { + // given + final String placementId = "placID"; + final String accId = "accId"; + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpMgid.of(accId, placementId, null, null, null, null)))) + .build())) + .tmax(1000L) + .id("reqID") + .build(); + + // when + final Result>> result = mgidBidder.makeHttpRequests(bidRequest); + + // then + final BidRequest expected = BidRequest.builder() + .imp(singletonList(Imp.builder() + .tagid(placementId) + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpMgid.of("accId", placementId, null, null, null, null)))) + .build())) + .tmax(1000L) + .id("reqID") + .build(); + + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getBody) + .containsOnly(Json.encode(expected)); + } + + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final HttpCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = mgidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token"); + assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(null)); + + // when + final Result> result = mgidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = mgidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidWhenBannerIsPresentAndExtCrtypeIsNotSet() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = mgidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnXNativeBidWhenBidIsPresentAndExtCrtypeIsNative() throws JsonProcessingException { + // given + final ObjectNode crtypeNode = Json.mapper.createObjectNode().put("crtype", "native"); + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").ext(crtypeNode)))); + + // when + final Result> result = mgidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").ext(crtypeNode).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBidWhenBidIsPresentAndExtCrtypeIsBlank() throws JsonProcessingException { + // given + final ObjectNode crtypeNode = Json.mapper.createObjectNode().put("crtype", ""); + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").ext(crtypeNode)))); + + // when + final Result> result = mgidBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").ext(crtypeNode).build(), banner, "USD")); + } + + @Test + public void extractTargetingShouldReturnEmptyMap() { + assertThat(mgidBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); + } +} diff --git a/src/test/java/org/prebid/server/it/MgidTest.java b/src/test/java/org/prebid/server/it/MgidTest.java new file mode 100644 index 00000000000..c3156c21be0 --- /dev/null +++ b/src/test/java/org/prebid/server/it/MgidTest.java @@ -0,0 +1,54 @@ +package org.prebid.server.it; + +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 io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; + +import io.restassured.RestAssured; +import io.restassured.parsing.Parser; +import io.restassured.response.Response; +import java.io.IOException; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +public class MgidTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheMgid() throws IOException, JSONException { + // given + wireMockRule.stubFor(post(urlPathEqualTo("/mgid-exchange/123")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/mgid/test-mgid-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/mgid/test-mgid-bid-response.json")))); + + // pre-bid cache + wireMockRule.stubFor(post(urlPathEqualTo("/cache")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/mgid/test-cache-mgid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/mgid/test-cache-mgid-response.json")))); + + // when + final Response response = given(spec) + .header("Referer", "http://www.example.com") + .header("X-Forwarded-For", "192.168.244.1") + .header("User-Agent", "userAgent") + .header("Origin", "http://www.example.com") + // this uids cookie value stands for {"uids":{"mgid":"MGID-UID"}} + .cookie("uids", "eyJ1aWRzIjp7ImxpZmVzdH9uZSI6IlJPLVVJRCJ9fQ1") + .body(jsonFrom("openrtb2/mgid/test-auction-mgid-request.json")) + .post("/openrtb2/auction"); + + // then + final String expectedAuctionResponse = openrtbAuctionResponseFrom( + "openrtb2/mgid/test-auction-mgid-response.json", + response, singletonList("mgid")); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-request.json new file mode 100644 index 00000000000..15e132d5387 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-request.json @@ -0,0 +1,47 @@ +{ + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "mgid": { + "accountId": "123", + "placementId": "456", + "bidfloor": 1.1, + "cur": "GBP" + } + } + } + ], + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site" + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "dnt": 0, + "language": "en" + }, + "user": { + "buyeruid": "test_reader_id" + }, + "test": 1, + "at": 1, + "tmax": 1000, + "cur": [ + "USD" + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json new file mode 100644 index 00000000000..3534de788e7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json @@ -0,0 +1,98 @@ +{ + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "nurl": "nurl", + "adm": "some-test-ad", + "crid": "crid002", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "bidder": { + "crtype": "banner" + } + } + } + ], + "seat": "mgid", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "httpcalls": { + "mgid": [ + { + "uri": "http://localhost:8090/mgid-exchange/123", + "requestbody": "{\"id\":\"test-request-id\",\"imp\":[{\"id\":\"test-imp-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250},{\"w\":300,\"h\":600}]},\"tagid\":\"456\",\"bidfloor\":1.1,\"bidfloorcur\":\"GBP\",\"ext\":{\"bidder\":{\"accountId\":\"123\",\"placementId\":\"456\",\"bidfloor\":1.1,\"cur\":\"GBP\"}}}],\"site\":{\"domain\":\"www.publisher.com\",\"page\":\"http://www.publisher.com/awesome/site\",\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"test-user-agent\",\"dnt\":0,\"ip\":\"123.123.123.123\",\"language\":\"en\"},\"user\":{\"buyeruid\":\"test_reader_id\"},\"test\":1,\"at\":1,\"tmax\":1000,\"cur\":[\"USD\"]}", + "responsebody": "{\"id\":\"test-request-id\",\"seatbid\":[{\"bid\":[{\"id\":\"test-bid-id\",\"impid\":\"test-imp-id\",\"price\":3.5,\"nurl\":\"nurl\",\"crid\":\"crid002\",\"adm\":\"some-test-ad\",\"w\":300,\"h\":250,\"ext\":{\"crtype\":\"banner\"}}]}]}", + "status": 200 + } + ] + }, + "resolvedrequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "mgid": { + "accountId": "123", + "placementId": "456", + "bidfloor": 1.1, + "cur": "GBP" + } + } + } + ], + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site", + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "test-user-agent", + "dnt": 0, + "ip": "123.123.123.123", + "language": "en" + }, + "user": { + "buyeruid": "test_reader_id" + }, + "test": 1, + "at": 1, + "tmax": 1000, + "cur": [ + "USD" + ] + } + }, + "responsetimemillis": { + "mgid": "{{ mgid.response_time_ms }}" + }, + "tmaxrequest": 1000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-cache-mgid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-cache-mgid-request.json new file mode 100644 index 00000000000..ddf2bf80eb7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-cache-mgid-request.json @@ -0,0 +1,16 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "nurl": "nurl", + "adm": "some-test-ad", + "w": 300, + "h": 250 + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-cache-mgid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-cache-mgid-response.json new file mode 100644 index 00000000000..773491fa9b0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-cache-mgid-response.json @@ -0,0 +1,7 @@ +{ + "responses": [ + { + "uuid": "a5d3a873-d06e-4f2f-8556-120e05d62b28" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-mgid-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-mgid-bid-request.json new file mode 100644 index 00000000000..a8824bf0397 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-mgid-bid-request.json @@ -0,0 +1,53 @@ +{ + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "bidfloor": 1.1, + "bidfloorcur": "GBP", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "456", + "ext": { + "bidder": { + "accountId": "123", + "placementId": "456", + "bidfloor": 1.1, + "cur": "GBP" + } + } + } + ], + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site", + "ext": { + "amp" : 0 + } + }, + "device": { + "ua": "test-user-agent", + "dnt": 0, + "ip": "123.123.123.123", + "language": "en" + }, + "user": { + "buyeruid": "test_reader_id" + }, + "test": 1, + "at": 1, + "tmax": 1000, + "cur": [ + "USD" + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-mgid-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-mgid-bid-response.json new file mode 100644 index 00000000000..f9848ebef1c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-mgid-bid-response.json @@ -0,0 +1,22 @@ +{ + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "nurl": "nurl", + "crid": "crid002", + "adm": "some-test-ad", + "w": 300, + "h": 250, + "ext": { + "crtype": "banner" + } + } + ] + } + ] +} 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 49c06cdeeb1..75a60f9c9f1 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -50,6 +50,10 @@ adapters.grid.enabled=true adapters.grid.endpoint=http://localhost:8090/grid-exchange adapters.grid.pbs-enforces-gdpr=true adapters.grid.usersync.url=//grid-usersync +adapters.mgid.enabled=true +adapters.mgid.endpoint=http://localhost:8090/mgid-exchange/ +adapters.mgid.pbs-enforces-gdpr=true +adapters.mgid.usersync.url=//mgid-usersync adapters.gumgum.enabled=true adapters.gumgum.endpoint=http://localhost:8090/gumgum-exchange adapters.gumgum.pbs-enforces-gdpr=true