diff --git a/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java b/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java index a569edbd0cb..f9f2bcc8a9a 100644 --- a/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java +++ b/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java @@ -1,12 +1,14 @@ package org.prebid.server.bidder.emxdigital; import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -41,7 +43,8 @@ public class EmxDigitalBidder implements Bidder { - private static final String DEFAULT_BID_CURRENCY = "USD"; + private static final String USD_CURRENCY = "USD"; + private static final Integer PROTOCOL_VAST_40 = 7; private static final TypeReference> EMXDIGITAL_EXT_TYPE_REFERENCE = new TypeReference>() { @@ -79,7 +82,7 @@ public Result>> makeHttpRequests(BidRequest request // Handle request errors and formatting to be sent to EMX private BidRequest makeBidRequest(BidRequest request) { - final boolean isSecure = isSecure(request.getSite()); + final boolean isSecure = resolveUrl(request).startsWith("https"); final List modifiedImps = request.getImp().stream() .map(imp -> modifyImp(imp, isSecure, unpackImpExt(imp))) @@ -90,10 +93,21 @@ private BidRequest makeBidRequest(BidRequest request) { .build(); } - private static boolean isSecure(Site site) { - return site != null - && StringUtils.isNotBlank(site.getPage()) - && site.getPage().startsWith("https"); + private static String resolveUrl(BidRequest request) { + final Site site = request.getSite(); + final String page = site != null ? site.getPage() : null; + if (StringUtils.isNotBlank(page)) { + return page; + } + final App app = request.getApp(); + if (app != null) { + if (StringUtils.isNotBlank(app.getDomain())) { + return app.getDomain(); + } else if (StringUtils.isNotBlank(app.getStoreurl())) { + return app.getStoreurl(); + } + } + return ""; } private ExtImpEmxDigital unpackImpExt(Imp imp) { @@ -122,13 +136,16 @@ private ExtImpEmxDigital unpackImpExt(Imp imp) { } private static Imp modifyImp(Imp imp, boolean isSecure, ExtImpEmxDigital extImpEmxDigital) { - final Banner banner = modifyImpBanner(imp.getBanner()); final Imp.ImpBuilder impBuilder = imp.toBuilder() .tagid(extImpEmxDigital.getTagid()) - .secure(BooleanUtils.toInteger(isSecure)) - .banner(banner) - .ext(null); + .secure(BooleanUtils.toInteger(isSecure)); + final Video video = imp.getVideo(); + if (video != null) { + impBuilder.video(modifyImpVideo(video)); + } else { + impBuilder.banner(modifyImpBanner(imp.getBanner())); + } final String stringBidfloor = extImpEmxDigital.getBidfloor(); if (StringUtils.isBlank(stringBidfloor)) { @@ -144,10 +161,36 @@ private static Imp modifyImp(Imp imp, boolean isSecure, ExtImpEmxDigital extImpE return impBuilder .bidfloor(bidfloor) - .bidfloorcur(DEFAULT_BID_CURRENCY) + .bidfloorcur(USD_CURRENCY) .build(); } + private static Video modifyImpVideo(Video video) { + if (CollectionUtils.isEmpty(video.getMimes())) { + throw new PreBidException("Video: missing required field mimes"); + } + if (isNotPresentSize(video.getH()) && isNotPresentSize(video.getW())) { + throw new PreBidException("Video: Need at least one size to build request"); + } + if (CollectionUtils.isNotEmpty(video.getProtocols())) { + final List updatedProtocols = removeVast40Protocols(video.getProtocols()); + return video.toBuilder().protocols(updatedProtocols).build(); + } + + return video; + } + + private static boolean isNotPresentSize(Integer size) { + return Objects.isNull(size) || size == 0; + } + + // not supporting VAST protocol 7 (VAST 4.0); + private static List removeVast40Protocols(List protocols) { + return protocols.stream() + .filter(protocol -> !protocol.equals(PROTOCOL_VAST_40)) + .collect(Collectors.toList()); + } + private static Banner modifyImpBanner(Banner banner) { if (banner == null) { throw new PreBidException("Request needs to include a Banner object"); @@ -186,8 +229,9 @@ private static MultiMap makeHeaders(BidRequest request) { } final Site site = request.getSite(); - if (site != null && StringUtils.isNotBlank(site.getPage())) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getPage()); + final String page = site != null ? site.getPage() : null; + if (StringUtils.isNotBlank(page)) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, page); } return headers; @@ -241,10 +285,15 @@ private static List bidsFromResponse(BidResponse bidResponse) { .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(modifyBid(bid), BidType.banner, bidResponse.getCur())) + .map(bid -> BidderBid.of(modifyBid(bid), getBidType(bid.getAdm()), bidResponse.getCur())) .collect(Collectors.toList()); } + private static BidType getBidType(String bidAdm) { + return StringUtils.containsAny(bidAdm, ">> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + final Imp expectedImp = Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .video(Video.builder() + .mimes(Collections.singletonList("someMime")) + .protocols(Arrays.asList(1, 2)) + .w(100) + .h(100) + .build()) + .tagid("123") + .secure(0) + .bidfloor(new BigDecimal("2")) + .bidfloorcur("USD") + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .containsOnly(expectedImp); + } + + @Test + public void shouldThrowExceptionIfVideoDoNotHaveAtLeastOneSizeParameter() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .video(Video.builder().mimes(Collections.singletonList("someMime")).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Video: Need at least one size to build request")); + } + + @Test + public void shouldThrowExceptionIfVideoDoNotHaveAnyMimeParameter() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .video(Video.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Video: missing required field mimes")); + } + + @Test + public void requestSecureShouldBeOneIfPageStartsWithHttps() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .site(Site.builder().page("https://exmaple/").build()) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(request -> request.getImp().get(0).getSecure()) + .containsOnly(1); + } + + @Test + public void requestSecureShouldBeOneIfUrlStartsWithHttps() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .app(App.builder().domain("https://exmaple/").build()) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getSecure) + .containsOnly(1); + } + + @Test + public void requestSecureShouldBe1IfStoreUrlStartsWithHttps() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .app(App.builder().storeurl("https://exmaple/").build()) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getSecure) + .containsOnly(1); + } + + @Test + public void requestSecureShouldBe0IfPageDoNotStartsWithHttps() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .banner(Banner.builder().w(100).h(100).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2")))) + .build())) + .tmax(1000L) + .site(Site.builder().page("http://exmaple/").build()) + .build(); + + // when + final Result>> result = emxDigitalBidder + .makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getSecure) + .containsOnly(0); + } + @Test public void makeHttpRequestsShouldModifyBannerFormatAndWidthAndHeightWhenRequestBannerWidthAndHeightIsNull() { // given @@ -255,6 +440,7 @@ public void makeHttpRequestsShouldModifyBannerFormatAndWidthAndHeightWhenRequest .format(singletonList(Format.builder().h(30).w(31).build())).build(); final Imp expectedImp = Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("1", "asd")))) .banner(expectedBanner) .tagid("1") .secure(0) @@ -395,7 +581,7 @@ public void makeBidsShouldReturnEmptyListWhenBidResponseSeatBidIsNull() } @Test - public void makeBidsShouldAlwaysReturnBannerBidWithChangedBidImpId() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBidWithChangedBidImpId() throws JsonProcessingException { // given final HttpCall httpCall = givenHttpCall( BidRequest.builder() @@ -413,6 +599,46 @@ public void makeBidsShouldAlwaysReturnBannerBidWithChangedBidImpId() throws Json .containsOnly(BidderBid.of(Bid.builder().id("321").impid("321").build(), banner, "USD")); } + @Test + public void makeBidsShouldReturnVideoBidIfAdmContainsVastPrefix() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.id("321").adm("> result = emxDigitalBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().id("321").adm(" httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.id("321").adm("> result = emxDigitalBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().id("321").adm(" bidCustomizer) { return BidResponse.builder() diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json index 9c5c067a286..6c9e0d2158f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json @@ -50,7 +50,7 @@ "emx_digital": [ { "uri": "{{ emx_digital.exchange_uri }}?t=1000&ts=2060541160", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0,\"ext\":{\"bidder\":{\"tagid\":\"25251\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", "responsebody": "{\"id\":\"some_test_auction\",\"seatbid\":[{\"seat\":\"12356\",\"bid\":[{\"id\":\"uuid\",\"adm\":\"
\",\"impid\":\"uuid\",\"ttl\":300,\"crid\":\"94395500\",\"w\":300,\"price\":2.942808,\"adid\":\"94395500\",\"h\":250}]}],\"cur\":\"USD\"}", "status": 200 }