From b68385d1502acbd3df963e4426f6e5ecfcda85d3 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Wed, 21 Oct 2020 01:46:56 +0300 Subject: [PATCH 1/4] Add invibes bidder --- .../server/bidder/invibes/InvibesBidder.java | 345 ++++++++++++++++++ .../invibes/model/InvibesBidParams.java | 22 ++ .../invibes/model/InvibesBidRequest.java | 49 +++ .../invibes/model/InvibesBidderResponse.java | 19 + .../invibes/model/InvibesInternalParams.java | 23 ++ .../model/InvibesPlacementProperty.java | 19 + .../bidder/invibes/model/InvibesTypedBid.java | 17 + .../ext/request/invibes/ExtImpInvibes.java | 20 + .../request/invibes/model/InvibesDebug.java | 16 + .../config/bidder/InvibesConfiguration.java | 56 +++ src/main/resources/bidder-config/invibes.yaml | 23 ++ .../static/bidder-params/invibes.json | 30 ++ .../bidder/invibes/InvibesBidderTest.java | 242 ++++++++++++ .../org/prebid/server/it/InvibesTest.java | 60 +++ .../invibes/test-auction-invibes-request.json | 90 +++++ .../test-auction-invibes-response.json | 58 +++ .../invibes/test-cache-invibes-request.json | 18 + .../invibes/test-cache-invibes-response.json | 7 + .../invibes/test-invibes-bid-request.json | 10 + .../invibes/test-invibes-bid-response.json | 19 + .../server/it/test-application.properties | 4 + 21 files changed, 1147 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java create mode 100644 src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidParams.java create mode 100644 src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidRequest.java create mode 100644 src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidderResponse.java create mode 100644 src/main/java/org/prebid/server/bidder/invibes/model/InvibesInternalParams.java create mode 100644 src/main/java/org/prebid/server/bidder/invibes/model/InvibesPlacementProperty.java create mode 100644 src/main/java/org/prebid/server/bidder/invibes/model/InvibesTypedBid.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/ExtImpInvibes.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/model/InvibesDebug.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/InvibesConfiguration.java create mode 100644 src/main/resources/bidder-config/invibes.yaml create mode 100644 src/main/resources/static/bidder-params/invibes.json create mode 100644 src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/InvibesTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/invibes/test-cache-invibes-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/invibes/test-cache-invibes-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/invibes/test-invibes-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/invibes/test-invibes-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java new file mode 100644 index 00000000000..fe1c64b2c3f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java @@ -0,0 +1,345 @@ +package org.prebid.server.bidder.invibes; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +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.Regs; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.invibes.model.InvibesBidParams; +import org.prebid.server.bidder.invibes.model.InvibesBidRequest; +import org.prebid.server.bidder.invibes.model.InvibesBidderResponse; +import org.prebid.server.bidder.invibes.model.InvibesInternalParams; +import org.prebid.server.bidder.invibes.model.InvibesPlacementProperty; +import org.prebid.server.bidder.invibes.model.InvibesTypedBid; +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.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.invibes.ExtImpInvibes; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class InvibesBidder implements Bidder { + + private static final TypeReference> INVIBES_EXT_TYPE_REFERENCE = + new TypeReference>() { + }; + private static final String INVIBES_BID_VERSION = "4"; + private static final String ADAPTER_VERSION = "prebid_1.0.0"; + private static final String URL_HOST_MACRO = "{{Host}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public InvibesBidder(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 String consentString = resolveConsentString(request.getUser()); + final Boolean gdprApplies = resolveGDPRApplies(request.getRegs()); + + InvibesInternalParams invibesInternalParams = + InvibesInternalParams.builder() + .bidParams(InvibesBidParams.builder() + .properties(new HashMap()) + .bidVersion(INVIBES_BID_VERSION) + .build()) + .build(); + + for (final Imp imp : request.getImp()) { + final ExtImpInvibes extImpInvibes; + try { + extImpInvibes = mapper.mapper().convertValue(imp.getExt(), INVIBES_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + errors.add(BidderError.badInput("Error parsing invibesExt parameters")); + continue; + } + final Banner banner = imp.getBanner(); + if (banner == null) { + errors.add(BidderError.badInput("Banner not specified")); + continue; + } + + invibesInternalParams = updateInvibesInternalParams(invibesInternalParams, extImpInvibes, imp); + } + //TODO add AMP parameter to invibesInternalParams, after reqInfo will be implemented + + if (invibesInternalParams.getBidParams() + .getPlacementIDs() == null + || invibesInternalParams.getBidParams().getPlacementIDs().size() == 0) { + return Result.of(Collections.emptyList(), errors); + } + + invibesInternalParams = updateWithGDPRParams(invibesInternalParams, consentString, gdprApplies); + + try { + final HttpRequest httpRequest = makeRequest(invibesInternalParams, request); + return Result.of(Collections.singletonList(httpRequest), errors); + } catch (PreBidException e) { + return Result.emptyWithError(BidderError.badInput(e.getMessage())); + } + } + + private String resolveConsentString(User user) { + final ExtUser extUser = user != null ? user.getExt() : null; + if (extUser != null) { + return extUser.getConsent(); + } + + return StringUtils.EMPTY; + } + + private Boolean resolveGDPRApplies(Regs regs) { + final ExtRegs extRegs = regs != null ? regs.getExt() : null; + final Integer gdpr = extRegs != null ? extRegs.getGdpr() : null; + + return gdpr != null ? gdpr == 1 : Boolean.TRUE; + } + + private InvibesInternalParams updateInvibesInternalParams( + InvibesInternalParams invibesInternalParams, ExtImpInvibes invibesExt, + Imp imp) { + final String placementId = invibesExt.getPlacementId(); + final List updatedPlacementIds = getUpdatedPlacementIds( + placementId, invibesInternalParams.getBidParams().getPlacementIDs()); + + final Map updatedProperties + = getUpdatedProperties(invibesInternalParams.getBidParams().getProperties(), + placementId, imp.getId(), resolveAdFormats(imp.getBanner())); + + final InvibesBidParams updatedBidParams = invibesInternalParams.getBidParams().toBuilder() + .placementIDs(updatedPlacementIds) + .properties(updatedProperties) + .build(); + + final InvibesInternalParams.InvibesInternalParamsBuilder internalParamsBuilder + = invibesInternalParams.toBuilder() + .domainID(invibesExt.getDomainId()) + .bidParams(updatedBidParams); + + if (invibesExt.getDebug() != null) { + if (!invibesExt.getDebug().getTestBvid().equals(StringUtils.EMPTY)) { + internalParamsBuilder.testBvid(invibesExt.getDebug().getTestBvid()); + } + internalParamsBuilder.testLog(invibesExt.getDebug().getTestLog()); + } + + return internalParamsBuilder.build(); + } + + private HttpRequest makeRequest(InvibesInternalParams invibesParams, BidRequest request) { + final String url = makeUrl(invibesParams.getDomainID()); + final InvibesBidRequest parameter = resolveParameter(invibesParams, request); + + final MultiMap headers = resolveHeaders(request.getDevice(), request.getSite()); + + final String body = mapper.encode(parameter); + + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(url) + .headers(headers) + .payload(parameter) + .body(body) + .build(); + + } + + private MultiMap resolveHeaders(Device device, Site site) { + final MultiMap headers = HttpUtil.headers(); + if (device != null) { + if (StringUtils.isNotEmpty(device.getIp())) { + headers.add("X-Forwarded-For", device.getIp()); + } else if (StringUtils.isNotEmpty(device.getIpv6())) { + headers.add("X-Forwarded-For", device.getIpv6()); + } + } + if (site != null) { + headers.add("Referer", site.getPage()); + } + headers.add("Aver", ADAPTER_VERSION); + return headers; + } + + private InvibesBidRequest resolveParameter(InvibesInternalParams invibesParams, BidRequest request) { + final String buyeruid = request.getUser() != null ? request.getUser().getBuyeruid() : null; + final String lid = StringUtils.isNotBlank(buyeruid) ? request.getUser().getBuyeruid() : StringUtils.EMPTY; + + if (request.getSite() == null) { + throw new PreBidException("Site not specified"); + } + + return InvibesBidRequest.builder() + .isTestBid(invibesParams.getTestBvid() != null + && !invibesParams.getTestBvid().equals(StringUtils.EMPTY)) + .bidParamsJson(mapper.encode(invibesParams.getBidParams())) + .location(request.getSite().getPage()) + .lid(lid) + .kw(request.getSite().getKeywords()) + .isAMP(invibesParams.getIsAMP()) + .width(resolveWidth(request.getDevice())) + .height(resolveHeight(request.getDevice())) + .gdprConsent(invibesParams.getGdprConsent()) + .gdpr(invibesParams.getGdpr()) + .bvid(invibesParams.getTestBvid()) + .invibBVLog(invibesParams.getTestLog()) + .videoAdDebug(invibesParams.getTestLog()) + .build(); + } + + private String resolveHeight(Device device) { + final Integer height = device != null ? device.getH() : null; + + return height != null && height > NumberUtils.INTEGER_ZERO + ? height.toString() : null; + } + + private String resolveWidth(Device device) { + final Integer width = device != null ? device.getW() : null; + + return width != null && width > NumberUtils.INTEGER_ZERO + ? width.toString() : null; + } + + private String makeUrl(Integer domainId) { + final String host = resolveHost(domainId); + final String url = endpointUrl.replace(URL_HOST_MACRO, host); + try { + HttpUtil.validateUrl(Objects.requireNonNull(url)); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + + return url; + } + + private String resolveHost(Integer domainId) { + if (domainId == null) { + return "bid.videostep.com"; + } else if (domainId >= 1002) { + return String.format("bid%s.videostep.com", domainId - 1000); + } else if (domainId == 1) { + return "adweb.videostepstage.com"; + } else if (domainId == 2) { + return "adweb.invibesstage.com"; + } else { + return "bid.videostep.com"; + } + } + + private InvibesInternalParams updateWithGDPRParams(InvibesInternalParams internalParams, + String consentString, Boolean gdprApplies) { + return internalParams.toBuilder() + .gdpr(gdprApplies) + .gdprConsent(consentString) + .build(); + } + + private List getUpdatedPlacementIds(String placementId, List placementIds) { + final List updatedPlacementIds = placementIds != null + ? placementIds : new ArrayList<>(); + if (StringUtils.isNotEmpty(placementId)) { + updatedPlacementIds.add(placementId.trim()); + } + return updatedPlacementIds; + } + + private Map getUpdatedProperties( + Map properties, String placementId, + String impId, List adFormats) { + properties.put(placementId, InvibesPlacementProperty.builder() + .impId(impId) + .formats(adFormats) + .build()); + + return properties; + } + + private List resolveAdFormats(Banner currentBanner) { + if (currentBanner.getFormat() != null) { + return currentBanner.getFormat(); + } else if (currentBanner.getW() != null && currentBanner.getH() != null) { + return Collections.singletonList(Format.builder() + .w(currentBanner.getW()) + .h(currentBanner.getH()) + .build()); + } + + return Collections.emptyList(); + } + + @Override + public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + final int statusCode = httpCall.getResponse().getStatusCode(); + if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { + return Result.of(Collections.emptyList(), Collections.emptyList()); + } + + try { + final InvibesBidderResponse bidResponse = + mapper.decodeValue(httpCall.getResponse().getBody(), InvibesBidderResponse.class); + if (bidResponse != null && StringUtils.isNotEmpty(bidResponse.getError())) { + return Result.emptyWithError( + BidderError.badServerResponse(String.format("Server error: %s.", bidResponse.getError()))); + } + return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + } catch (DecodeException | PreBidException e) { + return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(InvibesBidRequest bidRequest, InvibesBidderResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getTypedBids())) { + return Collections.emptyList(); + } + + return bidsFromResponse(bidRequest, bidResponse); + } + + private List bidsFromResponse(InvibesBidRequest bidRequest, InvibesBidderResponse bidResponse) { + return bidResponse.getTypedBids().stream() + .filter(Objects::nonNull) + .map(InvibesTypedBid::getBid) + .filter(Objects::nonNull) + //TODO add DealPriority + .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCurrency())) + .collect(Collectors.toList()); + } + + @Override + public Map extractTargeting(ObjectNode ext) { + return Collections.emptyMap(); + } +} diff --git a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidParams.java b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidParams.java new file mode 100644 index 00000000000..792785fdccf --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidParams.java @@ -0,0 +1,22 @@ +package org.prebid.server.bidder.invibes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Builder(toBuilder = true) +@Value +public class InvibesBidParams { + + @JsonProperty("PlacementIds") + List placementIDs; + + @JsonProperty("BidVersion") + String bidVersion; + + @JsonProperty("Properties") + Map properties; +} diff --git a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidRequest.java b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidRequest.java new file mode 100644 index 00000000000..2a858208271 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidRequest.java @@ -0,0 +1,49 @@ +package org.prebid.server.bidder.invibes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class InvibesBidRequest { + + @JsonProperty("BidParamsJson") + String bidParamsJson; + + @JsonProperty("Location") + String location; + + @JsonProperty("Lid") + String lid; + + @JsonProperty("IsTestBid") + Boolean isTestBid; + + @JsonProperty("Kw") + String kw; + + @JsonProperty("IsAMP") + Boolean isAMP; + + @JsonProperty("Width") + String width; + + @JsonProperty("Height") + String height; + + @JsonProperty("GdprConsent") + String gdprConsent; + + @JsonProperty("Gdpr") + Boolean gdpr; + + @JsonProperty("Bvid") + String bvid; + + @JsonProperty("InvibBVLog") + Boolean invibBVLog; + + @JsonProperty("VideoAdDebug") + Boolean videoAdDebug; +} diff --git a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidderResponse.java b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidderResponse.java new file mode 100644 index 00000000000..74c8f899701 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidderResponse.java @@ -0,0 +1,19 @@ +package org.prebid.server.bidder.invibes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder +@Value +public class InvibesBidderResponse { + + String currency; + + @JsonProperty("typedBids") + List typedBids; + + String error; +} diff --git a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesInternalParams.java b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesInternalParams.java new file mode 100644 index 00000000000..eb0fa91ca31 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesInternalParams.java @@ -0,0 +1,23 @@ +package org.prebid.server.bidder.invibes.model; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class InvibesInternalParams { + + InvibesBidParams bidParams; + + Integer domainID; + + Boolean isAMP; + + Boolean gdpr; + + String gdprConsent; + + String testBvid; + + Boolean testLog; +} diff --git a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesPlacementProperty.java b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesPlacementProperty.java new file mode 100644 index 00000000000..916d8079b16 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesPlacementProperty.java @@ -0,0 +1,19 @@ +package org.prebid.server.bidder.invibes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.request.Format; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder +@Value +public class InvibesPlacementProperty { + + @JsonProperty("Formats") + List formats; + + @JsonProperty("ImpID") + String impId; +} diff --git a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesTypedBid.java b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesTypedBid.java new file mode 100644 index 00000000000..14dedddf3ae --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesTypedBid.java @@ -0,0 +1,17 @@ +package org.prebid.server.bidder.invibes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import com.iab.openrtb.response.Bid; + +@Builder +@Value +public class InvibesTypedBid { + + Bid bid; + + @JsonProperty("dealPriority") + Integer dealPriority; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/ExtImpInvibes.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/ExtImpInvibes.java new file mode 100644 index 00000000000..1de204b5755 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/ExtImpInvibes.java @@ -0,0 +1,20 @@ +package org.prebid.server.proto.openrtb.ext.request.invibes; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.invibes.model.InvibesDebug; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpInvibes { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("domainId") + Integer domainId; + + @JsonProperty("debug") + InvibesDebug debug; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/model/InvibesDebug.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/model/InvibesDebug.java new file mode 100644 index 00000000000..f3df563e03f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/model/InvibesDebug.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.invibes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class InvibesDebug { + + @JsonProperty("testBvid") + String testBvid; + + @JsonProperty("testLog") + Boolean testLog; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/InvibesConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/InvibesConfiguration.java new file mode 100644 index 00000000000..d1694a86dce --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/InvibesConfiguration.java @@ -0,0 +1,56 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.invibes.InvibesBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties; +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/invibes.yaml", factory = YamlPropertySourceFactory.class) +public class InvibesConfiguration { + + private static final String BIDDER_NAME = "invibes"; + + @Value("${external-url}") + @NotBlank + private String externalUrl; + + @Autowired + private JacksonMapper mapper; + + @Autowired + @Qualifier("invibesConfigurationProperties") + private BidderConfigurationProperties configProperties; + + @Bean("invibesConfigurationProperties") + @ConfigurationProperties("adapters.invibes") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps invibesBidderDeps() { + final UsersyncConfigurationProperties usersync = configProperties.getUsersync(); + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(configProperties) + .bidderInfo(BidderInfoCreator.create(configProperties)) + .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl)) + .bidderCreator(() -> new InvibesBidder(configProperties.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/invibes.yaml b/src/main/resources/bidder-config/invibes.yaml new file mode 100644 index 00000000000..cda9d73d277 --- /dev/null +++ b/src/main/resources/bidder-config/invibes.yaml @@ -0,0 +1,23 @@ +adapters: + invibes: + enabled: false + endpoint: https://{{Host}}/bid/ServerBidAdContent + pbs-enforces-gdpr: true + pbs-enforces-ccpa: true + modifying-vast-xml-allowed: true + deprecated-names: + aliases: + meta-info: + maintainer-email: system_operations@invibes.com + app-media-types: + - banner + site-media-types: + - banner + supported-vendors: + vendor-id: 436 + usersync: + url: https://u2.videostep.com/User/getLid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirectUri= + redirect-url: /setuid?bidder=invibes&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID + cookie-family-name: invibes + type: redirect + support-cors: false diff --git a/src/main/resources/static/bidder-params/invibes.json b/src/main/resources/static/bidder-params/invibes.json new file mode 100644 index 00000000000..11d276f8d3e --- /dev/null +++ b/src/main/resources/static/bidder-params/invibes.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Invibes Adapter Params", + "description": "A schema which validates params accepted by the Invibes adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "An ID which identifies the site selling the impression" + }, + "domainId": { + "type": "integer", + "description": "Ad domain id" + }, + "debug": { + "type": "object", + "properties": { + "testBvid": { + "type": "string" + }, + "testLog": { + "type": "boolean" + } + }, + "description": "Parameters used for debugging porposes" + } + }, + "required": ["placementId"] +} diff --git a/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java new file mode 100644 index 00000000000..57ece91e869 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java @@ -0,0 +1,242 @@ +package org.prebid.server.bidder.invibes; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import org.apache.commons.lang3.StringUtils; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.invibes.model.InvibesBidRequest; +import org.prebid.server.bidder.invibes.model.InvibesBidderResponse; +import org.prebid.server.bidder.invibes.model.InvibesTypedBid; +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.invibes.ExtImpInvibes; +import org.prebid.server.proto.openrtb.ext.request.invibes.model.InvibesDebug; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.function.Function.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; + +public class InvibesBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://{{Host}}/test"; + + private InvibesBidder invibesBidder; + + @Before + public void setUp() { + invibesBidder = new InvibesBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + Assertions.assertThatIllegalArgumentException().isThrownBy(() -> + new InvibesBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldCreateCorrectURL() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), ExtImpInvibes + .of("12", 1003, InvibesDebug.of("test", true))) + .toBuilder().site(Site.builder().page("www.test.com").build()).build(); + + // when + final Result>> result = invibesBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().get(0).getUri()).isEqualTo("https://bid3.videostep.com/test"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) + .build())) + .build(); + + // when + final Result>> result = invibesBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()).startsWith("Error parsing invibesExt parameters"); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenBannerIsNull() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), ExtImpInvibes + .of("12", 15, InvibesDebug.of("test", true))); + + final BidRequest bidRequestWithoutBanner = bidRequest.toBuilder().imp( + Collections.singletonList(bidRequest.getImp().get(0).toBuilder().banner(null).build())).build(); + + // when + final Result>> result = + invibesBidder.makeHttpRequests(bidRequestWithoutBanner); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Banner not specified")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenSiteIsNotPresent() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), ExtImpInvibes + .of("12", 15, InvibesDebug.of("test", true))); + + // when + final Result>> result = invibesBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Site not specified")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenSiteIsNotPresent2() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), ExtImpInvibes + .of("12", 15, InvibesDebug.of("test", true))).toBuilder() + .site(Site.builder().page("www.awesome-page.com").build()).build(); + + // when + final Result>> result = invibesBidder.makeHttpRequests(bidRequest); + + // then + final InvibesBidRequest expectedRequest = InvibesBidRequest.builder() + .bidParamsJson("{\"PlacementIds\":[\"12\"],\"BidVersion\":\"4\"," + + "\"Properties\":{\"12\":{\"Formats\":[{\"w\":15,\"h\":12}]}}}") + .isTestBid(Boolean.TRUE) + .location("www.awesome-page.com") + .gdpr(Boolean.TRUE) + .gdprConsent(StringUtils.EMPTY) + .invibBVLog(Boolean.TRUE) + .videoAdDebug(Boolean.TRUE) + .lid(StringUtils.EMPTY) + .bvid("test") + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).extracting(HttpRequest::getPayload).containsOnly(expectedRequest); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final HttpCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = invibesBidder.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 makeBidsShouldReturnEmptyListWhenBidResponseIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(null)); + + // when + final Result> result = invibesBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + InvibesBidRequest.builder() + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = invibesBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR")); + } + + @Test + public void extractTargetingShouldReturnEmptyMap() { + assertThat(invibesBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); + } + + private static InvibesBidderResponse givenBidResponse(Function bidCustomizer) { + return InvibesBidderResponse.builder() + .typedBids(singletonList(InvibesTypedBid.builder() + .bid(bidCustomizer.apply(Bid.builder()).build()) + .dealPriority(12) + .build())) + .currency("EUR") + .build(); + } + + private static HttpCall givenHttpCall(InvibesBidRequest bidRequest, String body) { + return HttpCall.success( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } + + private static BidRequest givenBidRequest( + Function bidRequestCustomizer, + Function impCustomizer, + ExtImpInvibes extImpInvibes) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer, extImpInvibes)))) + .build(); + } + + private static BidRequest givenBidRequest(Function impCustomizer, + ExtImpInvibes extImpInvibes) { + return givenBidRequest(identity(), impCustomizer, extImpInvibes); + } + + private static Imp givenImp(Function impCustomizer, + ExtImpInvibes extImpInvibes) { + return impCustomizer.apply(Imp.builder() + .ext(mapper.valueToTree( + ExtPrebid.of(null, extImpInvibes)))) + .banner(Banner.builder().h(12).w(15).build()) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/it/InvibesTest.java b/src/test/java/org/prebid/server/it/InvibesTest.java new file mode 100644 index 00000000000..bef8a174bea --- /dev/null +++ b/src/test/java/org/prebid/server/it/InvibesTest.java @@ -0,0 +1,60 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +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; + +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 io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; + +@RunWith(SpringRunner.class) +public class InvibesTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromInvibes() throws IOException, JSONException { + // given + // InvibesBidder bid response for imp 001 + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/invibes-exchange")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) + .withHeader("Referer", equalTo("http://www.example.com")) + .withHeader("Aver", equalTo("prebid_1.0.0")) + .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/invibes/test-invibes-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/invibes/test-invibes-bid-response.json")))); + + // pre-bid cache + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/invibes/test-cache-invibes-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/invibes/test-cache-invibes-response.json")))); + + // when + final Response response = given(SPEC) + .header("Referer", "http://www.example.com") + .header("X-Forwarded-For", "193.168.244.1") + .header("User-Agent", "userAgent") + .header("Origin", "http://www.example.com") + // this uids cookie value stands for {"uids":{"invibes":"IV-UID"}} + .cookie("uids", "eyJ1aWRzIjp7ImludmliZXMiOiJJVi1VSUQifX0=") + .body(jsonFrom("openrtb2/invibes/test-auction-invibes-request.json")) + .post("/openrtb2/auction"); + + // then + final String expectedAuctionResponse = openrtbAuctionResponseFrom( + "openrtb2/invibes/test-auction-invibes-response.json", + response, singletonList("invibes")); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-request.json b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-request.json new file mode 100644 index 00000000000..77da3269f4f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-request.json @@ -0,0 +1,90 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId001", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", + "ext": { + "invibes": { + "placementId": "plcId", + "domainId": 1001, + "debug": { + "testBvid": "", + "testLog": false + } + } + } + } + ], + "device": { + "pxratio": 4.2, + "dnt": 2, + "ip": "193.168.244.1", + "language": "en", + "ifa": "ifaId", + "ua": "userAgent" + }, + "site": { + "domain": "example.com", + "ext": { + "amp": 0 + }, + "page": "http://www.example.com", + "publisher": { + "id": "publisherId" + } + }, + "at": 1, + "tmax": 3000, + "cur": [ + "USD" + ], + "source": { + "fd": 1, + "tid": "tid" + }, + "user": { + "ext": { + "consent": "consentValue", + "digitrust": { + "id": "id", + "keyv": 123, + "pref": 0 + } + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "targeting": { + "includebidderkeys": true, + "includewinners": true, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + } + }, + "cache": { + "bids": {} + }, + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json new file mode 100644 index 00000000000..f013f8816aa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json @@ -0,0 +1,58 @@ +{ + "id":"tid", + "seatbid":[ + { + "bid":[ + { + "id":"bid001", + "impid":"impId001", + "price":1.3, + "adm":"adm001", + "adid":"adid001", + "cid":"cid001", + "crid":"crid001", + "w":300, + "h":250, + "ext":{ + "prebid":{ + "type":"banner", + "targeting":{ + "hb_pb_invibes":"1.30", + "hb_cache_id_invibes":"f0ab9105-cb21-4e59-b433-70f5ad6671cb", + "hb_bidder_invibes":"invibes", + "hb_cache_id":"f0ab9105-cb21-4e59-b433-70f5ad6671cb", + "hb_pb":"1.30", + "hb_cache_path":"/cache", + "hb_size":"300x250", + "hb_bidder":"invibes", + "hb_size_invibes":"300x250", + "hb_cache_host":"{{ cache.host }}", + "hb_cache_path_invibes":"{{ cache.path }}", + "hb_cache_host_invibes": "{{ cache.host }}" + }, + "cache":{ + "bids":{ + "url":"http://localhost:8090/cache?uuid=f0ab9105-cb21-4e59-b433-70f5ad6671cb", + "cacheId":"f0ab9105-cb21-4e59-b433-70f5ad6671cb" + } + } + } + } + } + ], + "seat":"invibes", + "group":0 + } + ], + "cur":"USD", + "ext":{ + "responsetimemillis":{ + "cache":"{{ cache.response_time_ms }}", + "invibes":"{{ invibes.response_time_ms }}" + }, + "tmaxrequest":3000, + "prebid":{ + "auctiontimestamp":1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-cache-invibes-request.json b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-cache-invibes-request.json new file mode 100644 index 00000000000..7007994992c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-cache-invibes-request.json @@ -0,0 +1,18 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "bid001", + "impid": "impId001", + "price": 1.3, + "adm": "adm001", + "adid" : "adid001", + "cid" : "cid001", + "crid" : "crid001", + "w" : 300, + "h" : 250 + } + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-cache-invibes-response.json b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-cache-invibes-response.json new file mode 100644 index 00000000000..93d0b8de2cd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-cache-invibes-response.json @@ -0,0 +1,7 @@ +{ + "responses": [ + { + "uuid": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-invibes-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-invibes-bid-request.json new file mode 100644 index 00000000000..5f5a1df81d2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-invibes-bid-request.json @@ -0,0 +1,10 @@ +{ + "BidParamsJson" :"{\"PlacementIds\":[\"plcId\"],\"BidVersion\":\"4\",\"Properties\":{\"plcId\":{\"Formats\":[{\"w\":300,\"h\":250}],\"ImpID\":\"impId001\"}}}", + "Location" :"http://www.example.com", + "Lid" : "IV-UID", + "IsTestBid" : false, + "GdprConsent" :"consentValue", + "Gdpr" : false, + "InvibBVLog" : false, + "VideoAdDebug" : false +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-invibes-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-invibes-bid-response.json new file mode 100644 index 00000000000..bee6af29e05 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-invibes-bid-response.json @@ -0,0 +1,19 @@ +{ + "currency": "USD", + "typedBids": [ + { + "bid": { + "id": "bid001", + "impid": "impId001", + "price": 1.3, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + }, + "dealPriority": 12 + } + ] +} 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 b1d01d1af2f..0b56f6ae00d 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -302,6 +302,10 @@ adapters.inmobi.enabled=true adapters.inmobi.endpoint=http://localhost:8090/inmobi-exchange adapters.inmobi.pbs-enforces-gdpr=true adapters.inmobi.usersync.url=//inmobi-usersync +adapters.invibes.enabled=true +adapters.invibes.endpoint=http://localhost:8090/invibes-exchange +adapters.invibes.pbs-enforces-gdpr=true +adapters.invibes.usersync.url=//invibes-usersync adapters.yeahmobi.enabled=true adapters.yeahmobi.endpoint=http://localhost:8090/yeahmobi-exchange adapters.yeahmobi.pbs-enforces-gdpr=true From cb91ab2bcbbeb04fedfc965b7a4d66ddf9c7d8a8 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Thu, 22 Oct 2020 22:12:16 +0300 Subject: [PATCH 2/4] Fixes after review --- .../server/bidder/invibes/InvibesBidder.java | 274 ++++++++---------- .../invibes/model/InvibesBidParams.java | 2 +- .../invibes/model/InvibesBidRequest.java | 2 +- .../invibes/model/InvibesInternalParams.java | 12 +- src/main/resources/bidder-config/invibes.yaml | 2 - .../bidder/invibes/InvibesBidderTest.java | 148 +++++++--- 6 files changed, 248 insertions(+), 192 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java index fe1c64b2c3f..e68e8fc1d0d 100644 --- a/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java +++ b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java @@ -35,6 +35,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.invibes.ExtImpInvibes; +import org.prebid.server.proto.openrtb.ext.request.invibes.model.InvibesDebug; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -70,25 +71,20 @@ public Result>> makeHttpRequests(BidRequest final String consentString = resolveConsentString(request.getUser()); final Boolean gdprApplies = resolveGDPRApplies(request.getRegs()); - InvibesInternalParams invibesInternalParams = - InvibesInternalParams.builder() - .bidParams(InvibesBidParams.builder() - .properties(new HashMap()) - .bidVersion(INVIBES_BID_VERSION) - .build()) - .build(); + InvibesInternalParams invibesInternalParams = new InvibesInternalParams(); + invibesInternalParams.setBidParams(InvibesBidParams.builder() + .properties(new HashMap<>()) + .placementIds(new ArrayList<>()) + .bidVersion(INVIBES_BID_VERSION) + .build()); - for (final Imp imp : request.getImp()) { + for (Imp imp : request.getImp()) { final ExtImpInvibes extImpInvibes; try { - extImpInvibes = mapper.mapper().convertValue(imp.getExt(), INVIBES_EXT_TYPE_REFERENCE).getBidder(); - } catch (IllegalArgumentException e) { - errors.add(BidderError.badInput("Error parsing invibesExt parameters")); - continue; - } - final Banner banner = imp.getBanner(); - if (banner == null) { - errors.add(BidderError.badInput("Banner not specified")); + extImpInvibes = parseImpExt(imp); + validateImp(imp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); continue; } @@ -96,15 +92,18 @@ public Result>> makeHttpRequests(BidRequest } //TODO add AMP parameter to invibesInternalParams, after reqInfo will be implemented - if (invibesInternalParams.getBidParams() - .getPlacementIDs() == null - || invibesInternalParams.getBidParams().getPlacementIDs().size() == 0) { + final List placementIds = invibesInternalParams.getBidParams().getPlacementIds(); + if (CollectionUtils.isEmpty(placementIds)) { return Result.of(Collections.emptyList(), errors); } - invibesInternalParams = updateWithGDPRParams(invibesInternalParams, consentString, gdprApplies); + invibesInternalParams.setGdpr(gdprApplies); + invibesInternalParams.setGdprConsent(consentString); try { + if (request.getSite() == null) { + throw new PreBidException("Site not specified"); + } final HttpRequest httpRequest = makeRequest(invibesInternalParams, request); return Result.of(Collections.singletonList(httpRequest), errors); } catch (PreBidException e) { @@ -112,13 +111,23 @@ public Result>> makeHttpRequests(BidRequest } } - private String resolveConsentString(User user) { - final ExtUser extUser = user != null ? user.getExt() : null; - if (extUser != null) { - return extUser.getConsent(); + private ExtImpInvibes parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), INVIBES_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing invibesExt parameters"); + } + } + + private void validateImp(Imp imp) { + if (imp.getBanner() == null) { + throw new PreBidException("Banner not specified"); } + } - return StringUtils.EMPTY; + private String resolveConsentString(User user) { + final ExtUser extUser = user != null ? user.getExt() : null; + return extUser != null ? extUser.getConsent() : StringUtils.EMPTY; } private Boolean resolveGDPRApplies(Regs regs) { @@ -128,42 +137,67 @@ private Boolean resolveGDPRApplies(Regs regs) { return gdpr != null ? gdpr == 1 : Boolean.TRUE; } - private InvibesInternalParams updateInvibesInternalParams( - InvibesInternalParams invibesInternalParams, ExtImpInvibes invibesExt, - Imp imp) { - final String placementId = invibesExt.getPlacementId(); - final List updatedPlacementIds = getUpdatedPlacementIds( - placementId, invibesInternalParams.getBidParams().getPlacementIDs()); - - final Map updatedProperties - = getUpdatedProperties(invibesInternalParams.getBidParams().getProperties(), - placementId, imp.getId(), resolveAdFormats(imp.getBanner())); + private InvibesInternalParams updateInvibesInternalParams(InvibesInternalParams invibesInternalParams, + ExtImpInvibes invibesExt, + Imp imp) { + final String impExtPlacementId = invibesExt.getPlacementId(); + final InvibesBidParams bidParams = invibesInternalParams.getBidParams(); + final List updatedPlacementIds = bidParams.getPlacementIds(); - final InvibesBidParams updatedBidParams = invibesInternalParams.getBidParams().toBuilder() - .placementIDs(updatedPlacementIds) - .properties(updatedProperties) + if (StringUtils.isNotBlank(impExtPlacementId)) { + updatedPlacementIds.add(impExtPlacementId.trim()); + } + final Banner banner = imp.getBanner(); + final List adFormats = resolveAdFormats(banner); + + bidParams.getProperties() + .put(impExtPlacementId, InvibesPlacementProperty.builder() + .impId(imp.getId()) + .formats(adFormats) + .build()); + + final InvibesBidParams updatedBidParams = bidParams.toBuilder() + .placementIds(updatedPlacementIds) + .properties(bidParams.getProperties()) .build(); - final InvibesInternalParams.InvibesInternalParamsBuilder internalParamsBuilder - = invibesInternalParams.toBuilder() - .domainID(invibesExt.getDomainId()) - .bidParams(updatedBidParams); + invibesInternalParams.setDomainId(invibesExt.getDomainId()); + invibesInternalParams.setBidParams(updatedBidParams); - if (invibesExt.getDebug() != null) { - if (!invibesExt.getDebug().getTestBvid().equals(StringUtils.EMPTY)) { - internalParamsBuilder.testBvid(invibesExt.getDebug().getTestBvid()); - } - internalParamsBuilder.testLog(invibesExt.getDebug().getTestLog()); + final InvibesDebug invibesDebug = invibesExt.getDebug(); + final String invibesDebugTestBvid = invibesDebug != null ? invibesDebug.getTestBvid() : null; + if (StringUtils.isNotBlank(invibesDebugTestBvid)) { + invibesInternalParams.setTestBvid(invibesDebugTestBvid); } - return internalParamsBuilder.build(); + if (invibesDebug != null) { + invibesInternalParams.setTestLog(invibesDebug.getTestLog()); + } + + return invibesInternalParams; } - private HttpRequest makeRequest(InvibesInternalParams invibesParams, BidRequest request) { - final String url = makeUrl(invibesParams.getDomainID()); + private List resolveAdFormats(Banner currentBanner) { + if (currentBanner.getFormat() != null) { + return currentBanner.getFormat(); + } else { + final Integer formatW = currentBanner.getW(); + final Integer formatH = currentBanner.getH(); + return formatW != null && formatH != null + ? Collections.singletonList(Format.builder().w(formatW).h(formatH).build()) + : Collections.emptyList(); + } + } + + private HttpRequest makeRequest(InvibesInternalParams invibesParams, + BidRequest request) { + final String host = resolveHost(invibesParams.getDomainId()); + final String url = endpointUrl.replace(URL_HOST_MACRO, host); final InvibesBidRequest parameter = resolveParameter(invibesParams, request); - final MultiMap headers = resolveHeaders(request.getDevice(), request.getSite()); + final Device device = request.getDevice(); + final Site site = request.getSite(); + final MultiMap headers = resolveHeaders(device, site); final String body = mapper.encode(parameter); @@ -174,75 +208,48 @@ private HttpRequest makeRequest(InvibesInternalParams invibes .payload(parameter) .body(body) .build(); - - } - - private MultiMap resolveHeaders(Device device, Site site) { - final MultiMap headers = HttpUtil.headers(); - if (device != null) { - if (StringUtils.isNotEmpty(device.getIp())) { - headers.add("X-Forwarded-For", device.getIp()); - } else if (StringUtils.isNotEmpty(device.getIpv6())) { - headers.add("X-Forwarded-For", device.getIpv6()); - } - } - if (site != null) { - headers.add("Referer", site.getPage()); - } - headers.add("Aver", ADAPTER_VERSION); - return headers; } private InvibesBidRequest resolveParameter(InvibesInternalParams invibesParams, BidRequest request) { - final String buyeruid = request.getUser() != null ? request.getUser().getBuyeruid() : null; - final String lid = StringUtils.isNotBlank(buyeruid) ? request.getUser().getBuyeruid() : StringUtils.EMPTY; + final User user = request.getUser(); + final String buyeruid = user != null ? user.getBuyeruid() : null; + final String lid = StringUtils.isNotBlank(buyeruid) ? buyeruid : StringUtils.EMPTY; - if (request.getSite() == null) { - throw new PreBidException("Site not specified"); - } + return createRequest(invibesParams, lid, request.getDevice(), request.getSite()); + } + + private InvibesBidRequest createRequest(InvibesInternalParams invibesParams, String lid, + Device device, Site site) { + final String testBvid = invibesParams.getTestBvid(); + final Boolean testLog = invibesParams.getTestLog(); return InvibesBidRequest.builder() - .isTestBid(invibesParams.getTestBvid() != null - && !invibesParams.getTestBvid().equals(StringUtils.EMPTY)) + .isTestBid(StringUtils.isNotBlank(testBvid)) .bidParamsJson(mapper.encode(invibesParams.getBidParams())) - .location(request.getSite().getPage()) + .location(site.getPage()) .lid(lid) - .kw(request.getSite().getKeywords()) - .isAMP(invibesParams.getIsAMP()) - .width(resolveWidth(request.getDevice())) - .height(resolveHeight(request.getDevice())) + .kw(site.getKeywords()) + .isAmp(invibesParams.getIsAmp()) + .width(resolveWidth(device)) + .height(resolveHeight(device)) .gdprConsent(invibesParams.getGdprConsent()) .gdpr(invibesParams.getGdpr()) - .bvid(invibesParams.getTestBvid()) - .invibBVLog(invibesParams.getTestLog()) - .videoAdDebug(invibesParams.getTestLog()) + .bvid(testBvid) + .invibBVLog(testLog) + .videoAdDebug(testLog) .build(); } - private String resolveHeight(Device device) { + private static String resolveHeight(Device device) { final Integer height = device != null ? device.getH() : null; - return height != null && height > NumberUtils.INTEGER_ZERO - ? height.toString() : null; + return height != null && height > NumberUtils.INTEGER_ZERO ? height.toString() : null; } - private String resolveWidth(Device device) { + private static String resolveWidth(Device device) { final Integer width = device != null ? device.getW() : null; - return width != null && width > NumberUtils.INTEGER_ZERO - ? width.toString() : null; - } - - private String makeUrl(Integer domainId) { - final String host = resolveHost(domainId); - final String url = endpointUrl.replace(URL_HOST_MACRO, host); - try { - HttpUtil.validateUrl(Objects.requireNonNull(url)); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage()); - } - - return url; + return width != null && width > NumberUtils.INTEGER_ZERO ? width.toString() : null; } private String resolveHost(Integer domainId) { @@ -259,45 +266,20 @@ private String resolveHost(Integer domainId) { } } - private InvibesInternalParams updateWithGDPRParams(InvibesInternalParams internalParams, - String consentString, Boolean gdprApplies) { - return internalParams.toBuilder() - .gdpr(gdprApplies) - .gdprConsent(consentString) - .build(); - } - - private List getUpdatedPlacementIds(String placementId, List placementIds) { - final List updatedPlacementIds = placementIds != null - ? placementIds : new ArrayList<>(); - if (StringUtils.isNotEmpty(placementId)) { - updatedPlacementIds.add(placementId.trim()); + private MultiMap resolveHeaders(Device device, Site site) { + final MultiMap headers = HttpUtil.headers(); + if (device != null) { + if (StringUtils.isNotBlank(device.getIp())) { + headers.add("X-Forwarded-For", device.getIp()); + } else if (StringUtils.isNotBlank(device.getIpv6())) { + headers.add("X-Forwarded-For", device.getIpv6()); + } } - return updatedPlacementIds; - } - - private Map getUpdatedProperties( - Map properties, String placementId, - String impId, List adFormats) { - properties.put(placementId, InvibesPlacementProperty.builder() - .impId(impId) - .formats(adFormats) - .build()); - - return properties; - } - - private List resolveAdFormats(Banner currentBanner) { - if (currentBanner.getFormat() != null) { - return currentBanner.getFormat(); - } else if (currentBanner.getW() != null && currentBanner.getH() != null) { - return Collections.singletonList(Format.builder() - .w(currentBanner.getW()) - .h(currentBanner.getH()) - .build()); + if (site != null) { + headers.add("Referer", site.getPage()); } - - return Collections.emptyList(); + headers.add("Aver", ADAPTER_VERSION); + return headers; } @Override @@ -310,25 +292,23 @@ public final Result> makeBids(HttpCall httpCa try { final InvibesBidderResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), InvibesBidderResponse.class); - if (bidResponse != null && StringUtils.isNotEmpty(bidResponse.getError())) { + if (bidResponse != null && StringUtils.isNotBlank(bidResponse.getError())) { return Result.emptyWithError( BidderError.badServerResponse(String.format("Server error: %s.", bidResponse.getError()))); } - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.of(extractBids(bidResponse), Collections.emptyList()); } catch (DecodeException | PreBidException e) { return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(InvibesBidRequest bidRequest, InvibesBidderResponse bidResponse) { - if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getTypedBids())) { - return Collections.emptyList(); - } - - return bidsFromResponse(bidRequest, bidResponse); + private List extractBids(InvibesBidderResponse bidResponse) { + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getTypedBids()) + ? Collections.emptyList() + : bidsFromResponse(bidResponse); } - private List bidsFromResponse(InvibesBidRequest bidRequest, InvibesBidderResponse bidResponse) { + private List bidsFromResponse(InvibesBidderResponse bidResponse) { return bidResponse.getTypedBids().stream() .filter(Objects::nonNull) .map(InvibesTypedBid::getBid) diff --git a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidParams.java b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidParams.java index 792785fdccf..1cb4437063b 100644 --- a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidParams.java +++ b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidParams.java @@ -12,7 +12,7 @@ public class InvibesBidParams { @JsonProperty("PlacementIds") - List placementIDs; + List placementIds; @JsonProperty("BidVersion") String bidVersion; diff --git a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidRequest.java b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidRequest.java index 2a858208271..b00a5129334 100644 --- a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidRequest.java +++ b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesBidRequest.java @@ -24,7 +24,7 @@ public class InvibesBidRequest { String kw; @JsonProperty("IsAMP") - Boolean isAMP; + Boolean isAmp; @JsonProperty("Width") String width; diff --git a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesInternalParams.java b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesInternalParams.java index eb0fa91ca31..7c81a480e87 100644 --- a/src/main/java/org/prebid/server/bidder/invibes/model/InvibesInternalParams.java +++ b/src/main/java/org/prebid/server/bidder/invibes/model/InvibesInternalParams.java @@ -1,17 +1,17 @@ package org.prebid.server.bidder.invibes.model; -import lombok.Builder; -import lombok.Value; +import lombok.Data; +import lombok.NoArgsConstructor; -@Builder(toBuilder = true) -@Value +@Data +@NoArgsConstructor public class InvibesInternalParams { InvibesBidParams bidParams; - Integer domainID; + Integer domainId; - Boolean isAMP; + Boolean isAmp; Boolean gdpr; diff --git a/src/main/resources/bidder-config/invibes.yaml b/src/main/resources/bidder-config/invibes.yaml index cda9d73d277..f1fb9bb9886 100644 --- a/src/main/resources/bidder-config/invibes.yaml +++ b/src/main/resources/bidder-config/invibes.yaml @@ -9,8 +9,6 @@ adapters: aliases: meta-info: maintainer-email: system_operations@invibes.com - app-media-types: - - banner site-media-types: - banner supported-vendors: diff --git a/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java index 57ece91e869..2e63bdf597a 100644 --- a/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java @@ -3,16 +3,21 @@ import com.fasterxml.jackson.core.JsonProcessingException; 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.User; import com.iab.openrtb.response.Bid; import org.apache.commons.lang3.StringUtils; import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; import org.prebid.server.VertxTest; +import org.prebid.server.bidder.invibes.model.InvibesBidParams; import org.prebid.server.bidder.invibes.model.InvibesBidRequest; import org.prebid.server.bidder.invibes.model.InvibesBidderResponse; +import org.prebid.server.bidder.invibes.model.InvibesPlacementProperty; import org.prebid.server.bidder.invibes.model.InvibesTypedBid; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -24,8 +29,11 @@ import org.prebid.server.proto.openrtb.ext.request.invibes.ExtImpInvibes; import org.prebid.server.proto.openrtb.ext.request.invibes.model.InvibesDebug; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.Function; import static java.util.Collections.emptyMap; @@ -54,9 +62,10 @@ public void creationShouldFailOnInvalidEndpointUrl() { @Test public void makeHttpRequestsShouldCreateCorrectURL() { // given - final BidRequest bidRequest = givenBidRequest(identity(), ExtImpInvibes - .of("12", 1003, InvibesDebug.of("test", true))) - .toBuilder().site(Site.builder().page("www.test.com").build()).build(); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.site(Site.builder().page("www.test.com").build()), + impBuilder -> impBuilder.banner(Banner.builder().h(12).w(15).build()), + ExtImpInvibes.of("12", 1003, InvibesDebug.of("test", true))); // when final Result>> result = invibesBidder.makeHttpRequests(bidRequest); @@ -88,15 +97,13 @@ public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { @Test public void makeHttpRequestsShouldReturnErrorWhenBannerIsNull() { // given - final BidRequest bidRequest = givenBidRequest(identity(), ExtImpInvibes - .of("12", 15, InvibesDebug.of("test", true))); - - final BidRequest bidRequestWithoutBanner = bidRequest.toBuilder().imp( - Collections.singletonList(bidRequest.getImp().get(0).toBuilder().banner(null).build())).build(); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.site(Site.builder().page("www.awesome-page.com").build()), + impBuilder -> impBuilder.banner(null)); // when final Result>> result = - invibesBidder.makeHttpRequests(bidRequestWithoutBanner); + invibesBidder.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).hasSize(1) @@ -107,8 +114,8 @@ public void makeHttpRequestsShouldReturnErrorWhenBannerIsNull() { @Test public void makeHttpRequestsShouldReturnErrorWhenSiteIsNotPresent() { // given - final BidRequest bidRequest = givenBidRequest(identity(), ExtImpInvibes - .of("12", 15, InvibesDebug.of("test", true))); + final BidRequest bidRequest = givenBidRequest(identity(), + impBuilder -> impBuilder.banner(Banner.builder().h(12).w(15).build())); // when final Result>> result = invibesBidder.makeHttpRequests(bidRequest); @@ -120,31 +127,84 @@ public void makeHttpRequestsShouldReturnErrorWhenSiteIsNotPresent() { } @Test - public void makeHttpRequestsShouldReturnErrorWhenSiteIsNotPresent2() { + public void shouldCreateRequestWithDataFromEveryImpression() throws JsonProcessingException { + // given + final List imps = Arrays.asList(givenImp( + impBuilder -> impBuilder + .banner(Banner.builder().h(10).w(11).build()), ExtImpInvibes.of("12", 15, + InvibesDebug.of("test1", true))), + givenImp(impBuilder -> impBuilder + .banner(Banner.builder().h(14).w(15).build()), ExtImpInvibes.of("15", 1001, + InvibesDebug.of("test2", false))) + ); + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().page("www.awesome-page.com").build()) + .imp(imps) + .build(); + // when + final Result>> result = invibesBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload).hasSize(1); + InvibesBidParams expectedBidParams = + mapper.readValue(result.getValue().get(0).getPayload().getBidParamsJson(), InvibesBidParams.class); + assertThat(expectedBidParams.getPlacementIds()).hasSize(2) + .containsOnly("12", "15"); + final Format firstExpectedFormat = Format.builder().w(11).h(10).build(); + final Format secondExpectedFormat = Format.builder().w(15).h(14).build(); + assertThat(expectedBidParams.getProperties().values()).hasSize(2) + .containsOnly(InvibesPlacementProperty.builder() + .formats(Collections.singletonList(firstExpectedFormat)) + .build(), + InvibesPlacementProperty.builder() + .formats(Collections.singletonList(secondExpectedFormat)) + .build()); + } + + @Test + public void makeHttpRequestsShouldCreateInvibesBidRequestWithCorrectParams() throws JsonProcessingException { // given - final BidRequest bidRequest = givenBidRequest(identity(), ExtImpInvibes - .of("12", 15, InvibesDebug.of("test", true))).toBuilder() - .site(Site.builder().page("www.awesome-page.com").build()).build(); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder + .device(Device.builder().w(77).h(88).build()) + .user(User.builder().buyeruid("someUid").build()) + .site(Site.builder().page("www.awesome-page.com").build()), + impBuilder -> impBuilder.banner(Banner.builder().h(12).w(15).build())); // when final Result>> result = invibesBidder.makeHttpRequests(bidRequest); // then + final Map properties = new HashMap<>(); + properties.put("12", InvibesPlacementProperty.builder() + .formats(Collections.singletonList(Format.builder().w(15).h(12).build())).build()); + + final InvibesBidParams invibesBidParams = InvibesBidParams.builder() + .placementIds(Collections.singletonList("12")) + .bidVersion("4") + .properties(properties) + .build(); + final InvibesBidRequest expectedRequest = InvibesBidRequest.builder() - .bidParamsJson("{\"PlacementIds\":[\"12\"],\"BidVersion\":\"4\"," - + "\"Properties\":{\"12\":{\"Formats\":[{\"w\":15,\"h\":12}]}}}") - .isTestBid(Boolean.TRUE) + .bidParamsJson(mapper.writeValueAsString(invibesBidParams)) + .isTestBid(true) .location("www.awesome-page.com") - .gdpr(Boolean.TRUE) + .gdpr(true) .gdprConsent(StringUtils.EMPTY) - .invibBVLog(Boolean.TRUE) - .videoAdDebug(Boolean.TRUE) - .lid(StringUtils.EMPTY) + .invibBVLog(true) + .videoAdDebug(true) + .lid("someUid") .bvid("test") + .width("77") + .height("88") .build(); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).extracting(HttpRequest::getPayload).containsOnly(expectedRequest); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .containsOnly(expectedRequest); } @Test @@ -165,8 +225,7 @@ public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { @Test public void makeBidsShouldReturnEmptyListWhenBidResponseIsNull() throws JsonProcessingException { // given - final HttpCall httpCall = givenHttpCall(null, - mapper.writeValueAsString(null)); + final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); // when final Result> result = invibesBidder.makeBids(httpCall, null); @@ -180,10 +239,8 @@ public void makeBidsShouldReturnEmptyListWhenBidResponseIsNull() throws JsonProc public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { // given final HttpCall httpCall = givenHttpCall( - InvibesBidRequest.builder() - .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + InvibesBidRequest.builder().build(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); // when final Result> result = invibesBidder.makeBids(httpCall, null); @@ -194,6 +251,23 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR")); } + @Test + public void makeBidsShouldReturnErrorIdBidResponseContainsError() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + InvibesBidRequest.builder().build(), + mapper.writeValueAsString(InvibesBidderResponse.builder().error("someError").build())); + + // when + final Result> result = invibesBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .extracting(BidderError::getMessage) + .containsOnly("Server error: someError."); + assertThat(result.getValue()).isEmpty(); + } + @Test public void extractTargetingShouldReturnEmptyMap() { assertThat(invibesBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); @@ -226,17 +300,21 @@ private static BidRequest givenBidRequest( .build(); } - private static BidRequest givenBidRequest(Function impCustomizer, - ExtImpInvibes extImpInvibes) { - return givenBidRequest(identity(), impCustomizer, extImpInvibes); + private static BidRequest givenBidRequest( + Function bidRequestCustomizer, + Function impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList( + givenImp(impCustomizer, ExtImpInvibes.of("12", 15, + InvibesDebug.of("test", true)))))) + .build(); } private static Imp givenImp(Function impCustomizer, ExtImpInvibes extImpInvibes) { return impCustomizer.apply(Imp.builder() - .ext(mapper.valueToTree( - ExtPrebid.of(null, extImpInvibes)))) - .banner(Banner.builder().h(12).w(15).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, extImpInvibes)))) .build(); } } From f2931e3082c60ab0eb821e9714bac7b713fd1b9d Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Tue, 27 Oct 2020 10:07:34 +0200 Subject: [PATCH 3/4] Removed usersync links --- .../server/bidder/invibes/InvibesBidder.java | 36 ++++++++++--------- src/main/resources/bidder-config/invibes.yaml | 4 +-- .../bidder/invibes/InvibesBidderTest.java | 4 +-- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java index e68e8fc1d0d..6431d15db34 100644 --- a/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java +++ b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java @@ -15,7 +15,6 @@ import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.invibes.model.InvibesBidParams; import org.prebid.server.bidder.invibes.model.InvibesBidRequest; @@ -115,26 +114,27 @@ private ExtImpInvibes parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), INVIBES_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - throw new PreBidException("Error parsing invibesExt parameters"); + throw new PreBidException( + String.format("Error parsing invibesExt parameters in impression with id: %s", imp.getId())); } } private void validateImp(Imp imp) { if (imp.getBanner() == null) { - throw new PreBidException("Banner not specified"); + throw new PreBidException(String.format("Banner not specified in impression with id: %s", imp.getId())); } } private String resolveConsentString(User user) { final ExtUser extUser = user != null ? user.getExt() : null; - return extUser != null ? extUser.getConsent() : StringUtils.EMPTY; + return extUser != null ? extUser.getConsent() : ""; } private Boolean resolveGDPRApplies(Regs regs) { final ExtRegs extRegs = regs != null ? regs.getExt() : null; final Integer gdpr = extRegs != null ? extRegs.getGdpr() : null; - return gdpr != null ? gdpr == 1 : Boolean.TRUE; + return gdpr == null || gdpr == 1; } private InvibesInternalParams updateInvibesInternalParams(InvibesInternalParams invibesInternalParams, @@ -213,7 +213,7 @@ private HttpRequest makeRequest(InvibesInternalParams invibes private InvibesBidRequest resolveParameter(InvibesInternalParams invibesParams, BidRequest request) { final User user = request.getUser(); final String buyeruid = user != null ? user.getBuyeruid() : null; - final String lid = StringUtils.isNotBlank(buyeruid) ? buyeruid : StringUtils.EMPTY; + final String lid = StringUtils.isNotBlank(buyeruid) ? buyeruid : ""; return createRequest(invibesParams, lid, request.getDevice(), request.getSite()); } @@ -243,16 +243,16 @@ private InvibesBidRequest createRequest(InvibesInternalParams invibesParams, Str private static String resolveHeight(Device device) { final Integer height = device != null ? device.getH() : null; - return height != null && height > NumberUtils.INTEGER_ZERO ? height.toString() : null; + return height != null && height > 0 ? height.toString() : null; } private static String resolveWidth(Device device) { final Integer width = device != null ? device.getW() : null; - return width != null && width > NumberUtils.INTEGER_ZERO ? width.toString() : null; + return width != null && width > 0 ? width.toString() : null; } - private String resolveHost(Integer domainId) { + private static String resolveHost(Integer domainId) { if (domainId == null) { return "bid.videostep.com"; } else if (domainId >= 1002) { @@ -266,22 +266,26 @@ private String resolveHost(Integer domainId) { } } - private MultiMap resolveHeaders(Device device, Site site) { + private static MultiMap resolveHeaders(Device device, Site site) { final MultiMap headers = HttpUtil.headers(); if (device != null) { - if (StringUtils.isNotBlank(device.getIp())) { - headers.add("X-Forwarded-For", device.getIp()); - } else if (StringUtils.isNotBlank(device.getIpv6())) { - headers.add("X-Forwarded-For", device.getIpv6()); - } + addHeader(headers, "X-Forwarded-For", device.getIp()); + addHeader(headers, "X-Forwarded-For", device.getIpv6()); } if (site != null) { headers.add("Referer", site.getPage()); + addHeader(headers, "Referer", site.getPage()); } - headers.add("Aver", ADAPTER_VERSION); + addHeader(headers, "Aver", ADAPTER_VERSION); return headers; } + private static void addHeader(MultiMap headers, String header, String value) { + if (StringUtils.isNotBlank(value)) { + headers.add(header, value); + } + } + @Override public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { final int statusCode = httpCall.getResponse().getStatusCode(); diff --git a/src/main/resources/bidder-config/invibes.yaml b/src/main/resources/bidder-config/invibes.yaml index f1fb9bb9886..677164c1a18 100644 --- a/src/main/resources/bidder-config/invibes.yaml +++ b/src/main/resources/bidder-config/invibes.yaml @@ -14,8 +14,8 @@ adapters: supported-vendors: vendor-id: 436 usersync: - url: https://u2.videostep.com/User/getLid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirectUri= - redirect-url: /setuid?bidder=invibes&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID + url: + redirect-url: cookie-family-name: invibes type: redirect support-cors: false diff --git a/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java index 2e63bdf597a..7a075d3a72e 100644 --- a/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java @@ -99,7 +99,7 @@ public void makeHttpRequestsShouldReturnErrorWhenBannerIsNull() { // given final BidRequest bidRequest = givenBidRequest( bidRequestBuilder -> bidRequestBuilder.site(Site.builder().page("www.awesome-page.com").build()), - impBuilder -> impBuilder.banner(null)); + impBuilder -> impBuilder.id("123").banner(null)); // when final Result>> result = @@ -107,7 +107,7 @@ public void makeHttpRequestsShouldReturnErrorWhenBannerIsNull() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("Banner not specified")); + .containsOnly(BidderError.badInput("Banner not specified in impression with id: 123")); assertThat(result.getValue()).isEmpty(); } From 0b1e44eb398b96024f0762c923cec061b62c1faa Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Mon, 2 Nov 2020 16:04:29 +0200 Subject: [PATCH 4/4] Fixes after review --- .../server/bidder/invibes/InvibesBidder.java | 15 +-- .../bidder/invibes/InvibesBidderTest.java | 105 +++++++++++------- 2 files changed, 69 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java index 6431d15db34..0524ccf77c6 100644 --- a/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java +++ b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java @@ -65,6 +65,10 @@ public InvibesBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { + if (request.getSite() == null) { + return Result.emptyWithError(BidderError.badInput("Site not specified")); + } + final List errors = new ArrayList<>(); final String consentString = resolveConsentString(request.getUser()); @@ -86,8 +90,7 @@ public Result>> makeHttpRequests(BidRequest errors.add(BidderError.badInput(e.getMessage())); continue; } - - invibesInternalParams = updateInvibesInternalParams(invibesInternalParams, extImpInvibes, imp); + updateInvibesInternalParams(invibesInternalParams, extImpInvibes, imp); } //TODO add AMP parameter to invibesInternalParams, after reqInfo will be implemented @@ -100,9 +103,6 @@ public Result>> makeHttpRequests(BidRequest invibesInternalParams.setGdprConsent(consentString); try { - if (request.getSite() == null) { - throw new PreBidException("Site not specified"); - } final HttpRequest httpRequest = makeRequest(invibesInternalParams, request); return Result.of(Collections.singletonList(httpRequest), errors); } catch (PreBidException e) { @@ -137,7 +137,7 @@ private Boolean resolveGDPRApplies(Regs regs) { return gdpr == null || gdpr == 1; } - private InvibesInternalParams updateInvibesInternalParams(InvibesInternalParams invibesInternalParams, + private void updateInvibesInternalParams(InvibesInternalParams invibesInternalParams, ExtImpInvibes invibesExt, Imp imp) { final String impExtPlacementId = invibesExt.getPlacementId(); @@ -158,7 +158,6 @@ private InvibesInternalParams updateInvibesInternalParams(InvibesInternalParams final InvibesBidParams updatedBidParams = bidParams.toBuilder() .placementIds(updatedPlacementIds) - .properties(bidParams.getProperties()) .build(); invibesInternalParams.setDomainId(invibesExt.getDomainId()); @@ -173,8 +172,6 @@ private InvibesInternalParams updateInvibesInternalParams(InvibesInternalParams if (invibesDebug != null) { invibesInternalParams.setTestLog(invibesDebug.getTestLog()); } - - return invibesInternalParams; } private List resolveAdFormats(Banner currentBanner) { diff --git a/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java index 7a075d3a72e..b29aa91e115 100644 --- a/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java @@ -45,6 +45,19 @@ public class InvibesBidderTest extends VertxTest { private static final String ENDPOINT_URL = "https://{{Host}}/test"; + private static final int BANNER_H = 12; + private static final int BANNER_W = 15; + private static final String PAGE_URL = "www.test.com"; + private static final int SECOND_BANNER_H = 23; + private static final int SECOND_BANNER_W = 24; + private static final String FIRST_PLACEMENT_ID = "12"; + private static final String SECOND_PLACEMENT_ID = "15"; + private static final int DEVICE_W = 77; + private static final int DEVICE_H = 88; + private static final String BUYER_UID = "someUid"; + private static final String IMP_ID = "123"; + private static final String CURRENCY = "EUR"; + private static final String BID_VERSION = "4"; private InvibesBidder invibesBidder; @@ -63,8 +76,8 @@ public void creationShouldFailOnInvalidEndpointUrl() { public void makeHttpRequestsShouldCreateCorrectURL() { // given final BidRequest bidRequest = givenBidRequest( - bidRequestBuilder -> bidRequestBuilder.site(Site.builder().page("www.test.com").build()), - impBuilder -> impBuilder.banner(Banner.builder().h(12).w(15).build()), + identity(), + impBuilder -> impBuilder.banner(Banner.builder().h(BANNER_H).w(BANNER_W).build()), ExtImpInvibes.of("12", 1003, InvibesDebug.of("test", true))); // when @@ -80,6 +93,7 @@ public void makeHttpRequestsShouldCreateCorrectURL() { public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { // given final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().page(PAGE_URL).build()) .imp(singletonList(Imp.builder() .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) .build())) @@ -98,8 +112,8 @@ public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { public void makeHttpRequestsShouldReturnErrorWhenBannerIsNull() { // given final BidRequest bidRequest = givenBidRequest( - bidRequestBuilder -> bidRequestBuilder.site(Site.builder().page("www.awesome-page.com").build()), - impBuilder -> impBuilder.id("123").banner(null)); + bidRequestBuilder -> bidRequestBuilder.site(Site.builder().page(PAGE_URL).build()), + impBuilder -> impBuilder.id(IMP_ID).banner(null)); // when final Result>> result = @@ -107,7 +121,8 @@ public void makeHttpRequestsShouldReturnErrorWhenBannerIsNull() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("Banner not specified in impression with id: 123")); + .containsOnly( + BidderError.badInput(String.format("Banner not specified in impression with id: %s", IMP_ID))); assertThat(result.getValue()).isEmpty(); } @@ -115,7 +130,7 @@ public void makeHttpRequestsShouldReturnErrorWhenBannerIsNull() { public void makeHttpRequestsShouldReturnErrorWhenSiteIsNotPresent() { // given final BidRequest bidRequest = givenBidRequest(identity(), - impBuilder -> impBuilder.banner(Banner.builder().h(12).w(15).build())); + impBuilder -> impBuilder.banner(Banner.builder().h(BANNER_H).w(BANNER_W).build())); // when final Result>> result = invibesBidder.makeHttpRequests(bidRequest); @@ -131,36 +146,41 @@ public void shouldCreateRequestWithDataFromEveryImpression() throws JsonProcessi // given final List imps = Arrays.asList(givenImp( impBuilder -> impBuilder - .banner(Banner.builder().h(10).w(11).build()), ExtImpInvibes.of("12", 15, - InvibesDebug.of("test1", true))), + .banner(Banner.builder().h(BANNER_H).w(BANNER_W).build()), + ExtImpInvibes.of(FIRST_PLACEMENT_ID, 15, InvibesDebug.of("test1", true))), givenImp(impBuilder -> impBuilder - .banner(Banner.builder().h(14).w(15).build()), ExtImpInvibes.of("15", 1001, - InvibesDebug.of("test2", false))) - ); + .banner(Banner.builder().h(SECOND_BANNER_H).w(SECOND_BANNER_W).build()), + ExtImpInvibes.of(SECOND_PLACEMENT_ID, 1001, InvibesDebug.of("test2", false)))); final BidRequest bidRequest = BidRequest.builder() - .site(Site.builder().page("www.awesome-page.com").build()) + .site(Site.builder().page(PAGE_URL).build()) .imp(imps) .build(); + // when final Result>> result = invibesBidder.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .extracting(HttpRequest::getPayload).hasSize(1); - InvibesBidParams expectedBidParams = - mapper.readValue(result.getValue().get(0).getPayload().getBidParamsJson(), InvibesBidParams.class); - assertThat(expectedBidParams.getPlacementIds()).hasSize(2) - .containsOnly("12", "15"); - final Format firstExpectedFormat = Format.builder().w(11).h(10).build(); - final Format secondExpectedFormat = Format.builder().w(15).h(14).build(); - assertThat(expectedBidParams.getProperties().values()).hasSize(2) - .containsOnly(InvibesPlacementProperty.builder() - .formats(Collections.singletonList(firstExpectedFormat)) - .build(), - InvibesPlacementProperty.builder() - .formats(Collections.singletonList(secondExpectedFormat)) - .build()); + final Format firstExpectedFormat = Format.builder().w(BANNER_W).h(BANNER_H).build(); + final Format secondExpectedFormat = Format.builder().w(SECOND_BANNER_W).h(SECOND_BANNER_H).build(); + final Map bidProperties = new HashMap<>(); + bidProperties.put(FIRST_PLACEMENT_ID, InvibesPlacementProperty.builder() + .formats(Collections.singletonList(firstExpectedFormat)) + .build()); + bidProperties.put(SECOND_PLACEMENT_ID, InvibesPlacementProperty.builder() + .formats(Collections.singletonList(secondExpectedFormat)) + .build()); + + InvibesBidParams expectedBidParams = InvibesBidParams.builder() + .placementIds(Arrays.asList(FIRST_PLACEMENT_ID, SECOND_PLACEMENT_ID)) + .bidVersion(BID_VERSION) + .properties(bidProperties) + .build(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(invibesBidRequest -> + mapper.readValue(invibesBidRequest.getBidParamsJson(), InvibesBidParams.class)) + .containsOnly(expectedBidParams); } @Test @@ -168,37 +188,37 @@ public void makeHttpRequestsShouldCreateInvibesBidRequestWithCorrectParams() thr // given final BidRequest bidRequest = givenBidRequest( bidRequestBuilder -> bidRequestBuilder - .device(Device.builder().w(77).h(88).build()) - .user(User.builder().buyeruid("someUid").build()) - .site(Site.builder().page("www.awesome-page.com").build()), - impBuilder -> impBuilder.banner(Banner.builder().h(12).w(15).build())); + .device(Device.builder().w(DEVICE_W).h(DEVICE_H).build()) + .user(User.builder().buyeruid(BUYER_UID).build()) + .site(Site.builder().page(PAGE_URL).build()), + impBuilder -> impBuilder.banner(Banner.builder().h(BANNER_H).w(BANNER_W).build())); // when final Result>> result = invibesBidder.makeHttpRequests(bidRequest); // then final Map properties = new HashMap<>(); - properties.put("12", InvibesPlacementProperty.builder() - .formats(Collections.singletonList(Format.builder().w(15).h(12).build())).build()); + properties.put(FIRST_PLACEMENT_ID, InvibesPlacementProperty.builder() + .formats(Collections.singletonList(Format.builder().w(BANNER_W).h(BANNER_H).build())).build()); final InvibesBidParams invibesBidParams = InvibesBidParams.builder() - .placementIds(Collections.singletonList("12")) - .bidVersion("4") + .placementIds(Collections.singletonList(FIRST_PLACEMENT_ID)) + .bidVersion(BID_VERSION) .properties(properties) .build(); final InvibesBidRequest expectedRequest = InvibesBidRequest.builder() .bidParamsJson(mapper.writeValueAsString(invibesBidParams)) .isTestBid(true) - .location("www.awesome-page.com") + .location(PAGE_URL) .gdpr(true) .gdprConsent(StringUtils.EMPTY) .invibBVLog(true) .videoAdDebug(true) - .lid("someUid") + .lid(BUYER_UID) .bvid("test") - .width("77") - .height("88") + .width(String.valueOf(DEVICE_W)) + .height(String.valueOf(DEVICE_H)) .build(); assertThat(result.getErrors()).isEmpty(); @@ -240,7 +260,7 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { // given final HttpCall httpCall = givenHttpCall( InvibesBidRequest.builder().build(), - mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid(IMP_ID)))); // when final Result> result = invibesBidder.makeBids(httpCall, null); @@ -248,7 +268,7 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR")); + .containsOnly(BidderBid.of(Bid.builder().impid(IMP_ID).build(), banner, CURRENCY)); } @Test @@ -279,7 +299,7 @@ private static InvibesBidderResponse givenBidResponse(Function