diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java index ec79b79049c..ba6971cf8f8 100644 --- a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java @@ -247,7 +247,7 @@ private ExtDevice updateExt(ExtDevice ortbExtDevice) { } private ExtDevice copyExtDevice(ExtDevice original) { - final ExtDevice copy = ExtDevice.of(original.getAtts(), original.getPrebid()); + final ExtDevice copy = ExtDevice.of(original.getAtts(), original.getIfaType(), original.getPrebid()); mapper.fillExtension(copy, original); return copy; } diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java index f981f43ccaa..9433683b5e3 100644 --- a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java +++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java @@ -96,7 +96,7 @@ public void callShouldReturnNoActionWhenDeviceHasWurflProperty() { // given final String ua = "Mozilla/5.0 (testPhone; CPU testPhone OS 1_0_2) Version/17.4.1 Mobile/12E GrandTest/604.1"; final ExtDevicePrebid extDevicePrebid = ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)); - final ExtDevice extDevice = ExtDevice.of(0, extDevicePrebid); + final ExtDevice extDevice = ExtDevice.of(0, null, extDevicePrebid); final ObjectNode wurfl = mapper.mapper().createObjectNode(); wurfl.put("wurfl_id", "test_phone_ver1"); extDevice.addProperty("wurfl", wurfl); diff --git a/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java b/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java index eae090780c5..8c829b9b2c8 100644 --- a/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java +++ b/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.function.Function; /** @@ -72,12 +73,13 @@ public List ipFrom(MultiMap headers, String host) { private List ipFrom(Function headerGetter, String host) { final List candidates = new ArrayList<>(); - candidates.add(headerGetter.apply("True-Client-IP")); - final String xff = headerGetter.apply("X-Forwarded-For"); + candidates.add(headerGetter.apply(HttpUtil.TRUE_CLIENT_IP_HEADER.toString())); + final String xff = headerGetter.apply(HttpUtil.X_FORWARDED_FOR_HEADER.toString()); if (xff != null) { candidates.addAll(Arrays.asList(xff.split(","))); } - candidates.add(headerGetter.apply("X-Real-IP")); + candidates.add(headerGetter.apply(HttpUtil.X_REAL_IP_HEADER.toString())); + candidates.add(headerGetter.apply(HttpUtil.X_DEVICE_IP_HEADER.toString())); candidates.add(host); return candidates.stream() @@ -90,7 +92,12 @@ private List ipFrom(Function headerGetter, String host) * Determines User-Agent by checking 'User-Agent' http header. */ public String uaFrom(HttpRequestContext request) { - return StringUtils.trimToNull(request.getHeaders().get(HttpUtil.USER_AGENT_HEADER)); + return Optional.ofNullable(getTrimmedHeader(request, HttpUtil.USER_AGENT_HEADER)) + .orElseGet(() -> getTrimmedHeader(request, HttpUtil.X_DEVICE_USER_AGENT_HEADER)); + } + + private static String getTrimmedHeader(HttpRequestContext request, CharSequence header) { + return StringUtils.trimToNull(request.getHeaders().get(header)); } /** diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index e1c5e4240ce..31d85d2bd16 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -525,7 +525,7 @@ private BidRequest fillExplicitParameters(BidRequest bidRequest, Account account */ private BidRequest overrideParameters(BidRequest bidRequest, HttpRequestContext httpRequest, List errors) { final String requestTargeting = httpRequest.getQueryParams().get(TARGETING_REQUEST_PARAM); - final ObjectNode targetingNode = readTargeting(requestTargeting); + final ObjectNode targetingNode = readTargeting(requestTargeting, mapper); final String referer = implicitParametersExtractor.refererFrom(httpRequest); ortbTypesResolver.normalizeTargeting(targetingNode, errors, referer); @@ -542,7 +542,7 @@ private BidRequest overrideParameters(BidRequest bidRequest, HttpRequestContext return bidRequest; } - private ObjectNode readTargeting(String jsonTargeting) { + public static ObjectNode readTargeting(String jsonTargeting, JacksonMapper mapper) { try { final String decodedJsonTargeting = HttpUtil.decodeUrl(jsonTargeting); final JsonNode jsonNodeTargeting = decodedJsonTargeting != null @@ -554,7 +554,7 @@ private ObjectNode readTargeting(String jsonTargeting) { } } - private ObjectNode validateAndGetTargeting(JsonNode jsonNodeTargeting) { + private static ObjectNode validateAndGetTargeting(JsonNode jsonNodeTargeting) { if (jsonNodeTargeting.isObject()) { return (ObjectNode) jsonNodeTargeting; } else { diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index d873af3e696..42d5ccf59e3 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -38,6 +38,8 @@ */ public class AuctionRequestFactory { + private static final String ENDPOINT = Endpoint.openrtb2_auction.value(); + private final long maxRequestSize; private final Ortb2RequestFactory ortb2RequestFactory; private final StoredRequestProcessor storedRequestProcessor; @@ -55,8 +57,6 @@ public class AuctionRequestFactory { private final GeoLocationServiceWrapper geoLocationServiceWrapper; private final BidAdjustmentsEnricher bidAdjustmentsEnricher; - private static final String ENDPOINT = Endpoint.openrtb2_auction.value(); - public AuctionRequestFactory(long maxRequestSize, Ortb2RequestFactory ortb2RequestFactory, StoredRequestProcessor storedRequestProcessor, diff --git a/src/main/java/org/prebid/server/auction/requestfactory/GetInterfaceRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/GetInterfaceRequestFactory.java new file mode 100644 index 00000000000..c4bf85b40b2 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/requestfactory/GetInterfaceRequestFactory.java @@ -0,0 +1,979 @@ +package org.prebid.server.auction.requestfactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Content; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Dooh; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iab.openrtb.request.Video; +import io.vertx.core.Future; +import io.vertx.ext.web.RoutingContext; +import lombok.Value; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.DebugResolver; +import org.prebid.server.auction.FpdResolver; +import org.prebid.server.auction.GeoLocationServiceWrapper; +import org.prebid.server.auction.ImplicitParametersExtractor; +import org.prebid.server.auction.InterstitialProcessor; +import org.prebid.server.auction.IpAddressHelper; +import org.prebid.server.auction.OrtbTypesResolver; +import org.prebid.server.auction.externalortb.ProfilesProcessor; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; +import org.prebid.server.auction.gpp.AuctionGppService; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.ConsentType; +import org.prebid.server.auction.model.IpAddress; +import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; +import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; +import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; +import org.prebid.server.cookie.CookieDeprecationService; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.metric.MetricName; +import org.prebid.server.model.Endpoint; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.privacy.ccpa.Ccpa; +import org.prebid.server.privacy.gdpr.TcfDefinerService; +import org.prebid.server.proto.openrtb.ext.request.ConsentedProvidersSettings; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.settings.model.Account; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class GetInterfaceRequestFactory { + + private static final String ENDPOINT = Endpoint.openrtb2_get_interface.value(); + + private final Ortb2RequestFactory ortb2RequestFactory; + private final StoredRequestProcessor storedRequestProcessor; + private final ProfilesProcessor profilesProcessor; + private final BidRequestOrtbVersionConversionManager ortbVersionConversionManager; + private final AuctionGppService gppService; + private final CookieDeprecationService cookieDeprecationService; + private final ImplicitParametersExtractor paramsExtractor; + private final OrtbTypesResolver ortbTypesResolver; + private final IpAddressHelper ipAddressHelper; + private final Ortb2ImplicitParametersResolver paramsResolver; + private final FpdResolver fpdResolver; + private final InterstitialProcessor interstitialProcessor; + private final AuctionPrivacyContextFactory auctionPrivacyContextFactory; + private final DebugResolver debugResolver; + private final JacksonMapper mapper; + private final GeoLocationServiceWrapper geoLocationServiceWrapper; + private final BidAdjustmentsEnricher bidAdjustmentsEnricher; + + public GetInterfaceRequestFactory(Ortb2RequestFactory ortb2RequestFactory, + StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, + BidRequestOrtbVersionConversionManager ortbVersionConversionManager, + AuctionGppService gppService, + CookieDeprecationService cookieDeprecationService, + ImplicitParametersExtractor paramsExtractor, + OrtbTypesResolver ortbTypesResolver, + IpAddressHelper ipAddressHelper, + Ortb2ImplicitParametersResolver paramsResolver, + FpdResolver fpdResolver, + InterstitialProcessor interstitialProcessor, + AuctionPrivacyContextFactory auctionPrivacyContextFactory, + DebugResolver debugResolver, + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper, + BidAdjustmentsEnricher bidAdjustmentsEnricher) { + + this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); + this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.profilesProcessor = Objects.requireNonNull(profilesProcessor); + this.ortbVersionConversionManager = Objects.requireNonNull(ortbVersionConversionManager); + this.gppService = Objects.requireNonNull(gppService); + this.cookieDeprecationService = Objects.requireNonNull(cookieDeprecationService); + this.paramsExtractor = Objects.requireNonNull(paramsExtractor); + this.ortbTypesResolver = Objects.requireNonNull(ortbTypesResolver); + this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); + this.paramsResolver = Objects.requireNonNull(paramsResolver); + this.fpdResolver = Objects.requireNonNull(fpdResolver); + this.interstitialProcessor = Objects.requireNonNull(interstitialProcessor); + this.auctionPrivacyContextFactory = Objects.requireNonNull(auctionPrivacyContextFactory); + this.debugResolver = Objects.requireNonNull(debugResolver); + this.mapper = Objects.requireNonNull(mapper); + this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); + this.bidAdjustmentsEnricher = Objects.requireNonNull(bidAdjustmentsEnricher); + } + + public Future fromRequest(RoutingContext routingContext, long startTime) { + final String body = routingContext.body().asString(); + + final AuctionContext initialAuctionContext = ortb2RequestFactory.createAuctionContext( + Endpoint.openrtb2_get_interface, MetricName.openrtb2web); + + return ortb2RequestFactory.executeEntrypointHooks(routingContext, body, initialAuctionContext) + + .map(httpRequest -> ortb2RequestFactory.enrichAuctionContext( + initialAuctionContext, + httpRequest, + initialBidRequest(httpRequest, initialAuctionContext.getPrebidErrors()), + startTime)) + + .recover(ortb2RequestFactory::restoreResultFromRejection); + } + + public Future enrichAuctionContext(AuctionContext initialContext) { + if (initialContext.isRequestRejected()) { + return Future.succeededFuture(initialContext); + } + + return Future.succeededFuture(addTmpPublisher(initialContext)) + + .compose(auctionContext -> ortb2RequestFactory.fetchAccount(auctionContext) + .map(auctionContext::with)) + + .map(auctionContext -> auctionContext.with(removeTmpPublisher(auctionContext.getBidRequest()))) + + .map(auctionContext -> auctionContext.with(debugResolver.debugContextFrom(auctionContext))) + + .compose(auctionContext -> storedRequestProcessor.processAmpRequest( + auctionContext.getAccount().getId(), + storedRequestId(auctionContext.getBidRequest()), + auctionContext.getBidRequest()) + .map(auctionContext::with)) + + .map(auctionContext -> auctionContext.with(completeBidRequest(auctionContext))) + + .map(auctionContext -> auctionContext.with(requestTypeMetric(auctionContext.getBidRequest()))) + + .compose(auctionContext -> profilesProcessor.process(auctionContext, auctionContext.getBidRequest()) + .map(auctionContext::with)) + + .compose(auctionContext -> geoLocationServiceWrapper.lookup(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithGeolocationData(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> gppService.contextFrom(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> ortb2RequestFactory.activityInfrastructureFrom(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> updateAndValidateBidRequest(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> auctionPrivacyContextFactory.contextFrom(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext) + .map(auctionContext::with)) + + .map(auctionContext -> auctionContext.with(bidAdjustmentsEnricher.enrichBidRequest(auctionContext))) + + .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) + .map(auctionContext::with)) + + .map(ortb2RequestFactory::updateTimeout) + + .recover(ortb2RequestFactory::restoreResultFromRejection); + } + + private BidRequest initialBidRequest(HttpRequestContext httpRequest, List errors) { + final GetInterfaceParams params = new GetInterfaceParams(httpRequest, errors); + final Consent consent = params.consent(); + + return BidRequest.builder() + .device(initialDevice(params)) + .user(initialUser(params, consent)) + .tmax(params.tmax()) + .bcat(params.bCat()) + .badv(params.bAdv()) + .regs(initialRegs(params, consent)) + .ext(initialExtRequest(params)) + .build(); + } + + private static Device initialDevice(GetInterfaceParams params) { + final IpAddress ipAddress = params.ip(); + return Device.builder() + .dnt(params.dnt()) + .lmt(params.lmt()) + .ua(params.ua()) + .ip(ipAddress.getVersion() == IpAddress.IP.v4 ? ipAddress.getIp() : null) + .ipv6(ipAddress.getVersion() == IpAddress.IP.v6 ? ipAddress.getIp() : null) + .devicetype(params.deviceType()) + .ifa(params.ifa()) + .ext(ExtDevice.of(null, params.ifaType(), null)) + .build(); + } + + private static User initialUser(GetInterfaceParams params, Consent consent) { + return User.builder() + .consent(consent.getTcfConsent()) + .ext(ExtUser.builder() + .consentedProvidersSettings(ConsentedProvidersSettings.of(params.consentedProviders())) + .build()) + .build(); + } + + private static Regs initialRegs(GetInterfaceParams params, Consent consent) { + return Regs.builder() + .coppa(params.coppa()) + .gdpr(params.gdpr()) + .usPrivacy(consent.getUsPrivacy()) + .gpp(consent.getGpp()) + .gppSid(params.gppSid()) + .ext(ExtRegs.of(null, null, params.gpc(), null)) + .build(); + } + + private static ExtRequest initialExtRequest(GetInterfaceParams params) { + return ExtRequest.of(ExtRequestPrebid.builder() + .debug(params.debug()) + .storedrequest(ExtStoredRequest.of(params.storedRequestId())) + .profiles(params.requestProfiles()) + .storedAuctionResponse(ExtStoredAuctionResponse.of(params.storedAuctionResponseId(), null, null)) + .outputFormat(params.outputFormat()) + .outputModule(params.outputModule()) + .build()); + } + + private AuctionContext addTmpPublisher(AuctionContext auctionContext) { + final GetInterfaceParams params = new GetInterfaceParams( + auctionContext.getHttpRequest(), auctionContext.getPrebidErrors()); + + final BidRequest bidRequestWithTmpPublisher = auctionContext.getBidRequest().toBuilder() + .site(Site.builder() + .publisher(Publisher.builder().id(params.accountId()).build()) + .build()) + .build(); + + return auctionContext.with(bidRequestWithTmpPublisher); + } + + private static BidRequest removeTmpPublisher(BidRequest bidRequest) { + return bidRequest.toBuilder().site(null).build(); + } + + private static String storedRequestId(BidRequest bidRequest) { + return bidRequest.getExt().getPrebid().getStoredrequest().getId(); + } + + private BidRequest completeBidRequest(AuctionContext auctionContext) { + final Account account = auctionContext.getAccount(); + final GetInterfaceParams params = new GetInterfaceParams( + auctionContext.getHttpRequest(), auctionContext.getPrebidErrors()); + + final BidRequest bidRequest = auctionContext.getBidRequest(); + final List imps = bidRequest.getImp(); + if (CollectionUtils.isNotEmpty(imps) && imps.size() != 1) { + auctionContext.getPrebidErrors().add( + "Request includes %d imp elements. Only the first one will remain.".formatted(imps.size())); + } + + final Imp imp = CollectionUtils.isNotEmpty(imps) ? imps.getFirst() : null; + + return bidRequest.toBuilder() + .imp(imp != null ? Collections.singletonList(completeImp(imp, params)) : null) + .site(completeSite(bidRequest.getSite(), params, account)) + .app(completeApp(bidRequest.getApp(), params, account)) + .dooh(completeDooh(bidRequest.getDooh(), params, account)) + .build(); + } + + private Imp completeImp(Imp imp, GetInterfaceParams params) { + return imp.toBuilder() + .banner(completeBanner(imp.getBanner(), params)) + .video(completeVideo(imp.getVideo(), params)) + .audio(completeAudio(imp.getAudio(), params)) + .tagid(ObjectUtils.defaultIfNull(params.tagId(), imp.getTagid())) + .ext(completeImpExt(imp.getExt(), params)) + .build(); + } + + private static Banner completeBanner(Banner banner, GetInterfaceParams params) { + if (banner == null) { + return null; + } + + return banner.toBuilder() + .format(ObjectUtils.defaultIfNull(params.format(), banner.getFormat())) + .w(ObjectUtils.defaultIfNull(params.w(), banner.getW())) + .h(ObjectUtils.defaultIfNull(params.h(), banner.getH())) + .btype(ObjectUtils.defaultIfNull(params.bType(), banner.getBtype())) + .mimes(ObjectUtils.defaultIfNull(params.mimes(), banner.getMimes())) + .battr(ObjectUtils.defaultIfNull(params.bAttr(), banner.getBattr())) + .pos(ObjectUtils.defaultIfNull(params.pos(), banner.getPos())) + .topframe(ObjectUtils.defaultIfNull(params.topFrame(), banner.getTopframe())) + .expdir(ObjectUtils.defaultIfNull(params.expDir(), banner.getExpdir())) + .api(ObjectUtils.defaultIfNull(params.api(), banner.getApi())) + .build(); + } + + private static Video completeVideo(Video video, GetInterfaceParams params) { + if (video == null) { + return null; + } + + return video.toBuilder() + .w(ObjectUtils.defaultIfNull(params.w(), video.getW())) + .h(ObjectUtils.defaultIfNull(params.h(), video.getH())) + .mimes(ObjectUtils.defaultIfNull(params.mimes(), video.getMimes())) + .minduration(ObjectUtils.defaultIfNull(params.minDuration(), video.getMinduration())) + .maxduration(ObjectUtils.defaultIfNull(params.maxDuration(), video.getMaxduration())) + .startdelay(ObjectUtils.defaultIfNull(params.startDelay(), video.getStartdelay())) + .maxseq(ObjectUtils.defaultIfNull(params.maxSeq(), video.getMaxseq())) + .poddur(ObjectUtils.defaultIfNull(params.podDur(), video.getPoddur())) + .protocols(ObjectUtils.defaultIfNull(params.protocols(), video.getProtocols())) + .podid(ObjectUtils.defaultIfNull(params.podId(), video.getPodid())) + .podseq(ObjectUtils.defaultIfNull(params.podSeq(), video.getPodseq())) + .rqddurs(ObjectUtils.defaultIfNull(params.rqdDurs(), video.getRqddurs())) + .placement(ObjectUtils.defaultIfNull(params.placement(), video.getPlacement())) + .plcmt(ObjectUtils.defaultIfNull(params.plcmt(), video.getPlcmt())) + .linearity(ObjectUtils.defaultIfNull(params.linearity(), video.getLinearity())) + .skip(ObjectUtils.defaultIfNull(params.skip(), video.getSkip())) + .skipmin(ObjectUtils.defaultIfNull(params.skipMin(), video.getSkipmin())) + .skipafter(ObjectUtils.defaultIfNull(params.skipAfter(), video.getSkipafter())) + .sequence(ObjectUtils.defaultIfNull(params.sequence(), video.getSequence())) + .slotinpod(ObjectUtils.defaultIfNull(params.slotInPod(), video.getSlotinpod())) + .mincpmpersec(ObjectUtils.defaultIfNull(params.minCpmPerSec(), video.getMincpmpersec())) + .battr(ObjectUtils.defaultIfNull(params.bAttr(), video.getBattr())) + .pos(ObjectUtils.defaultIfNull(params.pos(), video.getPos())) + .maxextended(ObjectUtils.defaultIfNull(params.maxExtended(), video.getMaxextended())) + .minbitrate(ObjectUtils.defaultIfNull(params.minBitrate(), video.getMinbitrate())) + .maxbitrate(ObjectUtils.defaultIfNull(params.maxBitrate(), video.getMaxbitrate())) + .boxingallowed(ObjectUtils.defaultIfNull(params.boxingAllowed(), video.getBoxingallowed())) + .playbackmethod(ObjectUtils.defaultIfNull(params.playbackMethod(), video.getPlaybackmethod())) + .playbackend(ObjectUtils.defaultIfNull(params.playbackEnd(), video.getPlaybackend())) + .delivery(ObjectUtils.defaultIfNull(params.delivery(), video.getDelivery())) + .api(ObjectUtils.defaultIfNull(params.api(), video.getApi())) + .build(); + } + + private static Audio completeAudio(Audio audio, GetInterfaceParams params) { + if (audio == null) { + return null; + } + + return audio.toBuilder() + .mimes(ObjectUtils.defaultIfNull(params.mimes(), audio.getMimes())) + .minduration(ObjectUtils.defaultIfNull(params.minDuration(), audio.getMinduration())) + .maxduration(ObjectUtils.defaultIfNull(params.maxDuration(), audio.getMaxduration())) + .startdelay(ObjectUtils.defaultIfNull(params.startDelay(), audio.getStartdelay())) + .maxseq(ObjectUtils.defaultIfNull(params.maxSeq(), audio.getMaxseq())) + .poddur(ObjectUtils.defaultIfNull(params.podDur(), audio.getPoddur())) + .protocols(ObjectUtils.defaultIfNull(params.protocols(), audio.getProtocols())) + .podid(ObjectUtils.defaultIfNull(params.podId(), audio.getPodid())) + .podseq(ObjectUtils.defaultIfNull(params.podSeq(), audio.getPodseq())) + .rqddurs(ObjectUtils.defaultIfNull(params.rqdDurs(), audio.getRqddurs())) + .sequence(ObjectUtils.defaultIfNull(params.sequence(), audio.getSequence())) + .slotinpod(ObjectUtils.defaultIfNull(params.slotInPod(), audio.getSlotinpod())) + .mincpmpersec(ObjectUtils.defaultIfNull(params.minCpmPerSec(), audio.getMincpmpersec())) + .battr(ObjectUtils.defaultIfNull(params.bAttr(), audio.getBattr())) + .maxextended(ObjectUtils.defaultIfNull(params.maxExtended(), audio.getMaxextended())) + .minbitrate(ObjectUtils.defaultIfNull(params.minBitrate(), audio.getMinbitrate())) + .maxbitrate(ObjectUtils.defaultIfNull(params.maxBitrate(), audio.getMaxbitrate())) + .delivery(ObjectUtils.defaultIfNull(params.delivery(), audio.getDelivery())) + .api(ObjectUtils.defaultIfNull(params.api(), audio.getApi())) + .feed(ObjectUtils.defaultIfNull(params.feed(), audio.getFeed())) + .stitched(ObjectUtils.defaultIfNull(params.stitched(), audio.getStitched())) + .nvol(ObjectUtils.defaultIfNull(params.nvol(), audio.getNvol())) + .build(); + } + + private ObjectNode completeImpExt(ObjectNode ext, GetInterfaceParams params) { + final ObjectNode extWithTargeting = enrichImpExtWithTargeting(ext, params); + return enrichImpExtWithProfiles(extWithTargeting, params); + } + + private ObjectNode enrichImpExtWithTargeting(ObjectNode ext, GetInterfaceParams params) { + final ObjectNode targetingNode = params.targeting(); + + return targetingNode != null + ? fpdResolver.resolveImpExt(ext, targetingNode) + : ext; + } + + private ObjectNode enrichImpExtWithProfiles(ObjectNode ext, GetInterfaceParams params) { + final List impProfiles = params.impProfiles(); + if (CollectionUtils.isEmpty(impProfiles)) { + return ext; + } + + final ObjectNode modifiedExt = ext != null ? ext : mapper.mapper().createObjectNode(); + final ObjectNode extPrebid = Optional.ofNullable(modifiedExt.get("prebid")) + .filter(JsonNode::isObject) + .map(ObjectNode.class::cast) + .orElseGet(() -> modifiedExt.putObject("prebid")); + final ArrayNode profiles = extPrebid.putArray("profiles"); + impProfiles.forEach(profiles::add); + + return modifiedExt; + } + + private static Site completeSite(Site site, GetInterfaceParams params, Account account) { + if (site == null) { + return null; + } + + return site.toBuilder() + .page(ObjectUtils.defaultIfNull(params.page(), site.getPage())) + .publisher(completePublisher(site.getPublisher(), account)) + .content(completeContent(site.getContent(), params)) + .build(); + } + + private static App completeApp(App app, GetInterfaceParams params, Account account) { + if (app == null) { + return null; + } + + return app.toBuilder() + .name(ObjectUtils.defaultIfNull(params.name(), app.getName())) + .bundle(ObjectUtils.defaultIfNull(params.bundle(), app.getBundle())) + .storeurl(ObjectUtils.defaultIfNull(params.storeUrl(), app.getStoreurl())) + .publisher(completePublisher(app.getPublisher(), account)) + .content(completeContent(app.getContent(), params)) + .build(); + } + + private static Dooh completeDooh(Dooh dooh, GetInterfaceParams params, Account account) { + if (dooh == null) { + return null; + } + + return dooh.toBuilder() + .publisher(completePublisher(dooh.getPublisher(), account)) + .content(completeContent(dooh.getContent(), params)) + .build(); + } + + private static Publisher completePublisher(Publisher publisher, Account account) { + return Optional.ofNullable(publisher) + .map(Publisher::toBuilder) + .orElseGet(Publisher::builder) + .id(account.getId()) + .build(); + } + + private static Content completeContent(Content content, GetInterfaceParams params) { + final Content safeContent = content != null ? content : Content.builder().build(); + + return safeContent.toBuilder() + .title(ObjectUtils.defaultIfNull(params.title(), safeContent.getTitle())) + .series(ObjectUtils.defaultIfNull(params.series(), safeContent.getSeries())) + .genre(ObjectUtils.defaultIfNull(params.genre(), safeContent.getGenre())) + .url(ObjectUtils.defaultIfNull(params.url(), safeContent.getUrl())) + .cattax(ObjectUtils.defaultIfNull(params.catTax(), safeContent.getCattax())) + .cat(ObjectUtils.defaultIfNull(params.cat(), safeContent.getCat())) + .contentrating(ObjectUtils.defaultIfNull(params.contentRating(), safeContent.getContentrating())) + .livestream(ObjectUtils.defaultIfNull(params.liveStream(), safeContent.getLivestream())) + .language(ObjectUtils.defaultIfNull(params.language(), safeContent.getLanguage())) + .build(); + } + + private Future updateAndValidateBidRequest(AuctionContext auctionContext) { + final Account account = auctionContext.getAccount(); + final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); + final List debugWarnings = auctionContext.getDebugWarnings(); + + return updateBidRequest(auctionContext) + .compose(bidRequest -> ortb2RequestFactory.limitImpressions(account, bidRequest, debugWarnings)) + .compose(bidRequest -> ortb2RequestFactory.validateRequest( + account, bidRequest, httpRequest, auctionContext.getDebugContext(), debugWarnings)) + .map(interstitialProcessor::process); + } + + private Future updateBidRequest(AuctionContext auctionContext) { + return Future.succeededFuture(auctionContext.getBidRequest()) + .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) + .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) + .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, true)) + .map(bidRequest -> cookieDeprecationService.updateBidRequestDevice(bidRequest, auctionContext)) + .map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings())); + } + + private static MetricName requestTypeMetric(BidRequest bidRequest) { + if (bidRequest.getApp() != null) { + return MetricName.openrtb2app; + } else if (bidRequest.getDooh() != null) { + return MetricName.openrtb2dooh; + } else { + return MetricName.openrtb2web; + } + } + + private class GetInterfaceParams { + + private final HttpRequestContext httpRequestContext; + + private final List errors; + + GetInterfaceParams(HttpRequestContext httpRequestContext, List errors) { + this.httpRequestContext = Objects.requireNonNull(httpRequestContext); + this.errors = Objects.requireNonNull(errors); + } + + public String storedRequestId() { + return Optional.ofNullable(getString("srid")) + .or(() -> Optional.ofNullable(getString("tag_id"))) + .orElseThrow(() -> new InvalidRequestException("Request require the stored request id.")); + } + + public String accountId() { + return Optional.ofNullable(getString("pubid")) + .orElseGet(() -> getString("account")); + } + + public Long tmax() { + try { + final String value = getString("tmax"); + return StringUtils.isNotBlank(value) ? Long.parseLong(value) : null; + } catch (NumberFormatException e) { + throw new InvalidRequestException("Invalid number: " + e.getMessage()); + } + } + + public int debug() { + return Optional.ofNullable(getInteger("debug")).orElse(0); + } + + public String outputFormat() { + return getString("of"); + } + + public String outputModule() { + return getString("om"); + } + + public List requestProfiles() { + return getListOfStrings("rprof"); + } + + public List impProfiles() { + return getListOfStrings("iprof"); + } + + public String storedAuctionResponseId() { + return getString("sarid"); + } + + public List mimes() { + return getListOfStrings("mimes"); + } + + public Integer w() { + return Optional.ofNullable(getInteger("ow")) + .orElseGet(() -> getInteger("w")); + } + + public Integer h() { + return Optional.ofNullable(getInteger("oh")) + .orElseGet(() -> getInteger("h")); + } + + public List format() { + final List formats = new ArrayList<>(); + final Integer w = w(); + final Integer h = h(); + + if (w != null && h != null) { + formats.add(Format.builder().w(w).h(h).build()); + } + + final String sizesAsString = Optional.ofNullable(getString("sizes")) + .orElseGet(() -> getString("ms")); + + if (StringUtils.isNotBlank(sizesAsString)) { + Arrays.stream(sizesAsString.split(",")) + .map(sizeAsString -> sizeAsString.split("x")) + .filter(size -> size.length == 2) + .forEach(size -> formats.add(Format.builder() + .w(toInt(size[0])) + .h(toInt(size[1])) + .build())); + } + + return formats.isEmpty() ? null : formats; + } + + public String tagId() { + return getString("slot"); + } + + public Integer minDuration() { + return getInteger("mindur"); + } + + public Integer maxDuration() { + return getInteger("maxdur"); + } + + public List api() { + return getListOfIntegers("api"); + } + + public List bAttr() { + return getListOfIntegers("battr"); + } + + public List delivery() { + return getListOfIntegers("delivery"); + } + + public Integer linearity() { + return getInteger("linearity"); + } + + public Integer minBitrate() { + return getInteger("minbr"); + } + + public Integer maxBitrate() { + return getInteger("maxbr"); + } + + public Integer maxExtended() { + return getInteger("maxex"); + } + + public Integer maxSeq() { + return getInteger("maxseq"); + } + + public BigDecimal minCpmPerSec() { + try { + final String value = getString("mincpms"); + return StringUtils.isNotBlank(value) ? new BigDecimal(value) : null; + } catch (NumberFormatException e) { + throw new InvalidRequestException("Invalid number: " + e.getMessage()); + } + } + + public Integer podDur() { + return getInteger("poddur"); + } + + public Integer podId() { + return getInteger("podid"); + } + + public Integer podSeq() { + return getInteger("podseq"); + } + + public List protocols() { + return getListOfIntegers("proto"); + } + + public List rqdDurs() { + return getListOfIntegers("rqddurs"); + } + + public Integer sequence() { + return getInteger("seq"); + } + + public Integer slotInPod() { + return getInteger("slotinpod"); + } + + public Integer startDelay() { + return getInteger("startdelay"); + } + + public Integer skip() { + return getInteger("skip"); + } + + public Integer skipAfter() { + return getInteger("skipafter"); + } + + public Integer skipMin() { + return getInteger("skipmin"); + } + + public Integer pos() { + return getInteger("pos"); + } + + public Integer stitched() { + return getInteger("stitched"); + } + + public Integer feed() { + return getInteger("feed"); + } + + public Integer nvol() { + return getInteger("nvol"); + } + + public Integer placement() { + return getInteger("placement"); + } + + public Integer plcmt() { + return getInteger("plcmt"); + } + + public Integer playbackEnd() { + return getInteger("playbackend"); + } + + public List playbackMethod() { + return getListOfIntegers("playbackmethod"); + } + + public Integer boxingAllowed() { + return getInteger("boxingallowed"); + } + + public List bType() { + return getListOfIntegers("btype"); + } + + public List expDir() { + return getListOfIntegers("expdir"); + } + + public Integer topFrame() { + return getInteger("topframe"); + } + + public ObjectNode targeting() { + final ObjectNode targetingNode = AmpRequestFactory.readTargeting(getString("targeting"), mapper); + final String referer = paramsExtractor.refererFrom(httpRequestContext); + ortbTypesResolver.normalizeTargeting(targetingNode, errors, referer); + + return targetingNode; + } + + public Consent consent() { + String tcfConsent = getString("tcfc"); + String usPrivacy = getString("usp"); + String gpp = getString("gppc"); + + final String consentString = Optional.ofNullable(getString("consent_string")) + .orElseGet(() -> getString("gdpr_consent")); + final ConsentType consentType = ConsentType.from(getString("consent_type")); + + switch (consentType) { + case TCF_V1, TCF_V2 -> { + if (tcfConsent == null) { + tcfConsent = consentString; + } + } + case CCPA -> { + if (usPrivacy == null) { + usPrivacy = consentString; + } + } + case GPP -> { + if (gpp == null) { + gpp = consentString; + } + } + case UNKNOWN -> errors.add("Invalid consent_type param passed"); + } + + if (tcfConsent != null && !TcfDefinerService.isConsentStringValid(tcfConsent)) { + errors.add("TCF consent string has invalid format."); + } + + if (usPrivacy != null && !Ccpa.isValid(usPrivacy)) { + errors.add("UsPrivacy string has invalid format."); + } + + return Consent.of(tcfConsent, usPrivacy, gpp); + } + + public Integer gdpr() { + final Integer gdpr = getInteger("gdpr"); + if (gdpr != null) { + return gdpr; + } + + return switch (getString("gdpr_applies")) { + case "true" -> 1; + case "false" -> 0; + case null, default -> null; + }; + } + + public String consentedProviders() { + return getString("addtl_consent"); + } + + public List gppSid() { + return Optional.ofNullable(getListOfIntegers("gpps")) + .orElseGet(() -> getListOfIntegers("gpp_sid")); + } + + public Integer coppa() { + return getInteger("coppa"); + } + + public String gpc() { + return Optional.ofNullable(getString("gpc")) + .orElseGet(() -> paramsExtractor.gpcFrom(httpRequestContext)); + } + + public Integer dnt() { + return getInteger("dnt"); + } + + public Integer lmt() { + return getInteger("lmt"); + } + + public List bCat() { + return getListOfStrings("bcat"); + } + + public List bAdv() { + return getListOfStrings("badv"); + } + + public String page() { + return getString("page"); + } + + public String bundle() { + return getString("bundle"); + } + + public String name() { + return getString("name"); + } + + public String storeUrl() { + return getString("storeurl"); + } + + public String genre() { + return getString("cgenre"); + } + + public String language() { + return getString("clang"); + } + + public String contentRating() { + return getString("crating"); + } + + public List cat() { + return getListOfStrings("ccat"); + } + + public Integer catTax() { + return getInteger("ccattax"); + } + + public String series() { + return Optional.ofNullable(getString("cseries")) + .orElseGet(() -> getString("rss_feed")); + } + + public String title() { + return getString("ctitle"); + } + + public String url() { + return getString("curl"); + } + + public Integer liveStream() { + return getInteger("clivestream"); + } + + public IpAddress ip() { + return Optional.ofNullable(getString("ip")) + .map(ipAddressHelper::toIpAddress) + .orElse(IpAddress.of(null, null)); + } + + public String ua() { + return getString("ua"); + } + + public Integer deviceType() { + return getInteger("dtype"); + } + + public String ifa() { + return getString("ifa"); + } + + public String ifaType() { + return getString("ifat"); + } + + private String getString(String key) { + return httpRequestContext.getQueryParams().get(key); + } + + private Integer getInteger(String key) { + return toInt(getString(key)); + } + + private static Integer toInt(String value) { + try { + return StringUtils.isNotBlank(value) ? Integer.parseInt(value) : null; + } catch (NumberFormatException e) { + throw new InvalidRequestException("Invalid number: " + e.getMessage()); + } + } + + private List getListOfStrings(String key) { + final String value = getString(key); + if (StringUtils.isBlank(value)) { + return null; + } + + return Arrays.asList(value.split(",")); + } + + private List getListOfIntegers(String key) { + final List listOfStrings = getListOfStrings(key); + return listOfStrings != null + ? listOfStrings.stream() + .map(GetInterfaceParams::toInt) + .toList() + : null; + } + } + + @Value(staticConstructor = "of") + private static class Consent { + + String tcfConsent; + + String usPrivacy; + + String gpp; + } +} diff --git a/src/main/java/org/prebid/server/handler/openrtb2/GetInterfaceHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/GetInterfaceHandler.java new file mode 100644 index 00000000000..fa8f8c5a856 --- /dev/null +++ b/src/main/java/org/prebid/server/handler/openrtb2/GetInterfaceHandler.java @@ -0,0 +1,343 @@ +package org.prebid.server.handler.openrtb2; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.auction.AnalyticsTagsEnricher; +import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HookDebugInfoEnricher; +import org.prebid.server.auction.HooksMetricsService; +import org.prebid.server.auction.SkippedAuctionService; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.requestfactory.GetInterfaceRequestFactory; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.exception.BlocklistedAccountException; +import org.prebid.server.exception.BlocklistedAppException; +import org.prebid.server.exception.InvalidAccountConfigException; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.exception.UnauthorizedAccountException; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.HttpInteractionLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.model.Endpoint; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; + +import java.time.Clock; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public class GetInterfaceHandler implements ApplicationResource { + + private static final Logger logger = LoggerFactory.getLogger(GetInterfaceHandler.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + + private final double logSamplingRate; + private final GetInterfaceRequestFactory getInterfaceRequestFactory; + private final ExchangeService exchangeService; + private final SkippedAuctionService skippedAuctionService; + private final AnalyticsReporterDelegator analyticsDelegator; + private final Metrics metrics; + private final HooksMetricsService hooksMetricsService; + private final Clock clock; + private final HttpInteractionLogger httpInteractionLogger; + private final PrebidVersionProvider prebidVersionProvider; + private final HookStageExecutor hookStageExecutor; + private final JacksonMapper mapper; + + public GetInterfaceHandler(double logSamplingRate, + GetInterfaceRequestFactory getInterfaceRequestFactory, + ExchangeService exchangeService, + SkippedAuctionService skippedAuctionService, + AnalyticsReporterDelegator analyticsDelegator, + Metrics metrics, + HooksMetricsService hooksMetricsService, + Clock clock, + HttpInteractionLogger httpInteractionLogger, + PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, + JacksonMapper mapper) { + + this.logSamplingRate = logSamplingRate; + this.getInterfaceRequestFactory = Objects.requireNonNull(getInterfaceRequestFactory); + this.exchangeService = Objects.requireNonNull(exchangeService); + this.skippedAuctionService = Objects.requireNonNull(skippedAuctionService); + this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); + this.metrics = Objects.requireNonNull(metrics); + this.hooksMetricsService = Objects.requireNonNull(hooksMetricsService); + this.clock = Objects.requireNonNull(clock); + this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); + this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.openrtb2_get_interface.value())); + } + + @Override + public void handle(RoutingContext routingContext) { + // Prebid Server interprets request.tmax to be the maximum amount of time that a caller is willing to wait + // for bids. However, tmax may be defined in the Stored Request data. + // If so, then the trip to the backend might use a significant amount of this time. We can respect timeouts + // more accurately if we note the real start time, and use it to compute the auction timeout. + final long startTime = clock.millis(); + + final AuctionEvent.AuctionEventBuilder auctionEventBuilder = AuctionEvent.builder() + .httpContext(HttpRequestContext.from(routingContext)); + + getInterfaceRequestFactory.fromRequest(routingContext, startTime) + .compose(auctionContext -> skippedAuctionService.skipAuction(auctionContext) + .recover(throwable -> holdAuction(auctionEventBuilder, auctionContext))) + .map(context -> addContextAndBidResponseToEvent(context, auctionEventBuilder, context)) + .map(context -> prepareSuccessfulResponse(context, routingContext)) + .compose(this::invokeExitpointHooks) + .map(context -> addContextAndBidResponseToEvent( + context.getAuctionContext(), auctionEventBuilder, context)) + .onComplete(result -> handleResult(result, auctionEventBuilder, routingContext, startTime)); + } + + private Future holdAuction(AuctionEvent.AuctionEventBuilder auctionEventBuilder, + AuctionContext auctionContext) { + + return getInterfaceRequestFactory.enrichAuctionContext(auctionContext) + .map(this::updateAppAndNoCookieAndImpsMetrics) + // In case of holdAuction Exception and auctionContext is not present below + .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context)) + .compose(exchangeService::holdAuction); + } + + private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context) { + if (!context.isRequestRejected()) { + final BidRequest bidRequest = context.getBidRequest(); + final UidsCookie uidsCookie = context.getUidsCookie(); + + final List imps = bidRequest.getImp(); + metrics.updateAppAndNoCookieAndImpsRequestedMetrics( + bidRequest.getApp() != null, + uidsCookie.hasLiveUids(), + imps.size()); + + metrics.updateImpTypesMetrics(imps); + } + + return context; + } + + private static R addToEvent(T field, Consumer consumer, R result) { + consumer.accept(field); + return result; + } + + private static R addContextAndBidResponseToEvent(AuctionContext context, + AuctionEvent.AuctionEventBuilder auctionEventBuilder, + R result) { + + auctionEventBuilder.auctionContext(context); + auctionEventBuilder.bidResponse(context.getBidResponse()); + return result; + } + + private RawResponseContext prepareSuccessfulResponse(AuctionContext auctionContext, RoutingContext routingContext) { + final MultiMap responseHeaders = getCommonResponseHeaders(routingContext) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + + return RawResponseContext.builder() + .responseBody(mapper.encodeToString(auctionContext.getBidResponse())) + .responseHeaders(responseHeaders) + .auctionContext(auctionContext) + .build(); + } + + private MultiMap getCommonResponseHeaders(RoutingContext routingContext) { + final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap(); + HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord()); + + final MultiMap requestHeaders = routingContext.request().headers(); + if (requestHeaders.contains(HttpUtil.SEC_BROWSING_TOPICS_HEADER)) { + responseHeaders.add(HttpUtil.OBSERVE_BROWSING_TOPICS_HEADER, "?1"); + } + + return responseHeaders; + } + + private Future invokeExitpointHooks(RawResponseContext rawResponseContext) { + final AuctionContext auctionContext = rawResponseContext.getAuctionContext(); + + if (auctionContext.isAuctionSkipped()) { + return Future.succeededFuture(auctionContext) + .map(hooksMetricsService::updateHooksMetrics) + .map(rawResponseContext); + } + + return hookStageExecutor.executeExitpointStage( + rawResponseContext.getResponseHeaders(), + rawResponseContext.getResponseBody(), + auctionContext) + .map(HookStageExecutionResult::getPayload) + .compose(payload -> Future.succeededFuture(auctionContext) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) + .map(hooksMetricsService::updateHooksMetrics) + .map(context -> RawResponseContext.builder() + .auctionContext(context) + .responseHeaders(payload.responseHeaders()) + .responseBody(payload.responseBody()) + .build())); + } + + private void handleResult(AsyncResult responseResult, + AuctionEvent.AuctionEventBuilder auctionEventBuilder, + RoutingContext routingContext, + long startTime) { + + final boolean responseSucceeded = responseResult.succeeded(); + + final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null; + final AuctionContext auctionContext = rawResponseContext != null + ? rawResponseContext.getAuctionContext() + : null; + final boolean isAuctionSkipped = responseSucceeded && auctionContext.isAuctionSkipped(); + final MetricName requestType = responseSucceeded + ? auctionContext.getRequestTypeMetric() + : MetricName.openrtb2web; + + final MetricName metricRequestStatus; + final List errorMessages; + final HttpResponseStatus status; + final String body; + + final HttpServerResponse response = routingContext.response(); + final MultiMap responseHeaders = response.headers(); + + if (responseSucceeded) { + metricRequestStatus = MetricName.ok; + errorMessages = Collections.emptyList(); + status = HttpResponseStatus.OK; + + rawResponseContext.getResponseHeaders() + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + body = rawResponseContext.getResponseBody(); + } else { + getCommonResponseHeaders(routingContext) + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + + final Throwable exception = responseResult.cause(); + if (exception instanceof InvalidRequestException invalidRequestException) { + metricRequestStatus = MetricName.badinput; + + errorMessages = invalidRequestException.getMessages().stream() + .map(msg -> "Invalid request format: " + msg) + .toList(); + final String message = String.join("\n", errorMessages); + final String referer = routingContext.request().headers().get(HttpUtil.REFERER_HEADER); + conditionalLogger.info("%s, Referer: %s".formatted(message, referer), logSamplingRate); + + status = HttpResponseStatus.BAD_REQUEST; + body = message; + } else if (exception instanceof UnauthorizedAccountException) { + metricRequestStatus = MetricName.badinput; + final String message = exception.getMessage(); + conditionalLogger.info(message, logSamplingRate); + errorMessages = Collections.singletonList(message); + + status = HttpResponseStatus.UNAUTHORIZED; + + body = message; + } else if (exception instanceof BlocklistedAppException + || exception instanceof BlocklistedAccountException) { + metricRequestStatus = exception instanceof BlocklistedAccountException + ? MetricName.blocklisted_account + : MetricName.blocklisted_app; + final String message = "Blocklisted: " + exception.getMessage(); + logger.debug(message); + + errorMessages = Collections.singletonList(message); + status = HttpResponseStatus.FORBIDDEN; + body = message; + } else if (exception instanceof InvalidAccountConfigException) { + metricRequestStatus = MetricName.bad_requests; + final String message = exception.getMessage(); + conditionalLogger.error(exception.getMessage(), logSamplingRate); + + errorMessages = Collections.singletonList(message); + status = HttpResponseStatus.BAD_REQUEST; + body = message; + } else { + metricRequestStatus = MetricName.err; + logger.error("Critical error while running the auction", exception); + + final String message = exception.getMessage(); + errorMessages = Collections.singletonList(message); + + status = HttpResponseStatus.INTERNAL_SERVER_ERROR; + body = "Critical error while running the auction: " + message; + } + } + + final AuctionEvent auctionEvent = auctionEventBuilder.status(status.code()).errors(errorMessages).build(); + final PrivacyContext privacyContext = auctionContext != null ? auctionContext.getPrivacyContext() : null; + final TcfContext tcfContext = privacyContext != null ? privacyContext.getTcfContext() : TcfContext.empty(); + + final boolean responseSent = respondWith(routingContext, status, body, requestType); + + if (responseSent) { + metrics.updateRequestTimeMetric(MetricName.request_time, clock.millis() - startTime); + metrics.updateRequestTypeMetric(requestType, metricRequestStatus); + if (!isAuctionSkipped) { + analyticsDelegator.processEvent(auctionEvent, tcfContext); + } + } else { + metrics.updateRequestTypeMetric(requestType, MetricName.networkerr); + } + + httpInteractionLogger.maybeLogOpenrtb2GetInterface(auctionContext, routingContext, status.code(), body); + } + + private boolean respondWith(RoutingContext routingContext, + HttpResponseStatus status, + String body, + MetricName requestType) { + + return HttpUtil.executeSafely( + routingContext, + Endpoint.openrtb2_get_interface, + response -> response + .exceptionHandler(throwable -> handleResponseException(throwable, requestType)) + .setStatusCode(status.code()) + .end(body)); + + } + + private void handleResponseException(Throwable throwable, MetricName requestType) { + logger.warn("Failed to send auction response: {}", throwable.getMessage()); + metrics.updateRequestTypeMetric(requestType, MetricName.networkerr); + } +} diff --git a/src/main/java/org/prebid/server/log/HttpInteractionLogger.java b/src/main/java/org/prebid/server/log/HttpInteractionLogger.java index 2ca42da4a45..f79ea6f306d 100644 --- a/src/main/java/org/prebid/server/log/HttpInteractionLogger.java +++ b/src/main/java/org/prebid/server/log/HttpInteractionLogger.java @@ -79,6 +79,22 @@ public void maybeLogOpenrtb2Amp(AuctionContext auctionContext, } } + public void maybeLogOpenrtb2GetInterface(AuctionContext auctionContext, + RoutingContext routingContext, + int statusCode, + String responseBody) { + + if (interactionSatisfiesSpec(HttpLogSpec.Endpoint.get_interface, statusCode, auctionContext)) { + logger.info( + "Requested URL: \"{}\", response status: \"{}\", response body: \"{}\"", + routingContext.request().uri(), + statusCode, + responseBody); + + incLoggedInteractions(); + } + } + public void maybeLogBidderRequest(AuctionContext context, BidderRequest bidderRequest) { final String bidder = bidderRequest.getBidder(); if (interactionSatisfiesSpec(context, bidder)) { diff --git a/src/main/java/org/prebid/server/log/model/HttpLogSpec.java b/src/main/java/org/prebid/server/log/model/HttpLogSpec.java index 75ba4fcfbc6..9f35e96d3ff 100644 --- a/src/main/java/org/prebid/server/log/model/HttpLogSpec.java +++ b/src/main/java/org/prebid/server/log/model/HttpLogSpec.java @@ -16,6 +16,6 @@ public class HttpLogSpec { int limit; public enum Endpoint { - auction, amp + auction, amp, get_interface } } diff --git a/src/main/java/org/prebid/server/model/Endpoint.java b/src/main/java/org/prebid/server/model/Endpoint.java index 71131b0b85b..8acda765b29 100644 --- a/src/main/java/org/prebid/server/model/Endpoint.java +++ b/src/main/java/org/prebid/server/model/Endpoint.java @@ -15,6 +15,7 @@ public enum Endpoint { openrtb2_auction("/openrtb2/auction"), openrtb2_amp("/openrtb2/amp"), openrtb2_video("/openrtb2/video"), + openrtb2_get_interface("/openrtb2/get"), cookie_sync("/cookie_sync"), setuid("/setuid"), diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevice.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevice.java index f8036cc616e..51781afeddb 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevice.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevice.java @@ -15,9 +15,11 @@ public class ExtDevice extends FlexibleExtension { Integer atts; + String ifaType; + ExtDevicePrebid prebid; public static ExtDevice empty() { - return of(null, null); + return of(null, null, null); } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java index 1380e543991..98e9b19ab83 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java @@ -200,4 +200,10 @@ public class ExtRequestPrebid { ExtRequestPrebidAlternateBidderCodes alternateBidderCodes; ObjectNode kvps; + + @JsonProperty("outputformat") + String outputFormat; + + @JsonProperty("outputmodule") + String outputModule; } diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 228d4702435..939e552ba7b 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -60,6 +60,7 @@ import org.prebid.server.auction.privacy.enforcement.PrivacyEnforcementService; import org.prebid.server.auction.requestfactory.AmpRequestFactory; import org.prebid.server.auction.requestfactory.AuctionRequestFactory; +import org.prebid.server.auction.requestfactory.GetInterfaceRequestFactory; import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; import org.prebid.server.auction.requestfactory.Ortb2RequestFactory; import org.prebid.server.auction.requestfactory.VideoRequestFactory; @@ -550,6 +551,45 @@ AmpRequestFactory ampRequestFactory(Ortb2RequestFactory ortb2RequestFactory, geoLocationServiceWrapper); } + @Bean + GetInterfaceRequestFactory getInterfaceRequestFactory( + Ortb2RequestFactory ortb2RequestFactory, + StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, + BidRequestOrtbVersionConversionManager bidRequestOrtbVersionConversionManager, + AuctionGppService auctionGppService, + CookieDeprecationService cookieDeprecationService, + ImplicitParametersExtractor implicitParametersExtractor, + OrtbTypesResolver ortbTypesResolver, + IpAddressHelper ipAddressHelper, + Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver, + FpdResolver fpdResolver, + AuctionPrivacyContextFactory auctionPrivacyContextFactory, + DebugResolver debugResolver, + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper, + BidAdjustmentsEnricher bidAdjustmentsEnricher) { + + return new GetInterfaceRequestFactory( + ortb2RequestFactory, + storedRequestProcessor, + profilesProcessor, + bidRequestOrtbVersionConversionManager, + auctionGppService, + cookieDeprecationService, + implicitParametersExtractor, + ortbTypesResolver, + ipAddressHelper, + ortb2ImplicitParametersResolver, + fpdResolver, + new InterstitialProcessor(), + auctionPrivacyContextFactory, + debugResolver, + mapper, + geoLocationServiceWrapper, + bidAdjustmentsEnricher); + } + @Bean VideoRequestFactory videoRequestFactory( @Value("${auction.max-request-size}") int maxRequestSize, diff --git a/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java index c6ae167ae94..5fe6feb9d85 100644 --- a/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java @@ -24,6 +24,7 @@ import org.prebid.server.auction.privacy.contextfactory.SetuidPrivacyContextFactory; import org.prebid.server.auction.requestfactory.AmpRequestFactory; import org.prebid.server.auction.requestfactory.AuctionRequestFactory; +import org.prebid.server.auction.requestfactory.GetInterfaceRequestFactory; import org.prebid.server.auction.requestfactory.VideoRequestFactory; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.cache.CoreCacheService; @@ -39,9 +40,9 @@ import org.prebid.server.handler.NoCacheHandler; import org.prebid.server.handler.NotificationEventHandler; import org.prebid.server.handler.OptoutHandler; +import org.prebid.server.handler.PostVtrackHandler; import org.prebid.server.handler.SetuidHandler; import org.prebid.server.handler.StatusHandler; -import org.prebid.server.handler.PostVtrackHandler; import org.prebid.server.handler.info.BidderDetailsHandler; import org.prebid.server.handler.info.BiddersHandler; import org.prebid.server.handler.info.filters.BaseOnlyBidderInfoFilterStrategy; @@ -49,6 +50,7 @@ import org.prebid.server.handler.info.filters.EnabledOnlyBidderInfoFilterStrategy; import org.prebid.server.handler.openrtb2.AmpHandler; import org.prebid.server.handler.openrtb2.AuctionHandler; +import org.prebid.server.handler.openrtb2.GetInterfaceHandler; import org.prebid.server.handler.openrtb2.VideoHandler; import org.prebid.server.health.HealthChecker; import org.prebid.server.health.PeriodicHealthChecker; @@ -270,6 +272,35 @@ AmpHandler openrtbAmpHandler( logSamplingRate); } + @Bean + GetInterfaceHandler getInterfaceHandler( + ExchangeService exchangeService, + SkippedAuctionService skippedAuctionService, + GetInterfaceRequestFactory getInterfaceRequestFactory, + AnalyticsReporterDelegator analyticsReporter, + Metrics metrics, + HooksMetricsService hooksMetricsService, + Clock clock, + HttpInteractionLogger httpInteractionLogger, + PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, + JacksonMapper mapper) { + + return new GetInterfaceHandler( + logSamplingRate, + getInterfaceRequestFactory, + exchangeService, + skippedAuctionService, + analyticsReporter, + metrics, + hooksMetricsService, + clock, + httpInteractionLogger, + prebidVersionProvider, + hookStageExecutor, + mapper); + } + @Bean VideoHandler openrtbVideoHandler( VideoRequestFactory videoRequestFactory, diff --git a/src/main/java/org/prebid/server/util/HttpUtil.java b/src/main/java/org/prebid/server/util/HttpUtil.java index e08a276c6fa..144d8eb6b84 100644 --- a/src/main/java/org/prebid/server/util/HttpUtil.java +++ b/src/main/java/org/prebid/server/util/HttpUtil.java @@ -40,8 +40,10 @@ public final class HttpUtil { HttpHeaderValues.APPLICATION_JSON + ";" + HttpHeaderValues.CHARSET + "=" + StandardCharsets.UTF_8.toString().toLowerCase(); + public static final CharSequence TRUE_CLIENT_IP_HEADER = HttpHeaders.createOptimized("True-Client-Ip"); public static final CharSequence X_FORWARDED_FOR_HEADER = HttpHeaders.createOptimized("X-Forwarded-For"); public static final CharSequence X_REAL_IP_HEADER = HttpHeaders.createOptimized("X-Real-Ip"); + public static final CharSequence X_DEVICE_IP_HEADER = HttpHeaders.createOptimized("X-Device-Ip"); public static final CharSequence DNT_HEADER = HttpHeaders.createOptimized("DNT"); public static final CharSequence ORIGIN_HEADER = HttpHeaders.createOptimized("Origin"); public static final CharSequence ACCEPT_HEADER = HttpHeaders.createOptimized("Accept"); @@ -53,6 +55,7 @@ public final class HttpUtil { public static final CharSequence X_REQUESTED_WITH_HEADER = HttpHeaders.createOptimized("X-Requested-With"); public static final CharSequence REFERER_HEADER = HttpHeaders.createOptimized("Referer"); public static final CharSequence USER_AGENT_HEADER = HttpHeaders.createOptimized("User-Agent"); + public static final CharSequence X_DEVICE_USER_AGENT_HEADER = HttpHeaders.createOptimized("X-Device-User-Agent"); public static final CharSequence COOKIE_HEADER = HttpHeaders.createOptimized("Cookie"); public static final CharSequence SEC_COOKIE_DEPRECATION = HttpHeaders.createOptimized("Sec-Cookie-Deprecation"); diff --git a/src/test/java/org/prebid/server/auction/FpdResolverTest.java b/src/test/java/org/prebid/server/auction/FpdResolverTest.java index 1e094a120d6..a97d1048903 100644 --- a/src/test/java/org/prebid/server/auction/FpdResolverTest.java +++ b/src/test/java/org/prebid/server/auction/FpdResolverTest.java @@ -198,7 +198,7 @@ public void resolveDeviceShouldReturnFpdDeviceIfOriginDeviceIsNull() { public void resolveDeviceShouldNotChangeOriginExtDataIfFPDDoesNotHaveExt() { // given final Device originDevice = Device.builder() - .ext(ExtDevice.of(1, ExtDevicePrebid.of(ExtDeviceInt.of(10, 20)))) + .ext(ExtDevice.of(1, null, ExtDevicePrebid.of(ExtDeviceInt.of(10, 20)))) .build(); final Device fpdDevice = Device.builder().build(); @@ -208,7 +208,7 @@ public void resolveDeviceShouldNotChangeOriginExtDataIfFPDDoesNotHaveExt() { // then assertThat(resultDevice).isEqualTo(Device.builder() - .ext(ExtDevice.of(1, ExtDevicePrebid.of(ExtDeviceInt.of(10, 20)))) + .ext(ExtDevice.of(1, null, ExtDevicePrebid.of(ExtDeviceInt.of(10, 20)))) .build()); } diff --git a/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java b/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java index 4062e9f4651..6f001efc1f9 100644 --- a/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java +++ b/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java @@ -127,12 +127,13 @@ public void ipFromShouldReturnIpFromHeadersAndRemoteAddress() { .add("True-Client-IP", "192.168.144.1 ") .add("X-Forwarded-For", "192.168.144.2 , 192.168.144.3 ") .add("X-Real-IP", "192.168.144.4 ") + .add("X-Device-IP", "192.168.144.5") .build(); - final String remoteHost = "192.168.144.5"; + final String remoteHost = "192.168.144.6"; // when and then assertThat(extractor.ipFrom(headers, remoteHost)).containsExactly( - "192.168.144.1", "192.168.144.2", "192.168.144.3", "192.168.144.4", remoteHost); + "192.168.144.1", "192.168.144.2", "192.168.144.3", "192.168.144.4", "192.168.144.5", remoteHost); } @Test @@ -153,6 +154,7 @@ public void uaFromShouldReturnUaFromUserAgentHeader() { final HttpRequestContext httpRequest = HttpRequestContext.builder() .headers(CaseInsensitiveMultiMap.builder() .add(HttpUtil.USER_AGENT_HEADER, " user agent ") + .add(HttpUtil.X_DEVICE_USER_AGENT_HEADER, " device user agent ") .build()) .build(); @@ -160,6 +162,19 @@ public void uaFromShouldReturnUaFromUserAgentHeader() { assertThat(extractor.uaFrom(httpRequest)).isEqualTo("user agent"); } + @Test + public void uaFromShouldReturnUaFromXDeviceUserAgentHeader() { + // given + final HttpRequestContext httpRequest = HttpRequestContext.builder() + .headers(CaseInsensitiveMultiMap.builder() + .add(HttpUtil.X_DEVICE_USER_AGENT_HEADER, " device user agent ") + .build()) + .build(); + + // when and then + assertThat(extractor.uaFrom(httpRequest)).isEqualTo("device user agent"); + } + @Test public void secureFromShouldReturnOneIfXForwardedProtoIsHttps() { // given diff --git a/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java b/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java index 662c58e7aa8..f3886588328 100644 --- a/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java @@ -29,7 +29,7 @@ public void processShouldReturnBidRequestUpdatedWithImpsInterstitialFormat() { .format(singletonList(Format.builder().w(400).h(600).build())).build()).instl(1) .build())) .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) .build()) .build(); @@ -53,7 +53,7 @@ public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsUsingSize final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder().banner(Banner.builder().build()).instl(1).build())) .device(Device.builder().w(400).h(600) - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) .build()) .build(); @@ -78,7 +78,7 @@ public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsUsingSize .imp(singletonList(Imp.builder().banner(Banner.builder().format(singletonList( Format.builder().w(1).h(1).build())).build()).instl(1).build())) .device(Device.builder().w(400).h(600) - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) .build()) .build(); @@ -104,7 +104,7 @@ public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsLimitedBy .format(singletonList(Format.builder().w(400).h(600).build())).build()).instl(1) .build())) .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(1, 1)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(1, 1)))) .build()) .build(); @@ -133,7 +133,7 @@ public void processShouldNotUpdateBidRequestWhenImpInstlIsEqualToZero() { final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder().banner(Banner.builder().build()).instl(0).build())) .device(Device.builder().w(400).h(600) - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) .build()) .build(); @@ -171,7 +171,7 @@ public void processShouldNotUpdateImpWithInstlZeroAndUpdateImpWithIstlOne() { .format(singletonList(Format.builder().w(400).h(600).build())) .build()).instl(1).build())) .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) .build()) .build(); @@ -192,7 +192,7 @@ public void processShouldNotUpdateImpWithInstlZeroAndUpdateImpWithIstlOne() { Format.builder().w(320).h(481).build())) .build()).instl(1).build())) .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) .build()) .build()); } @@ -203,7 +203,7 @@ public void processShouldNotUpdateImpWithoutBanner() { final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder().instl(1).build())) .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) .build()) .build(); @@ -214,7 +214,7 @@ public void processShouldNotUpdateImpWithoutBanner() { assertThat(result).isEqualTo(BidRequest.builder() .imp(singletonList(Imp.builder().instl(1).build())) .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) .build()) .build()); } @@ -228,7 +228,7 @@ public void processShouldThrowInvalidRequestExceptionWhenCantFindMaxWidthOrMaxHe .format(singletonList(Format.builder().w(1).h(1).build())) .build()).instl(1).build())) .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) .build()) .build(); @@ -248,7 +248,7 @@ public void processShouldNotUpdateImpWhenInterstitialSizesWereNotFound() { .format(singletonList(Format.builder().w(10).h(10).build())) .build()).instl(1).build())) .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) .build()) .build(); @@ -262,7 +262,7 @@ public void processShouldNotUpdateImpWhenInterstitialSizesWereNotFound() { .format(singletonList(Format.builder().w(10).h(10).build())) .build()).instl(1).build())) .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) .build()) .build()); } diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index 2e30e8d1bc4..233bbcfaa71 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -125,7 +125,7 @@ public class AuctionRequestFactoryTest extends VertxTest { private Account defaultAccount; private BidRequest defaultBidRequest; - private AuctionContext defaultActionContext; + private AuctionContext defaultAuctionContext; @BeforeEach public void setUp() { @@ -140,7 +140,7 @@ public void setUp() { .coppa(0) .build(), TcfContext.empty()); - defaultActionContext = AuctionContext.builder() + defaultAuctionContext = AuctionContext.builder() .requestTypeMetric(MetricName.openrtb2web) .bidRequest(defaultBidRequest) .account(defaultAccount) @@ -167,7 +167,7 @@ public void setUp() { given(debugResolver.debugContextFrom(any())).willReturn(DebugContext.of(true, true, null)); - given(ortb2RequestFactory.createAuctionContext(any(), any())).willReturn(defaultActionContext); + given(ortb2RequestFactory.createAuctionContext(any(), any())).willReturn(defaultAuctionContext); given(ortb2RequestFactory.executeEntrypointHooks(any(), any(), any())) .willAnswer(invocation -> toHttpRequest(invocation.getArgument(0), invocation.getArgument(1))); given(ortb2RequestFactory.executeRawAuctionRequestHooks(any())) @@ -375,7 +375,7 @@ public void shouldEnrichAuctionContextWithDebugContext() { givenValidBidRequest(); // when - final Future result = target.enrichAuctionContext(defaultActionContext); + final Future result = target.enrichAuctionContext(defaultAuctionContext); // then verify(debugResolver).debugContextFrom(any()); @@ -398,7 +398,7 @@ public void shouldUseBidRequestModifiedByRawAuctionRequestHooks() { .executeRawAuctionRequestHooks(any()); // when - target.enrichAuctionContext(defaultActionContext); + target.enrichAuctionContext(defaultAuctionContext); // then final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); @@ -425,7 +425,7 @@ public void shouldReturnFailedFutureIfRawAuctionRequestHookRejectedRequest() { .restoreResultFromRejection(eq(exception)); // when - final Future future = target.enrichAuctionContext(defaultActionContext); + final Future future = target.enrichAuctionContext(defaultAuctionContext); // then assertThat(future).succeededWith(auctionContext); @@ -446,7 +446,7 @@ public void shouldUseBidRequestModifiedByProcessedAuctionRequestHooks() { .executeProcessedAuctionRequestHooks(any()); // when - final Future result = target.enrichAuctionContext(defaultActionContext); + final Future result = target.enrichAuctionContext(defaultAuctionContext); // then final BidRequest resultBidRequest = result.result().getBidRequest(); @@ -470,7 +470,7 @@ public void shouldReturnFailedFutureIfProcessedAuctionRequestHookRejectedRequest .restoreResultFromRejection(eq(exception)); // when - final Future future = target.enrichAuctionContext(defaultActionContext); + final Future future = target.enrichAuctionContext(defaultAuctionContext); // then assertThat(future).succeededWith(auctionContext); @@ -583,7 +583,7 @@ public void shouldReturnFailedFutureIfOrtb2RequestFactoryReturnedFailedFuture() given(ortb2RequestFactory.fetchAccount(any())).willReturn(Future.failedFuture("error")); // when - final Future future = target.enrichAuctionContext(defaultActionContext); + final Future future = target.enrichAuctionContext(defaultAuctionContext); // then assertThat(future.failed()).isTrue(); @@ -648,7 +648,7 @@ public void storedRequestProcessorShouldUseAccountIdFetchedByOrtb2RequestFactory givenValidBidRequest(); // when - target.enrichAuctionContext(defaultActionContext); + target.enrichAuctionContext(defaultAuctionContext); // then verify(storedRequestProcessor).processAuctionRequest(eq(ACCOUNT_ID), any()); @@ -662,7 +662,7 @@ public void shouldReturnFailedFutureIfProcessStoredRequestsFailed() { .willReturn(Future.failedFuture("error")); // when - final Future future = target.enrichAuctionContext(defaultActionContext); + final Future future = target.enrichAuctionContext(defaultAuctionContext); // then assertThat(future.failed()).isTrue(); @@ -678,7 +678,7 @@ public void shouldReturnFailedFutureIfRequestValidationFailed() { .willReturn(Future.failedFuture(new InvalidRequestException("errors"))); // when - final Future future = target.enrichAuctionContext(defaultActionContext); + final Future future = target.enrichAuctionContext(defaultAuctionContext); // then assertThat(future.failed()).isTrue(); @@ -695,7 +695,7 @@ public void shouldReturnAuctionContextWithExpectedParameters() { final AuctionContext result = target.parseRequest(routingContext, 0L).result(); // then - assertThat(result).isEqualTo(defaultActionContext); + assertThat(result).isEqualTo(defaultAuctionContext); } @Test @@ -708,7 +708,7 @@ public void shouldReturnModifiedBidRequestInAuctionContextWhenRequestWasPopulate given(bidAdjustmentsEnricher.enrichBidRequest(any())).willReturn(updatedBidRequest); // when - final AuctionContext result = target.enrichAuctionContext(defaultActionContext).result(); + final AuctionContext result = target.enrichAuctionContext(defaultAuctionContext).result(); // then assertThat(result.getBidRequest()).isEqualTo(updatedBidRequest); @@ -732,7 +732,7 @@ public void shouldReturnPopulatedPrivacyContextAndGetWhenPrivacyEnforcementRetur .willReturn(Future.succeededFuture(privacyContext)); // when - final AuctionContext result = target.enrichAuctionContext(defaultActionContext).result(); + final AuctionContext result = target.enrichAuctionContext(defaultAuctionContext).result(); // then assertThat(result.getPrivacyContext()).isEqualTo(privacyContext); @@ -757,7 +757,7 @@ public void shouldReturnPopulatedBidAdjustments() { given(bidAdjustmentsEnricher.enrichBidRequest(any())).willReturn(givenBidRequest); // when - final AuctionContext result = target.enrichAuctionContext(defaultActionContext).result(); + final AuctionContext result = target.enrichAuctionContext(defaultAuctionContext).result(); // then assertThat(result) @@ -780,7 +780,7 @@ public void shouldConvertBidRequestToInternalOpenRTBVersion() { .build()); // when - target.enrichAuctionContext(defaultActionContext); + target.enrichAuctionContext(defaultAuctionContext); // then verify(paramsResolver).resolve( @@ -802,7 +802,7 @@ public void shouldUpdateTimeout() { }); // when - final Future future = target.enrichAuctionContext(defaultActionContext); + final Future future = target.enrichAuctionContext(defaultAuctionContext); // then assertThat(future).isSucceeded(); @@ -823,7 +823,7 @@ public void shouldUseProfilesResult() { .build())); // when - target.enrichAuctionContext(defaultActionContext); + target.enrichAuctionContext(defaultAuctionContext); // then verify(paramsResolver).resolve( @@ -843,7 +843,7 @@ private void givenBidRequest(BidRequest bidRequest) { private void givenAuctionContext(BidRequest bidRequest, Account account) { given(ortb2RequestFactory.enrichAuctionContext(any(), any(), any(), anyLong())) - .willReturn(defaultActionContext.toBuilder() + .willReturn(defaultAuctionContext.toBuilder() .bidRequest(bidRequest) .build()); given(ortb2RequestFactory.fetchAccount(any())).willReturn(Future.succeededFuture(account)); diff --git a/src/test/java/org/prebid/server/auction/requestfactory/GetInterfaceRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/GetInterfaceRequestFactoryTest.java new file mode 100644 index 00000000000..fa82b267941 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/requestfactory/GetInterfaceRequestFactoryTest.java @@ -0,0 +1,2096 @@ +package org.prebid.server.auction.requestfactory; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Content; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Dooh; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iab.openrtb.request.Video; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.net.impl.SocketAddressImpl; +import io.vertx.ext.web.RequestBody; +import io.vertx.ext.web.RoutingContext; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.DebugResolver; +import org.prebid.server.auction.FpdResolver; +import org.prebid.server.auction.GeoLocationServiceWrapper; +import org.prebid.server.auction.ImplicitParametersExtractor; +import org.prebid.server.auction.InterstitialProcessor; +import org.prebid.server.auction.IpAddressHelper; +import org.prebid.server.auction.OrtbTypesResolver; +import org.prebid.server.auction.externalortb.ProfilesProcessor; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; +import org.prebid.server.auction.gpp.AuctionGppService; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.IpAddress; +import org.prebid.server.auction.model.debug.DebugContext; +import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; +import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; +import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; +import org.prebid.server.cookie.CookieDeprecationService; +import org.prebid.server.geolocation.model.GeoInfo; +import org.prebid.server.metric.MetricName; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.Endpoint; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.privacy.ccpa.Ccpa; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.model.Privacy; +import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.openrtb.ext.request.ConsentedProvidersSettings; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.settings.model.Account; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.function.UnaryOperator.identity; +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class GetInterfaceRequestFactoryTest extends VertxTest { + + private static final String ACCOUNT_ID = "acc_id"; + + @Mock(strictness = LENIENT) + private Ortb2RequestFactory ortb2RequestFactory; + @Mock(strictness = LENIENT) + private StoredRequestProcessor storedRequestProcessor; + @Mock(strictness = LENIENT) + private ProfilesProcessor profilesProcessor; + @Mock(strictness = LENIENT) + private BidRequestOrtbVersionConversionManager ortbVersionConversionManager; + @Mock(strictness = LENIENT) + private AuctionGppService auctionGppService; + @Mock(strictness = LENIENT) + private CookieDeprecationService cookieDeprecationService; + @Mock(strictness = LENIENT) + private ImplicitParametersExtractor paramsExtractor; + @Mock(strictness = LENIENT) + private OrtbTypesResolver ortbTypesResolver; + @Mock(strictness = LENIENT) + private IpAddressHelper ipAddressHelper; + @Mock(strictness = LENIENT) + private Ortb2ImplicitParametersResolver paramsResolver; + @Mock(strictness = LENIENT) + private FpdResolver fpdResolver; + @Mock(strictness = LENIENT) + private InterstitialProcessor interstitialProcessor; + @Mock(strictness = LENIENT) + private AuctionPrivacyContextFactory auctionPrivacyContextFactory; + @Mock(strictness = LENIENT) + private DebugResolver debugResolver; + @Mock(strictness = LENIENT) + private GeoLocationServiceWrapper geoLocationServiceWrapper; + @Mock(strictness = LENIENT) + private BidAdjustmentsEnricher bidAdjustmentsEnricher; + + private GetInterfaceRequestFactory target; + + @Mock(strictness = LENIENT) + private RoutingContext routingContext; + @Mock(strictness = LENIENT) + private HttpServerRequest httpRequest; + @Mock(strictness = LENIENT) + private RequestBody requestBody; + + private BidRequest defaultBidRequest; + private AuctionContext defaultAuctionContext; + + @BeforeEach + public void setUp() { + given(routingContext.request()).willReturn(httpRequest); + given(routingContext.queryParams()).willReturn(MultiMap.caseInsensitiveMultiMap()); + given(routingContext.body()).willReturn(requestBody); + given(httpRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap()); + given(httpRequest.remoteAddress()).willReturn(new SocketAddressImpl(1234, "host")); + + defaultBidRequest = BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .storedrequest(ExtStoredRequest.of("defaultSrid")) + .build())) + .build(); + + defaultAuctionContext = AuctionContext.builder() + .httpRequest(toHttpRequest(routingContext, null).result()) + .bidRequest(defaultBidRequest) + .requestTypeMetric(MetricName.openrtb2web) + .prebidErrors(new ArrayList<>()) + .privacyContext(PrivacyContext.of( + Privacy.builder() + .gdpr("0") + .consentString(EMPTY) + .ccpa(Ccpa.EMPTY) + .coppa(0) + .build(), + TcfContext.empty())) + .debugContext(DebugContext.of(true, true, null)) + .build(); + + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(2))); + + given(profilesProcessor.process(any(), any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(1))); + + given(ortbVersionConversionManager.convertToAuctionSupportedVersion(any())) + .willAnswer(invocation -> invocation.getArgument(0)); + + given(auctionGppService.contextFrom(any())).willReturn(Future.succeededFuture()); + given(auctionGppService.updateBidRequest(any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + + given(cookieDeprecationService.updateBidRequestDevice(any(), any())) + .will(invocationOnMock -> invocationOnMock.getArgument(0)); + + given(paramsResolver.resolve(any(), any(), any(), anyBoolean())) + .will(invocationOnMock -> invocationOnMock.getArgument(0)); + + given(interstitialProcessor.process(any())) + .will(invocationOnMock -> invocationOnMock.getArgument(0)); + + given(auctionPrivacyContextFactory.contextFrom(any())) + .willReturn(Future.succeededFuture(defaultAuctionContext.getPrivacyContext())); + + given(debugResolver.debugContextFrom(any())).willReturn(defaultAuctionContext.getDebugContext()); + + given(geoLocationServiceWrapper.lookup(any())) + .willReturn(Future.succeededFuture(GeoInfo.builder().vendor("vendor").build())); + + given(bidAdjustmentsEnricher.enrichBidRequest(any())) + .willAnswer(invocation -> ((AuctionContext) invocation.getArgument(0)).getBidRequest()); + + given(ortb2RequestFactory.createAuctionContext(any(), any())).willReturn(defaultAuctionContext); + given(ortb2RequestFactory.executeEntrypointHooks(any(), any(), any())) + .willAnswer(invocation -> toHttpRequest(invocation.getArgument(0), invocation.getArgument(1))); + given(ortb2RequestFactory.enrichAuctionContext(any(), any(), any(), anyLong())) + .willAnswer(invocation -> ((AuctionContext) invocation.getArgument(0)).toBuilder() + .bidRequest(invocation.getArgument(2)) + .build()); + given(ortb2RequestFactory.restoreResultFromRejection(any())) + .willAnswer(invocation -> Future.failedFuture((Throwable) invocation.getArgument(0))); + + given(ortb2RequestFactory.fetchAccount(any())).willReturn(Future.succeededFuture(Account.empty(ACCOUNT_ID))); + given(ortb2RequestFactory.enrichBidRequestWithGeolocationData(any())) + .willAnswer(invocation -> Future.succeededFuture( + ((AuctionContext) invocation.getArgument(0)).getBidRequest())); + given(ortb2RequestFactory.activityInfrastructureFrom(any())) + .willReturn(Future.succeededFuture()); + given(ortb2RequestFactory.removeEmptyEids(any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + given(ortb2RequestFactory.limitImpressions(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(1))); + given(ortb2RequestFactory.validateRequest(any(), any(), any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(1))); + given(ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(any())) + .willAnswer(invocation -> Future.succeededFuture( + ((AuctionContext) invocation.getArgument(0)).getBidRequest())); + given(ortb2RequestFactory.executeProcessedAuctionRequestHooks(any())) + .willAnswer(invocation -> Future.succeededFuture( + ((AuctionContext) invocation.getArgument(0)).getBidRequest())); + given(ortb2RequestFactory.updateTimeout(any())).willAnswer(invocation -> invocation.getArgument(0)); + + target = new GetInterfaceRequestFactory( + ortb2RequestFactory, + storedRequestProcessor, + profilesProcessor, + ortbVersionConversionManager, + auctionGppService, + cookieDeprecationService, + paramsExtractor, + ortbTypesResolver, + ipAddressHelper, + paramsResolver, + fpdResolver, + interstitialProcessor, + auctionPrivacyContextFactory, + debugResolver, + jacksonMapper, + geoLocationServiceWrapper, + bidAdjustmentsEnricher); + } + + @Test + public void fromRequestShouldProceedAllExpectedSteps() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap().add("srid", "stored_id")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final InOrder inOrder = inOrder(ortb2RequestFactory); + inOrder.verify(ortb2RequestFactory) + .createAuctionContext(eq(Endpoint.openrtb2_get_interface), eq(MetricName.openrtb2web)); + inOrder.verify(ortb2RequestFactory).executeEntrypointHooks(any(), any(), any()); + inOrder.verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), any(), anyLong()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void fromRequestShouldRestoreResultFromRejection() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap()); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.failed()).isTrue(); + + final InOrder inOrder = inOrder(ortb2RequestFactory); + inOrder.verify(ortb2RequestFactory) + .createAuctionContext(eq(Endpoint.openrtb2_get_interface), eq(MetricName.openrtb2web)); + inOrder.verify(ortb2RequestFactory).executeEntrypointHooks(any(), any(), any()); + inOrder.verify(ortb2RequestFactory).restoreResultFromRejection(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void fromRequestShouldReadAllInitialFields() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + // Device + .add("dnt", "0") + .add("lmt", "1") + .add("ua", "ua") + .add("dtype", "2") + .add("ifa", "ifa") + .add("ifat", "ifaType") + + // User + .add("addtl_consent", "consentedProviders") + + .add("tmax", "3") + .add("bcat", "bCat0,bCat1") + .add("badv", "bAdv0,bAdv1") + + // Regs + .add("coppa", "4") + .add("gdpr", "5") + .add("gpps", "6,7") + .add("gpc", "gpc") + + // Ext + .add("debug", "8") + .add("srid", "storedRequestId") + .add("rprof", "requestProfile0,requestProfile1") + .add("sarid", "storedAuctionResponseId") + .add("of", "outputFormat") + .add("om", "outputModule")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest).isEqualTo(BidRequest.builder() + .device(Device.builder() + .dnt(0) + .lmt(1) + .ua("ua") + .devicetype(2) + .ifa("ifa") + .ext(ExtDevice.of(null, "ifaType", null)) + .build()) + .user(User.builder() + .ext(ExtUser.builder() + .consentedProvidersSettings(ConsentedProvidersSettings.of("consentedProviders")) + .build()) + .build()) + .tmax(3L) + .bcat(List.of("bCat0", "bCat1")) + .badv(List.of("bAdv0", "bAdv1")) + .regs(Regs.builder() + .coppa(4) + .gdpr(5) + .gppSid(List.of(6, 7)) + .ext(ExtRegs.of(null, null, "gpc", null)) + .build()) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .debug(8) + .storedrequest(ExtStoredRequest.of("storedRequestId")) + .profiles(List.of("requestProfile0", "requestProfile1")) + .storedAuctionResponse(ExtStoredAuctionResponse.of("storedAuctionResponseId", null, null)) + .outputFormat("outputFormat") + .outputModule("outputModule") + .build())) + .build()); + } + + @Test + public void fromRequestShouldReadStoredRequestIdFromTagId() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap().add("tag_id", "storedRequestId")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getExt()) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getStoredrequest) + .extracting(ExtStoredRequest::getId) + .isEqualTo("storedRequestId"); + } + + @Test + public void fromRequestShouldFailIfStoredRequestMissed() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap()); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isFalse(); + assertThat(result.cause()).hasMessage("Request require the stored request id."); + } + + @Test + public void fromRequestShouldFailOnInvalidTmax() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("tmax", "12a3")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isFalse(); + assertThat(result.cause()).hasMessage("Invalid number: For input string: \"12a3\""); + } + + @Test + public void fromRequestShouldUseDefaultDebug() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap().add("srid", "storedRequestId")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getExt()) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getDebug) + .isEqualTo(0); + } + + @Test + public void fromRequestShouldReadIpV4() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("ip", "ip")); + given(ipAddressHelper.toIpAddress(eq("ip"))) + .willReturn(IpAddress.of("ip", IpAddress.IP.v4)); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getDevice()) + .isEqualTo(Device.builder() + .ip("ip") + .ext(ExtDevice.of(null, null, null)) + .build()); + } + + @Test + public void fromRequestShouldReadIpV6() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("ip", "ip")); + given(ipAddressHelper.toIpAddress(eq("ip"))) + .willReturn(IpAddress.of("ip", IpAddress.IP.v6)); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getDevice()) + .isEqualTo(Device.builder() + .ipv6("ip") + .ext(ExtDevice.of(null, null, null)) + .build()); + } + + @Test + public void fromRequestShouldReadGdprAs1FromGdprApplies() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("gdpr_applies", "true")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getGdpr) + .isEqualTo(1); + } + + @Test + public void fromRequestShouldReadGdprAs0FromGdprApplies() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("gdpr_applies", "false")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getGdpr) + .isEqualTo(0); + } + + @Test + public void fromRequestShouldReadGppSidFromAlias() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("gpp_sid", "1,2")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getGppSid) + .asInstanceOf(InstanceOfAssertFactories.list(Integer.class)) + .containsExactly(1, 2); + } + + @Test + public void fromRequestShouldReadGpcFromHeaders() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap().add("srid", "storedRequestId")); + given(paramsExtractor.gpcFrom(any())).willReturn("gpc"); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getExt) + .extracting(ExtRegs::getGpc) + .isEqualTo("gpc"); + } + + @Test + public void fromRequestShouldReadTcfConsent() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("tcfc", "tcfConsent")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getUser()) + .extracting(User::getConsent) + .isEqualTo("tcfConsent"); + } + + @Test + public void fromRequestShouldReadUspConsent() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("usp", "usPrivacy")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getUsPrivacy) + .isEqualTo("usPrivacy"); + } + + @Test + public void fromRequestShouldReadGppConsent() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("gppc", "gpp")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getGpp) + .isEqualTo("gpp"); + } + + @Test + public void fromRequestShouldIgnoreNonPrimaryConsentFromConsentStringParam() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("tcfc", "tcfConsent") + .add("usp", "usPrivacy") + .add("gppc", "gpp") + .add("consent_type", "4") + .add("consent_string", "oldConsent")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getUser()) + .extracting(User::getConsent) + .isEqualTo("tcfConsent"); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getUsPrivacy, Regs::getGpp) + .containsExactly("usPrivacy", "gpp"); + } + + @Test + public void fromRequestShouldIgnoreNonPrimaryConsentFromGdprConsentParam() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("tcfc", "tcfConsent") + .add("usp", "usPrivacy") + .add("gppc", "gpp") + .add("consent_type", "4") + .add("gdpr_consent", "oldConsent")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getUser()) + .extracting(User::getConsent) + .isEqualTo("tcfConsent"); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getUsPrivacy, Regs::getGpp) + .containsExactly("usPrivacy", "gpp"); + } + + @Test + public void fromRequestShouldUseNonPrimaryConsentFromConsentStringParamForTcfV1() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("usp", "usPrivacy") + .add("gppc", "gpp") + .add("consent_type", "1") + .add("consent_string", "oldConsent")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getUser()) + .extracting(User::getConsent) + .isEqualTo("oldConsent"); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getUsPrivacy, Regs::getGpp) + .containsExactly("usPrivacy", "gpp"); + } + + @Test + public void fromRequestShouldUseNonPrimaryConsentFromGdprConsentParamForTcfV1() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("usp", "usPrivacy") + .add("gppc", "gpp") + .add("consent_type", "1") + .add("gdpr_consent", "oldConsent")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getUser()) + .extracting(User::getConsent) + .isEqualTo("oldConsent"); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getUsPrivacy, Regs::getGpp) + .containsExactly("usPrivacy", "gpp"); + } + + @Test + public void fromRequestShouldUseNonPrimaryConsentFromConsentStringParamForTcfV2() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("usp", "usPrivacy") + .add("gppc", "gpp") + .add("consent_type", "2") + .add("consent_string", "oldConsent")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getUser()) + .extracting(User::getConsent) + .isEqualTo("oldConsent"); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getUsPrivacy, Regs::getGpp) + .containsExactly("usPrivacy", "gpp"); + } + + @Test + public void fromRequestShouldUseNonPrimaryConsentFromGdprConsentParamForTcfV2() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("usp", "usPrivacy") + .add("gppc", "gpp") + .add("consent_type", "2") + .add("gdpr_consent", "oldConsent")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getUser()) + .extracting(User::getConsent) + .isEqualTo("oldConsent"); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getUsPrivacy, Regs::getGpp) + .containsExactly("usPrivacy", "gpp"); + } + + @Test + public void fromRequestShouldUseNonPrimaryConsentFromConsentStringParamForUsPrivacy() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("tcfc", "tcfConsent") + .add("gppc", "gpp") + .add("consent_type", "3") + .add("consent_string", "oldConsent")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getUser()) + .extracting(User::getConsent) + .isEqualTo("tcfConsent"); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getUsPrivacy, Regs::getGpp) + .containsExactly("oldConsent", "gpp"); + } + + @Test + public void fromRequestShouldUseNonPrimaryConsentFromGdprConsentParamForUsPrivacy() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("tcfc", "tcfConsent") + .add("gppc", "gpp") + .add("consent_type", "3") + .add("gdpr_consent", "oldConsent")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getUser()) + .extracting(User::getConsent) + .isEqualTo("tcfConsent"); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getUsPrivacy, Regs::getGpp) + .containsExactly("oldConsent", "gpp"); + } + + @Test + public void fromRequestShouldUseNonPrimaryConsentFromConsentStringParamForGpp() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("tcfc", "tcfConsent") + .add("usp", "usPrivacy") + .add("consent_type", "4") + .add("consent_string", "oldConsent")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getUser()) + .extracting(User::getConsent) + .isEqualTo("tcfConsent"); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getUsPrivacy, Regs::getGpp) + .containsExactly("usPrivacy", "oldConsent"); + } + + @Test + public void fromRequestShouldUseNonPrimaryConsentFromGdprConsentParamForGpp() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("tcfc", "tcfConsent") + .add("usp", "usPrivacy") + .add("consent_type", "4") + .add("gdpr_consent", "oldConsent")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong()); + + final BidRequest initialBidRequest = captor.getValue(); + assertThat(initialBidRequest.getUser()) + .extracting(User::getConsent) + .isEqualTo("tcfConsent"); + assertThat(initialBidRequest.getRegs()) + .extracting(Regs::getUsPrivacy, Regs::getGpp) + .containsExactly("usPrivacy", "oldConsent"); + } + + @Test + public void fromRequestShouldEmitErrorOnInvalidConsentType() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("consent_type", "123")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result().getPrebidErrors()) + .containsExactly("Invalid consent_type param passed"); + } + + @Test + public void fromRequestShouldEmitErrorOnInvalidTcfConsent() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("tcfc", "invalid")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result().getPrebidErrors()) + .containsExactly("TCF consent string has invalid format."); + } + + @Test + public void fromRequestShouldEmitErrorOnInvalidUspConsent() { + // given + givenQueryParams(MultiMap.caseInsensitiveMultiMap() + .add("srid", "storedRequestId") + .add("usp", "invalid")); + + // when + final Future result = target.fromRequest(routingContext, 0); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result().getPrebidErrors()) + .containsExactly("UsPrivacy string has invalid format."); + } + + @Test + public void enrichAuctionContextShouldProceedAllExpectedSteps() { + // when + final Future result = target.enrichAuctionContext(defaultAuctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final InOrder inOrder = inOrder( + ortb2RequestFactory, + debugResolver, + storedRequestProcessor, + profilesProcessor, + geoLocationServiceWrapper, + auctionGppService, + ortbVersionConversionManager, + paramsResolver, + cookieDeprecationService, + interstitialProcessor, + auctionPrivacyContextFactory, + bidAdjustmentsEnricher); + + inOrder.verify(ortb2RequestFactory).fetchAccount(any()); + inOrder.verify(debugResolver).debugContextFrom(any()); + inOrder.verify(storedRequestProcessor).processAmpRequest(any(), any(), any()); + inOrder.verify(profilesProcessor).process(any(), any()); + inOrder.verify(geoLocationServiceWrapper).lookup(any()); + inOrder.verify(ortb2RequestFactory).enrichBidRequestWithGeolocationData(any()); + inOrder.verify(auctionGppService).contextFrom(any()); + inOrder.verify(ortb2RequestFactory).activityInfrastructureFrom(any()); + inOrder.verify(ortbVersionConversionManager).convertToAuctionSupportedVersion(any()); + inOrder.verify(auctionGppService).updateBidRequest(any(), any()); + inOrder.verify(paramsResolver).resolve(any(), any(), any(), eq(true)); + inOrder.verify(cookieDeprecationService).updateBidRequestDevice(any(), any()); + inOrder.verify(ortb2RequestFactory).removeEmptyEids(any(), any()); + inOrder.verify(ortb2RequestFactory).limitImpressions(any(), any(), any()); + inOrder.verify(ortb2RequestFactory).validateRequest(any(), any(), any(), any(), any()); + inOrder.verify(interstitialProcessor).process(any()); + inOrder.verify(auctionPrivacyContextFactory).contextFrom(any()); + inOrder.verify(ortb2RequestFactory).enrichBidRequestWithAccountAndPrivacyData(any()); + inOrder.verify(bidAdjustmentsEnricher).enrichBidRequest(any()); + inOrder.verify(ortb2RequestFactory).executeProcessedAuctionRequestHooks(any()); + inOrder.verify(ortb2RequestFactory).updateTimeout(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void enrichAuctionContextRestoreResultFromRejection() { + // given + given(ortb2RequestFactory.fetchAccount(any())).willReturn(Future.failedFuture("Failed")); + + // when + final Future result = target.enrichAuctionContext(defaultAuctionContext); + + // then + assertThat(result.failed()).isTrue(); + + final InOrder inOrder = inOrder(ortb2RequestFactory); + inOrder.verify(ortb2RequestFactory).fetchAccount(any()); + inOrder.verify(ortb2RequestFactory).restoreResultFromRejection(any()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void enrichAuctionContextShouldAddTempPublisherUsingAccountFromPubId() { + // given + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap().add("pubid", "accountId")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor1 = ArgumentCaptor.forClass(AuctionContext.class); + final ArgumentCaptor captor2 = ArgumentCaptor.forClass(AuctionContext.class); + verify(ortb2RequestFactory).fetchAccount(captor1.capture()); + verify(debugResolver).debugContextFrom(captor2.capture()); + + assertThat(captor1.getValue()) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getSite) + .isEqualTo(Site.builder() + .publisher(Publisher.builder().id("accountId").build()) + .build()); + assertThat(captor2.getValue()) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getSite) + .isNull(); + } + + @Test + public void enrichAuctionContextShouldAddTempPublisherUsingAccountFromAccount() { + // given + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap().add("account", "accountId")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor1 = ArgumentCaptor.forClass(AuctionContext.class); + final ArgumentCaptor captor2 = ArgumentCaptor.forClass(AuctionContext.class); + verify(ortb2RequestFactory).fetchAccount(captor1.capture()); + verify(debugResolver).debugContextFrom(captor2.capture()); + + assertThat(captor1.getValue()) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getSite) + .isEqualTo(Site.builder() + .publisher(Publisher.builder().id("accountId").build()) + .build()); + assertThat(captor2.getValue()) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getSite) + .isNull(); + } + + @Test + public void enrichAuctionContextShouldLimitImpsTo1() { + // given + final AuctionContext auctionContext = givenAuctionContext(givenBidRequest( + givenImp(identity()), + givenImp(identity()))); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getImp) + .extracting(List::size) + .isEqualTo(1); + assertThat(result.result()) + .extracting(AuctionContext::getPrebidErrors) + .asInstanceOf(InstanceOfAssertFactories.list(String.class)) + .containsExactly("Request includes 2 imp elements. Only the first one will remain."); + } + + @Test + public void enrichAuctionContextShouldReadAllExpectedFieldsAndOverrideExistingValuesForImpWithBanner() { + // given + final BidRequest storedBidRequest = givenBidRequest(givenImp(imp -> imp + .banner(Banner.builder() + .format(List.of(Format.builder().w(101).h(102).build())) + .w(103) + .h(104) + .btype(List.of(105)) + .mimes(List.of("storedMimes")) + .battr(List.of(106)) + .pos(107) + .topframe(108) + .expdir(List.of(109)) + .api(List.of(110)) + .ext(storedExt()) + .build()))); + + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(storedBidRequest)); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap() + .add("mimes", "mimes1,mimes2") + .add("battr", "1,2") + .add("api", "3,4") + .add("w", "5") + .add("h", "6") + .add("pos", "7") + .add("btype", "26,27") + .add("topframe", "28") + .add("expdir", "29,30")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + + final BidRequest expectedBidRequest = givenBidRequest(givenImp(imp -> imp + .banner(Banner.builder() + .format(List.of(Format.builder().w(5).h(6).build())) + .w(5) + .h(6) + .btype(List.of(26, 27)) + .mimes(List.of("mimes1", "mimes2")) + .battr(List.of(1, 2)) + .pos(7) + .topframe(28) + .expdir(List.of(29, 30)) + .api(List.of(3, 4)) + .ext(storedExt()) + .build()))); + assertThat(captor.getValue()).isEqualTo(expectedBidRequest); + } + + @Test + public void enrichAuctionContextShouldPreserveValuesFromStoredRequestForImpBanner() { + // given + final BidRequest storedBidRequest = givenBidRequest(givenImp(imp -> imp + .banner(Banner.builder() + .format(List.of(Format.builder().w(101).h(102).build())) + .w(103) + .h(104) + .btype(List.of(105)) + .mimes(List.of("storedMimes")) + .battr(List.of(106)) + .pos(107) + .topframe(108) + .expdir(List.of(109)) + .api(List.of(110)) + .ext(storedExt()) + .build()))); + + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(storedBidRequest)); + + // when + final Future result = target.enrichAuctionContext(defaultAuctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()).isEqualTo(storedBidRequest); + } + + @Test + public void enrichAuctionContextShouldReadAllExpectedFieldsAndOverrideExistingValuesForImpWithVideo() { + // given + final BidRequest storedBidRequest = givenBidRequest(givenImp(imp -> imp + .video(Video.builder() + .w(111) + .h(112) + .mimes(List.of("storedMimes")) + .minduration(113) + .maxduration(114) + .startdelay(115) + .maxseq(116) + .poddur(117) + .protocols(List.of(118)) + .podid(119) + .podseq(120) + .rqddurs(List.of(121)) + .placement(122) + .plcmt(123) + .linearity(124) + .skip(125) + .skipmin(126) + .skipafter(127) + .sequence(128) + .slotinpod(129) + .mincpmpersec(BigDecimal.ONE) + .battr(List.of(130)) + .pos(131) + .maxextended(132) + .minbitrate(133) + .maxbitrate(134) + .boxingallowed(135) + .playbackmethod(List.of(136)) + .playbackend(137) + .delivery(List.of(138)) + .api(List.of(139)) + .ext(storedExt()) + .build()))); + + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(storedBidRequest)); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap() + .add("mimes", "mimes1,mimes2") + .add("battr", "1,2") + .add("api", "3,4") + .add("w", "5") + .add("h", "6") + .add("pos", "7") + .add("mindur", "8") + .add("maxdur", "9") + .add("startdelay", "10") + .add("maxseq", "11") + .add("poddur", "12") + .add("proto", "13,14") + .add("podid", "15") + .add("podseq", "16") + .add("rqddurs", "17,18") + .add("seq", "19") + .add("slotinpod", "20") + .add("mincpms", "21") + .add("maxex", "22") + .add("minbr", "23") + .add("maxbr", "24") + .add("delivery", "25") + .add("placement", "31") + .add("plcmt", "32") + .add("linearity", "33") + .add("skip", "34") + .add("skipmin", "35") + .add("skipafter", "36") + .add("boxingallowed", "37") + .add("playbackmethod", "38,39") + .add("playbackend", "40")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + + final BidRequest expectedBidRequest = givenBidRequest(givenImp(imp -> imp + .video(Video.builder() + .w(5) + .h(6) + .mimes(List.of("mimes1", "mimes2")) + .minduration(8) + .maxduration(9) + .startdelay(10) + .maxseq(11) + .poddur(12) + .protocols(List.of(13, 14)) + .podid(15) + .podseq(16) + .rqddurs(List.of(17, 18)) + .placement(31) + .plcmt(32) + .linearity(33) + .skip(34) + .skipmin(35) + .skipafter(36) + .sequence(19) + .slotinpod(20) + .mincpmpersec(new BigDecimal("21")) + .battr(List.of(1, 2)) + .pos(7) + .maxextended(22) + .minbitrate(23) + .maxbitrate(24) + .boxingallowed(37) + .playbackmethod(List.of(38, 39)) + .playbackend(40) + .delivery(List.of(25)) + .api(List.of(3, 4)) + .ext(storedExt()) + .build()))); + assertThat(captor.getValue()).isEqualTo(expectedBidRequest); + } + + @Test + public void enrichAuctionContextShouldPreserveValuesFromStoredRequestForImpVideo() { + // given + final BidRequest storedBidRequest = givenBidRequest(givenImp(imp -> imp + .video(Video.builder() + .w(111) + .h(112) + .mimes(List.of("storedMimes")) + .minduration(113) + .maxduration(114) + .startdelay(115) + .maxseq(116) + .poddur(117) + .protocols(List.of(118)) + .podid(119) + .podseq(120) + .rqddurs(List.of(121)) + .placement(122) + .plcmt(123) + .linearity(124) + .skip(125) + .skipmin(126) + .skipafter(127) + .sequence(128) + .slotinpod(129) + .mincpmpersec(BigDecimal.ONE) + .battr(List.of(130)) + .pos(131) + .maxextended(132) + .minbitrate(133) + .maxbitrate(134) + .boxingallowed(135) + .playbackmethod(List.of(136)) + .playbackend(137) + .delivery(List.of(138)) + .api(List.of(139)) + .ext(storedExt()) + .build()))); + + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(storedBidRequest)); + + // when + final Future result = target.enrichAuctionContext(defaultAuctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()).isEqualTo(storedBidRequest); + } + + @Test + public void enrichAuctionContextShouldReadAllExpectedFieldsAndOverrideExistingValuesForImpWithAudio() { + // given + final BidRequest storedBidRequest = givenBidRequest(givenImp(imp -> imp + .audio(Audio.builder() + .mimes(List.of("storedMimes")) + .minduration(140) + .maxduration(141) + .startdelay(142) + .maxseq(143) + .poddur(144) + .protocols(List.of(145)) + .podid(146) + .podseq(147) + .rqddurs(List.of(148)) + .sequence(149) + .slotinpod(150) + .mincpmpersec(BigDecimal.ONE) + .battr(List.of(151)) + .maxextended(152) + .minbitrate(153) + .maxbitrate(154) + .delivery(List.of(155)) + .api(List.of(156)) + .feed(157) + .stitched(158) + .nvol(159) + .ext(storedExt()) + .build()))); + + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(storedBidRequest)); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap() + .add("mimes", "mimes1,mimes2") + .add("battr", "1,2") + .add("api", "3,4") + .add("mindur", "8") + .add("maxdur", "9") + .add("startdelay", "10") + .add("maxseq", "11") + .add("poddur", "12") + .add("proto", "13,14") + .add("podid", "15") + .add("podseq", "16") + .add("rqddurs", "17,18") + .add("seq", "19") + .add("slotinpod", "20") + .add("mincpms", "21") + .add("maxex", "22") + .add("minbr", "23") + .add("maxbr", "24") + .add("delivery", "25") + .add("feed", "41") + .add("stitched", "42") + .add("nvol", "43")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + + final BidRequest expectedBidRequest = givenBidRequest(givenImp(imp -> imp + .audio(Audio.builder() + .mimes(List.of("mimes1", "mimes2")) + .minduration(8) + .maxduration(9) + .startdelay(10) + .maxseq(11) + .poddur(12) + .protocols(List.of(13, 14)) + .podid(15) + .podseq(16) + .rqddurs(List.of(17, 18)) + .sequence(19) + .slotinpod(20) + .mincpmpersec(new BigDecimal("21")) + .battr(List.of(1, 2)) + .maxextended(22) + .minbitrate(23) + .maxbitrate(24) + .delivery(List.of(25)) + .api(List.of(3, 4)) + .feed(41) + .stitched(42) + .nvol(43) + .ext(storedExt()) + .build()))); + assertThat(captor.getValue()).isEqualTo(expectedBidRequest); + } + + @Test + public void enrichAuctionContextShouldPreserveValuesFromStoredRequestForImpAudio() { + // given + final BidRequest storedBidRequest = givenBidRequest(givenImp(imp -> imp + .audio(Audio.builder() + .mimes(List.of("storedMimes")) + .minduration(140) + .maxduration(141) + .startdelay(142) + .maxseq(143) + .poddur(144) + .protocols(List.of(145)) + .podid(146) + .podseq(147) + .rqddurs(List.of(148)) + .sequence(149) + .slotinpod(150) + .mincpmpersec(BigDecimal.ONE) + .battr(List.of(151)) + .maxextended(152) + .minbitrate(153) + .maxbitrate(154) + .delivery(List.of(155)) + .api(List.of(156)) + .feed(157) + .stitched(158) + .nvol(159) + .ext(storedExt()) + .build()))); + + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(storedBidRequest)); + + // when + final Future result = target.enrichAuctionContext(defaultAuctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()).isEqualTo(storedBidRequest); + } + + @Test + public void enrichAuctionContextShouldReadAllExpectedFieldsAndOverrideExistingValuesForImp() { + // given + final BidRequest storedBidRequest = givenBidRequest(givenImp(imp -> imp + .tagid("storedTagId") + .ext(storedExt()))); + + given(fpdResolver.resolveImpExt(any(), any())) + .willAnswer(invocation -> invocation.getArgument(1)); + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(storedBidRequest)); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap() + .add("slot", "slot") + .add("targeting", "{\"field\":\"value\"}") + .add("iprof", "impProfile1,impProfile2")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + verify(paramsExtractor).refererFrom(any()); + verify(ortbTypesResolver).normalizeTargeting(any(), any(), any()); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + + final BidRequest expectedBidRequest = givenBidRequest(givenImp(imp -> imp + .tagid("slot") + .ext(mapper.valueToTree(Map.of( + "field", "value", + "prebid", Map.of("profiles", List.of("impProfile1", "impProfile2"))))))); + assertThat(captor.getValue()).isEqualTo(expectedBidRequest); + } + + @Test + public void enrichAuctionContextShouldPreserveValuesFromStoredRequestForImp() { + // given + final BidRequest storedBidRequest = givenBidRequest(givenImp(imp -> imp + .tagid("storedTagId") + .ext(storedExt()))); + + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(storedBidRequest)); + + // when + final Future result = target.enrichAuctionContext(defaultAuctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()).isEqualTo(storedBidRequest); + } + + @Test + public void enrichAuctionContextShouldReadAllExpectedFieldsAndOverrideExistingValuesForChannels() { + // given + final Publisher storedPublisher = Publisher.builder() + .id(ACCOUNT_ID) + .name("publisher") + .build(); + + final Content storedContent = Content.builder() + .id("contentId") + .title("storedTitle") + .series("storedSeries") + .genre("storedGenre") + .url("storedUrl") + .cattax(160) + .cat(List.of("storedCat")) + .contentrating("storedContentRating") + .livestream(161) + .language("storedLanguage") + .build(); + + final BidRequest storedBidRequest = givenBidRequest(request -> request + .site(Site.builder() + .id("siteId") + .page("storedPage") + .publisher(storedPublisher) + .content(storedContent) + .build()) + .app(App.builder() + .id("appId") + .name("storedName") + .bundle("storedBundle") + .storeurl("storedStoreUrl") + .publisher(storedPublisher) + .content(storedContent) + .build()) + .dooh(Dooh.builder() + .id("doohId") + .publisher(storedPublisher) + .content(storedContent) + .build())); + + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(storedBidRequest)); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap() + // content + .add("ctitle", "contentTitle") + .add("cseries", "contentSeries") + .add("cgenre", "contentGenre") + .add("curl", "contentUrl") + .add("ccattax", "44") + .add("ccat", "contentCat1,contentCat2") + .add("crating", "contentRating") + .add("clivestream", "45") + .add("clang", "contentLanguage") + + // site + .add("page", "page") + + // app + .add("name", "name") + .add("bundle", "bundle") + .add("storeurl", "storeUrl")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + + final Content expectedContent = Content.builder() + .id("contentId") + .title("contentTitle") + .series("contentSeries") + .genre("contentGenre") + .url("contentUrl") + .cattax(44) + .cat(List.of("contentCat1", "contentCat2")) + .contentrating("contentRating") + .livestream(45) + .language("contentLanguage") + .build(); + + final BidRequest expectedBidRequest = givenBidRequest(request -> request + .site(Site.builder() + .id("siteId") + .page("page") + .publisher(storedPublisher) + .content(expectedContent) + .build()) + .app(App.builder() + .id("appId") + .name("name") + .bundle("bundle") + .storeurl("storeUrl") + .publisher(storedPublisher) + .content(expectedContent) + .build()) + .dooh(Dooh.builder() + .id("doohId") + .publisher(storedPublisher) + .content(expectedContent) + .build())); + assertThat(captor.getValue()).isEqualTo(expectedBidRequest); + } + + @Test + public void enrichAuctionContextShouldPreserveValuesFromStoredRequestForChannels() { + // given + final Publisher storedPublisher = Publisher.builder() + .id(ACCOUNT_ID) + .name("publisher") + .build(); + + final Content storedContent = Content.builder() + .id("contentId") + .title("storedTitle") + .series("storedSeries") + .genre("storedGenre") + .url("storedUrl") + .cattax(160) + .cat(List.of("storedCat")) + .contentrating("storedContentRating") + .livestream(161) + .language("storedLanguage") + .build(); + + final BidRequest storedBidRequest = givenBidRequest(request -> request + .site(Site.builder() + .id("siteId") + .page("storedPage") + .publisher(storedPublisher) + .content(storedContent) + .build()) + .app(App.builder() + .id("appId") + .name("storedName") + .bundle("storedBundle") + .storeurl("storedStoreUrl") + .publisher(storedPublisher) + .content(storedContent) + .build()) + .dooh(Dooh.builder() + .id("doohId") + .publisher(storedPublisher) + .content(storedContent) + .build())); + + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(storedBidRequest)); + + // when + final Future result = target.enrichAuctionContext(defaultAuctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()).isEqualTo(storedBidRequest); + } + + @Test + public void enrichAuctionContextShouldReadFormatProperlyFromSizes() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())).willReturn( + Future.succeededFuture(givenBidRequest(givenImp(imp -> imp.banner(Banner.builder().build()))))); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap() + .add("ow", "1") + .add("w", "10") + .add("h", "2") + .add("sizes", "3x4,5x6")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()) + .extracting(BidRequest::getImp) + .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) + .extracting(Imp::getBanner) + .flatExtracting(Banner::getFormat) + .containsExactly( + Format.builder().w(1).h(2).build(), + Format.builder().w(3).h(4).build(), + Format.builder().w(5).h(6).build()); + } + + @Test + public void enrichAuctionContextShouldReadFormatProperlyFromMs() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())).willReturn(Future.succeededFuture( + givenBidRequest(givenImp(imp -> imp.banner(Banner.builder().build()))))); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap() + .add("w", "1") + .add("oh", "2") + .add("h", "20") + .add("ms", "3x4,5x6")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()) + .extracting(BidRequest::getImp) + .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) + .extracting(Imp::getBanner) + .flatExtracting(Banner::getFormat) + .containsExactly( + Format.builder().w(1).h(2).build(), + Format.builder().w(3).h(4).build(), + Format.builder().w(5).h(6).build()); + } + + @Test + public void enrichAuctionContextShouldReadWFromOw() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())).willReturn(Future.succeededFuture( + givenBidRequest(givenImp(imp -> imp.banner(Banner.builder().build()))))); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap() + .add("ow", "1") + .add("w", "10")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()) + .extracting(BidRequest::getImp) + .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) + .extracting(Imp::getBanner) + .extracting(Banner::getW) + .containsExactly(1); + } + + @Test + public void enrichAuctionContextShouldReadHFromOh() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())).willReturn( + Future.succeededFuture(givenBidRequest(givenImp(imp -> imp.banner(Banner.builder().build()))))); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap() + .add("oh", "1") + .add("h", "10")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()) + .extracting(BidRequest::getImp) + .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) + .extracting(Imp::getBanner) + .extracting(Banner::getH) + .containsExactly(1); + } + + @Test + public void enrichAuctionContextShouldUseExistentPrebidNodeForImpProfiles() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(givenBidRequest( + givenImp(imp -> imp.ext(mapper.valueToTree(Map.of("prebid", Map.of("field", "value")))))))); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap().add("iprof", "impProfile1,impProfile2")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()) + .extracting(BidRequest::getImp) + .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) + .extracting(Imp::getExt) + .containsExactly(mapper.valueToTree(Map.of( + "prebid", Map.of( + "field", "value", + "profiles", List.of("impProfile1", "impProfile2"))))); + } + + @Test + public void enrichAuctionContextShouldCreatePrebidNodeForImpProfiles() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(givenBidRequest(givenImp(identity())))); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap().add("iprof", "impProfile1,impProfile2")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()) + .extracting(BidRequest::getImp) + .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) + .extracting(Imp::getExt) + .containsExactly(mapper.valueToTree(Map.of( + "prebid", Map.of("profiles", List.of("impProfile1", "impProfile2"))))); + } + + @Test + public void enrichAuctionContextShouldOverridePublisherId() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(givenBidRequest(request -> request.site( + Site.builder().publisher(Publisher.builder().id("id").build()).build())))); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap().add("ctitle", "contentTitle")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .extracting(Publisher::getId) + .isEqualTo(ACCOUNT_ID); + } + + @Test + public void enrichAuctionContextShouldCreateContent() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(givenBidRequest(request -> request.site(Site.builder().build())))); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap().add("ctitle", "contentTitle")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()) + .extracting(BidRequest::getSite) + .extracting(Site::getContent) + .isNotNull(); + } + + @Test + public void enrichAuctionContextShouldUseRssFeed() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(givenBidRequest(request -> request.site(Site.builder().build())))); + + final AuctionContext auctionContext = givenAuctionContext( + MultiMap.caseInsensitiveMultiMap().add("rss_feed", "rssFeed")); + + // when + final Future result = target.enrichAuctionContext(auctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(profilesProcessor).process(any(), captor.capture()); + assertThat(captor.getValue()) + .extracting(BidRequest::getSite) + .extracting(Site::getContent) + .extracting(Content::getSeries) + .isEqualTo("rssFeed"); + } + + @Test + public void enrichAuctionContextShouldDetermineWebChannel() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(givenBidRequest(request -> request.site(Site.builder().build())))); + + // when + final Future result = target.enrichAuctionContext(defaultAuctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()) + .extracting(AuctionContext::getRequestTypeMetric) + .isEqualTo(MetricName.openrtb2web); + } + + @Test + public void enrichAuctionContextShouldDetermineAppChannel() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(givenBidRequest(request -> request.app(App.builder().build())))); + + // when + final Future result = target.enrichAuctionContext(defaultAuctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()) + .extracting(AuctionContext::getRequestTypeMetric) + .isEqualTo(MetricName.openrtb2app); + } + + @Test + public void enrichAuctionContextShouldDetermineDoohChannel() { + // given + given(storedRequestProcessor.processAmpRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(givenBidRequest(request -> request.dooh(Dooh.builder().build())))); + + // when + final Future result = target.enrichAuctionContext(defaultAuctionContext); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()) + .extracting(AuctionContext::getRequestTypeMetric) + .isEqualTo(MetricName.openrtb2dooh); + } + + private static Future toHttpRequest(RoutingContext routingContext, String body) { + return Future.succeededFuture(HttpRequestContext.builder() + .absoluteUri(routingContext.request().absoluteURI()) + .queryParams(toCaseInsensitiveMultiMap(routingContext.queryParams())) + .headers(toCaseInsensitiveMultiMap(routingContext.request().headers())) + .body(body) + .scheme(routingContext.request().scheme()) + .remoteHost(routingContext.request().remoteAddress().host()) + .build()); + } + + private static CaseInsensitiveMultiMap toCaseInsensitiveMultiMap(MultiMap originalMap) { + final CaseInsensitiveMultiMap.Builder mapBuilder = CaseInsensitiveMultiMap.builder(); + originalMap.entries().forEach(entry -> mapBuilder.add(entry.getKey(), entry.getValue())); + + return mapBuilder.build(); + } + + private void givenQueryParams(MultiMap params) { + given(routingContext.queryParams()).willReturn(params); + } + + private AuctionContext givenAuctionContext(MultiMap params) { + final HttpRequestContext initialHttpRequestContext = defaultAuctionContext.getHttpRequest(); + return defaultAuctionContext.toBuilder() + .httpRequest(HttpRequestContext.builder() + .absoluteUri(initialHttpRequestContext.getAbsoluteUri()) + .queryParams(toCaseInsensitiveMultiMap(params)) + .headers(initialHttpRequestContext.getHeaders()) + .body(initialHttpRequestContext.getBody()) + .scheme(initialHttpRequestContext.getScheme()) + .remoteHost(initialHttpRequestContext.getRemoteHost()) + .build()) + .build(); + } + + private AuctionContext givenAuctionContext(BidRequest bidRequest) { + return defaultAuctionContext.with(bidRequest); + } + + private BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { + return bidRequestCustomizer.apply(defaultBidRequest.toBuilder()).build(); + } + + private BidRequest givenBidRequest(Imp... imps) { + return givenBidRequest(request -> request.imp(List.of(imps))); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder()).build(); + } + + private static ObjectNode storedExt() { + return mapper.createObjectNode().put("storedField", "storedValue"); + } +} diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java index 41fa98842e7..b280c7dfde1 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java @@ -730,7 +730,7 @@ public void shouldSetDeviceLmtOneForIos14Minor2AndAtts0() { .device(Device.builder() .os("iOS") .osv("14.2") - .ext(ExtDevice.of(0, null)) + .ext(ExtDevice.of(0, null, null)) .build()) .build(); @@ -750,7 +750,7 @@ public void shouldOverrideDeviceLmtToOneForIos14Minor2AndAtts0() { .lmt(0) .os("iOS") .osv("14.2") - .ext(ExtDevice.of(0, null)) + .ext(ExtDevice.of(0, null, null)) .build()) .build(); @@ -769,7 +769,7 @@ public void shouldSetDeviceLmtOneForIos14Minor2AndAtts1() { .device(Device.builder() .os("iOS") .osv("14.2") - .ext(ExtDevice.of(1, null)) + .ext(ExtDevice.of(1, null, null)) .build()) .build(); @@ -789,7 +789,7 @@ public void shouldOverrideDeviceLmtToOneForIos14Minor2AndAtts1() { .lmt(0) .os("iOS") .osv("14.2") - .ext(ExtDevice.of(1, null)) + .ext(ExtDevice.of(1, null, null)) .build()) .build(); @@ -808,7 +808,7 @@ public void shouldSetDeviceLmtOneForIos15Minor0AndAtts0() { .device(Device.builder() .os("iOS") .osv("15.0") - .ext(ExtDevice.of(0, null)) + .ext(ExtDevice.of(0, null, null)) .build()) .build(); @@ -827,7 +827,7 @@ public void shouldSetDeviceLmtOneForIos15Minor0AndAtts1() { .device(Device.builder() .os("iOS") .osv("15.0") - .ext(ExtDevice.of(1, null)) + .ext(ExtDevice.of(1, null, null)) .build()) .build(); @@ -846,7 +846,7 @@ public void shouldSetDeviceLmtOneForIos15Minor0AndAtts2() { .device(Device.builder() .os("iOS") .osv("15.0") - .ext(ExtDevice.of(2, null)) + .ext(ExtDevice.of(2, null, null)) .build()) .build(); @@ -866,7 +866,7 @@ public void shouldOverrideDeviceLmtToOneForIos14Minor2AndAtts2() { .lmt(0) .os("iOS") .osv("14.2") - .ext(ExtDevice.of(2, null)) + .ext(ExtDevice.of(2, null, null)) .build()) .build(); @@ -885,7 +885,7 @@ public void shouldSetDeviceLmtZeroForIos14Minor2AndAtts3() { .device(Device.builder() .os("iOS") .osv("14.2") - .ext(ExtDevice.of(3, null)) + .ext(ExtDevice.of(3, null, null)) .build()) .build(); @@ -905,7 +905,7 @@ public void shouldOverrideDeviceLmtToZeroForIos14Minor2AndAtts3() { .lmt(1) .os("iOS") .osv("14.2") - .ext(ExtDevice.of(3, null)) + .ext(ExtDevice.of(3, null, null)) .build()) .build(); @@ -924,7 +924,7 @@ public void shouldNotSetDeviceLmtForIos14Minor3AndAtts4() { .device(Device.builder() .os("iOS") .osv("14.3") - .ext(ExtDevice.of(4, null)) + .ext(ExtDevice.of(4, null, null)) .build()) .build(); @@ -943,7 +943,7 @@ public void shouldNotSetDeviceLmtForIos14Minor3AndAttsNull() { .device(Device.builder() .os("iOS") .osv("14.3") - .ext(ExtDevice.of(null, null)) + .ext(ExtDevice.of(null, null, null)) .build()) .build(); diff --git a/src/test/java/org/prebid/server/cookie/CookieDeprecationServiceTest.java b/src/test/java/org/prebid/server/cookie/CookieDeprecationServiceTest.java index 111d4f441f5..e05daba9f8f 100644 --- a/src/test/java/org/prebid/server/cookie/CookieDeprecationServiceTest.java +++ b/src/test/java/org/prebid/server/cookie/CookieDeprecationServiceTest.java @@ -303,7 +303,7 @@ public void updateBidRequestDeviceShouldAddCdepValueWhenDeviceExtIsPresentButWit final List debugWarnings = new ArrayList<>(); final Account givenAccount = givenAccount(true, 100L); final AuctionContext auctionContext = givenContext(headers, givenAccount, debugWarnings); - final ExtDevice extDevice = ExtDevice.of(1, ExtDevicePrebid.of(ExtDeviceInt.of(2, 3))); + final ExtDevice extDevice = ExtDevice.of(1, null, ExtDevicePrebid.of(ExtDeviceInt.of(2, 3))); extDevice.addProperty("some_property", TextNode.valueOf("some_property_value")); final BidRequest bidRequest = givenBidRequest(builder -> builder.ext(extDevice).ip("ip")); @@ -311,7 +311,7 @@ public void updateBidRequestDeviceShouldAddCdepValueWhenDeviceExtIsPresentButWit final BidRequest actualBidRequest = target.updateBidRequestDevice(bidRequest, auctionContext); // then - final ExtDevice expectedExtDevice = ExtDevice.of(1, ExtDevicePrebid.of(ExtDeviceInt.of(2, 3))); + final ExtDevice expectedExtDevice = ExtDevice.of(1, null, ExtDevicePrebid.of(ExtDeviceInt.of(2, 3))); expectedExtDevice.addProperty("cdep", TextNode.valueOf(cdep)); expectedExtDevice.addProperty("some_property", TextNode.valueOf("some_property_value")); diff --git a/src/test/java/org/prebid/server/handler/HttpInteractionLogHandlerTest.java b/src/test/java/org/prebid/server/handler/HttpInteractionLogHandlerTest.java index 5309745e9f0..fa5e4def3fe 100644 --- a/src/test/java/org/prebid/server/handler/HttpInteractionLogHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/HttpInteractionLogHandlerTest.java @@ -86,7 +86,8 @@ public void shouldRespondWithErrorWhenEndpointNotValid() { // then verify(httpResponse).setStatusCode(eq(400)); - verify(httpResponse).end(eq("Invalid 'endpoint' parameter value, allowed values '[auction, amp]'")); + verify(httpResponse).end( + eq("Invalid 'endpoint' parameter value, allowed values '[auction, amp, get_interface]'")); verifyNoInteractions(httpInteractionLogger); } diff --git a/src/test/java/org/prebid/server/handler/openrtb2/GetInterfaceHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/GetInterfaceHandlerTest.java new file mode 100644 index 00000000000..afc8ff67d46 --- /dev/null +++ b/src/test/java/org/prebid/server/handler/openrtb2/GetInterfaceHandlerTest.java @@ -0,0 +1,1273 @@ +package org.prebid.server.handler.openrtb2; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HooksMetricsService; +import org.prebid.server.auction.SkippedAuctionService; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.TimeoutContext; +import org.prebid.server.auction.model.debug.DebugContext; +import org.prebid.server.auction.requestfactory.GetInterfaceRequestFactory; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.exception.BlocklistedAccountException; +import org.prebid.server.exception.BlocklistedAppException; +import org.prebid.server.exception.InvalidAccountConfigException; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.exception.UnauthorizedAccountException; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; +import org.prebid.server.log.HttpInteractionLogger; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.Endpoint; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; +import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; +import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; +import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +public class GetInterfaceHandlerTest extends VertxTest { + + @Mock + private GetInterfaceRequestFactory getInterfaceRequestFactory; + @Mock + private ExchangeService exchangeService; + @Mock(strictness = LENIENT) + private SkippedAuctionService skippedAuctionService; + @Mock + private AnalyticsReporterDelegator analyticsReporterDelegator; + @Mock + private Metrics metrics; + @Mock + private Clock clock; + @Mock + private HttpInteractionLogger httpInteractionLogger; + @Mock + private PrebidVersionProvider prebidVersionProvider; + @Mock(strictness = LENIENT) + private HooksMetricsService hooksMetricsService; + @Mock(strictness = LENIENT) + private HookStageExecutor hookStageExecutor; + + private GetInterfaceHandler target; + @Mock + private RoutingContext routingContext; + @Mock + private HttpServerRequest httpRequest; + @Mock(strictness = LENIENT) + private HttpServerResponse httpResponse; + @Mock + private UidsCookie uidsCookie; + + private Timeout timeout; + + @BeforeEach + public void setUp() { + given(routingContext.request()).willReturn(httpRequest); + given(routingContext.response()).willReturn(httpResponse); + + given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap()); + given(httpRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap()); + + given(httpResponse.exceptionHandler(any())).willReturn(httpResponse); + given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + given(httpResponse.headers()).willReturn(MultiMap.caseInsensitiveMultiMap()); + + given(skippedAuctionService.skipAuction(any())) + .willReturn(Future.failedFuture("Auction cannot be skipped")); + + given(clock.millis()).willReturn(Instant.now().toEpochMilli()); + + given(prebidVersionProvider.getNameVersionRecord()).willReturn("pbs-java/1.00"); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1))))); + + given(hooksMetricsService.updateHooksMetrics(any())).willAnswer(invocation -> invocation.getArgument(0)); + + timeout = new TimeoutFactory(clock).create(2000L); + + target = new GetInterfaceHandler( + 0.01, + getInterfaceRequestFactory, + exchangeService, + skippedAuctionService, + analyticsReporterDelegator, + metrics, + hooksMetricsService, + clock, + httpInteractionLogger, + prebidVersionProvider, + hookStageExecutor, + jacksonMapper); + } + + @Test + public void shouldSetRequestTypeMetricToAuctionContext() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionContext auctionContext = captureAuctionContext(); + assertThat(auctionContext.getRequestTypeMetric()).isNotNull(); + } + + @Test + public void shouldUseTimeoutFromAuctionContext() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + assertThat(captureAuctionContext()) + .extracting(AuctionContext::getTimeoutContext) + .extracting(TimeoutContext::getTimeout) + .extracting(Timeout::remaining) + .isEqualTo(2000L); + } + + @Test + public void shouldAddPrebidVersionResponseHeader() { + // given + given(prebidVersionProvider.getNameVersionRecord()).willReturn("pbs-java/1.00"); + + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + given(exchangeService.holdAuction(any())) + .willAnswer(inv -> Future.succeededFuture(((AuctionContext) inv.getArgument(0)).toBuilder() + .bidResponse(BidResponse.builder().build()) + .build())); + + // when + target.handle(routingContext); + + // then + assertThat(httpResponse.headers()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .contains(tuple("x-prebid", "pbs-java/1.00")); + } + + @Test + public void shouldAddObserveBrowsingTopicsResponseHeader() { + // given + httpRequest.headers().add(HttpUtil.SEC_BROWSING_TOPICS_HEADER, ""); + + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + given(exchangeService.holdAuction(any())) + .willAnswer(inv -> Future.succeededFuture(((AuctionContext) inv.getArgument(0)).toBuilder() + .bidResponse(BidResponse.builder().build()) + .build())); + + // when + target.handle(routingContext); + + // then + assertThat(httpResponse.headers()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .contains(tuple("Observe-Browsing-Topics", "?1")); + } + + @Test + public void shouldComputeTimeoutBasedOnRequestProcessingStartTime() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + givenHoldAuction(BidResponse.builder().build()); + + final Instant now = Instant.now(); + given(clock.millis()).willReturn(now.toEpochMilli()).willReturn(now.plusMillis(50L).toEpochMilli()); + + // when + target.handle(routingContext); + + // then + assertThat(captureAuctionContext()) + .extracting(AuctionContext::getTimeoutContext) + .extracting(TimeoutContext::getTimeout) + .extracting(Timeout::remaining) + .asInstanceOf(InstanceOfAssertFactories.LONG) + .isLessThanOrEqualTo(1950L); + } + + @Test + public void shouldRespondWithServiceUnavailableIfBidRequestHasAccountBlocklisted() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willReturn(Future.failedFuture(new BlocklistedAccountException("Blocklisted account"))); + + // when + target.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(eq(403)); + verify(httpResponse).end(eq("Blocklisted: Blocklisted account")); + + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.blocklisted_account)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); + } + + @Test + public void shouldRespondWithBadRequestIfBidRequestHasAccountWithInvalidConfig() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willReturn(Future.failedFuture(new InvalidAccountConfigException("Invalid config"))); + + // when + target.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(eq(400)); + verify(httpResponse).end(eq("Invalid config")); + + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.bad_requests)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); + } + + @Test + public void shouldRespondWithServiceUnavailableIfBidRequestHasAppBlocklisted() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willReturn(Future.failedFuture(new BlocklistedAppException("Blocklisted app"))); + + // when + target.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(eq(403)); + verify(httpResponse).end(eq("Blocklisted: Blocklisted app")); + + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.blocklisted_app)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); + } + + @Test + public void shouldRespondWithBadRequestIfBidRequestIsInvalid() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); + + // when + target.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(eq(400)); + verify(httpResponse).end(eq("Invalid request format: Request is invalid")); + + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.badinput)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); + } + + @Test + public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willReturn(Future.failedFuture(new UnauthorizedAccountException("Account id is not provided", null))); + + // when + target.handle(routingContext); + + // then + verifyNoInteractions(exchangeService); + verify(httpResponse).setStatusCode(eq(401)); + verify(httpResponse).end(eq("Account id is not provided")); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); + } + + @Test + public void shouldRespondWithInternalServerErrorIfAuctionFails() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + given(exchangeService.holdAuction(any())) + .willThrow(new RuntimeException("Unexpected exception")); + + // when + target.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(eq(500)); + verify(httpResponse).end(eq("Critical error while running the auction: Unexpected exception")); + + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.err)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); + } + + @Test + public void shouldNotSendResponseIfClientClosedConnection() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willReturn(Future.failedFuture(new RuntimeException())); + + given(routingContext.response().closed()).willReturn(true); + + // when + target.handle(routingContext); + + // then + verify(httpResponse, never()).end(anyString()); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); + } + + @Test + public void shouldRespondWithBidResponse() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + given(exchangeService.holdAuction(any())) + .willReturn(Future.succeededFuture(auctionContext.with(BidResponse.builder().build()))); + + // when + target.handle(routingContext); + + // then + verify(exchangeService).holdAuction(any()); + assertThat(httpResponse.headers()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(httpResponse).end(eq("{}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldRespondWithBidResponseWhenExitpointChangesHeadersAndResponse() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + given(exchangeService.holdAuction(any())) + .willReturn(Future.succeededFuture(auctionContext.with(BidResponse.builder().build()))); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"response\":{}}")))); + + // when + target.handle(routingContext); + + // then + verify(exchangeService).holdAuction(any()); + assertThat(httpResponse.headers()).hasSize(1) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder(tuple("New-Header", "New-Header-Value")); + + verify(httpResponse).end(eq("{\"response\":{}}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldRespondWithCorrectResolvedRequestMediaTypePriceGranularity() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + final ExtGranularityRange granularityRange = ExtGranularityRange.of(BigDecimal.TEN, BigDecimal.ONE); + final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList(granularityRange)); + final ExtMediaTypePriceGranularity priceGranuality = ExtMediaTypePriceGranularity.of( + mapper.valueToTree(priceGranularity), null, mapper.createObjectNode()); + final BidRequest resolvedRequest = BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder().mediatypepricegranularity(priceGranuality).build()) + .auctiontimestamp(0L) + .build())) + .build(); + + final BidResponse bidResponse = BidResponse.builder() + .ext(ExtBidResponse.builder() + .debug(ExtResponseDebug.of(null, resolvedRequest, null)) + .build()) + .build(); + given(exchangeService.holdAuction(any())) + .willReturn(Future.succeededFuture(auctionContext.with(bidResponse))); + + // when + target.handle(routingContext); + + // then + verify(exchangeService).holdAuction(any()); + verify(httpResponse).end(eq("{\"ext\":{\"debug\":{\"resolvedrequest\":{\"ext\":{\"prebid\":" + + "{\"targeting\":{\"mediatypepricegranularity\":{\"banner\":{\"precision\":1,\"ranges\":" + + "[{\"max\":10,\"increment\":1}]},\"native\":{}}},\"auctiontimestamp\":0}}}}}}")); + + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"ext\":{\"debug\":{\"resolvedrequest\":{\"ext\":{\"prebid\":" + + "{\"targeting\":{\"mediatypepricegranularity\":{\"banner\":{\"precision\":1,\"ranges\":" + + "[{\"max\":10,\"increment\":1}]},\"native\":{}}},\"auctiontimestamp\":0}}}}}}"), + any()); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldIncrementOkOpenrtb2WebRequestMetrics() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.ok)); + } + + @Test + public void shouldIncrementOkOpenrtb2AppRequestMetrics() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willReturn(Future.succeededFuture( + givenAuctionContext(identity(), builder -> builder.requestTypeMetric(MetricName.openrtb2app)))); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2app), eq(MetricName.ok)); + } + + @Test + public void shouldIncrementAppRequestMetrics() { + // given + givenHoldAuction(BidResponse.builder().build()); + + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willReturn(Future.succeededFuture(givenAuctionContext(builder -> builder.app(App.builder().build())))); + + // when + target.handle(routingContext); + + // then + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyInt()); + } + + @Test + public void shouldIncrementNoCookieMetrics() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + givenHoldAuction(BidResponse.builder().build()); + + given(uidsCookie.hasLiveUids()).willReturn(false); + + httpRequest.headers().add(HttpUtil.USER_AGENT_HEADER, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) " + + "AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7"); + + // when + target.handle(routingContext); + + // then + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt()); + } + + @Test + public void shouldIncrementImpsRequestedMetrics() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willReturn(Future.succeededFuture( + givenAuctionContext(builder -> builder.imp(singletonList(Imp.builder().build()))))); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), eq(1)); + } + + @Test + public void shouldIncrementImpTypesMetrics() { + // given + final List imps = singletonList(Imp.builder().build()); + + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willReturn(Future.succeededFuture(givenAuctionContext(builder -> builder.imp(imps)))); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + verify(metrics).updateImpTypesMetrics(same(imps)); + } + + @Test + public void shouldIncrementBadinputOnParsingRequestOpenrtb2WebRequestMetrics() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); + + // when + target.handle(routingContext); + + // then + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.badinput)); + } + + @Test + public void shouldIncrementErrOpenrtb2WebRequestMetrics() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.failedFuture(new RuntimeException())); + + // when + target.handle(routingContext); + + // then + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.err)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldUpdateRequestTimeMetric() { + // given + // set up clock mock to check that request_time metric has been updated with expected value + given(clock.millis()).willReturn(5000L).willReturn(5500L); + + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + givenHoldAuction(BidResponse.builder().build()); + + // simulate calling end handler that is supposed to update request_time timer value + given(httpResponse.endHandler(any())).willAnswer(inv -> { + ((Handler) inv.getArgument(0)).handle(null); + return null; + }); + + // when + target.handle(routingContext); + + // then + verify(metrics).updateRequestTimeMetric(eq(MetricName.request_time), eq(500L)); + } + + @Test + public void shouldNotUpdateRequestTimeMetricIfRequestFails() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); + + // when + target.handle(routingContext); + + // then + verify(httpResponse, never()).endHandler(any()); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldUpdateNetworkErrorMetric() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + givenHoldAuction(BidResponse.builder().build()); + + // simulate calling exception handler that is supposed to update networkerr timer value + given(httpResponse.exceptionHandler(any())).willAnswer(inv -> { + ((Handler) inv.getArgument(0)).handle(new RuntimeException()); + return httpResponse; + }); + + // when + target.handle(routingContext); + + // then + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr)); + } + + @Test + public void shouldNotUpdateNetworkErrorMetricIfResponseSucceeded() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + verify(metrics, never()).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr)); + } + + @Test + public void shouldUpdateNetworkErrorMetricIfClientClosedConnection() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + givenHoldAuction(BidResponse.builder().build()); + + given(routingContext.response().closed()).willReturn(true); + + // when + target.handle(routingContext); + + // then + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr)); + } + + @Test + public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + assertThat(auctionEvent).isEqualTo(AuctionEvent.builder() + .httpContext(givenHttpContext()) + .status(400) + .errors(singletonList("Invalid request format: Request is invalid")) + .build()); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); + } + + @Test + public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + given(exchangeService.holdAuction(any())) + .willThrow(new RuntimeException("Unexpected exception")); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + final AuctionContext expectedAuctionContext = auctionContext.toBuilder() + .requestTypeMetric(MetricName.openrtb2web) + .build(); + + assertThat(auctionEvent).isEqualTo(AuctionEvent.builder() + .httpContext(givenHttpContext()) + .auctionContext(expectedAuctionContext) + .status(500) + .errors(singletonList("Unexpected exception")) + .build()); + + verifyNoInteractions(hooksMetricsService, hookStageExecutor); + } + + @Test + public void shouldPassSuccessfulEventToAnalyticsReporter() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + assertThat(auctionEvent.getHttpContext()).isEqualTo(givenHttpContext()); + assertThat(auctionEvent.getBidResponse()).isEqualTo(BidResponse.builder().build()); + assertThat(auctionEvent.getStatus()).isEqualTo(200); + assertThat(auctionEvent.getAuctionContext().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2web); + assertThat(auctionEvent.getAuctionContext().getBidResponse()).isEqualTo(BidResponse.builder().build()); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldPassSuccessfulEventToAnalyticsReporterWhenExitpointHookChangesResponseAndHeaders() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"response\":{}}")))); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + assertThat(auctionEvent.getHttpContext()).isEqualTo(givenHttpContext()); + assertThat(auctionEvent.getBidResponse()).isEqualTo(BidResponse.builder().build()); + assertThat(auctionEvent.getStatus()).isEqualTo(200); + assertThat(auctionEvent.getAuctionContext().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2web); + assertThat(auctionEvent.getAuctionContext().getBidResponse()).isEqualTo(BidResponse.builder().build()); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldTolerateDuplicateQueryParamNames() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + final MultiMap params = MultiMap.caseInsensitiveMultiMap(); + params.add("param", "value1"); + given(httpRequest.params()).willReturn(params); + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + final CaseInsensitiveMultiMap expectedParams = CaseInsensitiveMultiMap.builder() + .add("param", "value1") + .build(); + assertThat(auctionEvent.getHttpContext().getQueryParams()).isEqualTo(expectedParams); + } + + @Test + public void shouldTolerateDuplicateHeaderNames() { + // given + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + final MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add("header", "value1"); + given(httpRequest.headers()).willReturn(headers); + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + final CaseInsensitiveMultiMap expectedHeaders = CaseInsensitiveMultiMap.builder() + .add("header", "value1") + .add("header", "value2") + .build(); + assertThat(auctionEvent.getHttpContext().getHeaders()).isEqualTo(expectedHeaders); + } + + @Test + public void shouldSkipAuction() { + // given + final AuctionContext givenAuctionContext = givenAuctionContext(identity()); + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext)); + given(skippedAuctionService.skipAuction(any())) + .willReturn(Future.succeededFuture( + givenAuctionContext.skipAuction().with(BidResponse.builder().build()))); + + // when + target.handle(routingContext); + + // then + verify(getInterfaceRequestFactory, never()).enrichAuctionContext(any()); + verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.ok)); + verifyNoInteractions(exchangeService, analyticsReporterDelegator, hookStageExecutor); + verify(hooksMetricsService).updateHooksMetrics(any()); + verify(httpResponse).setStatusCode(eq(200)); + verify(httpResponse).end("{}"); + } + + @Test + public void shouldReturnSendAuctionEventWithAuctionContextBidResponseDebugInfoHoldingExitpointHookOutcome() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()).toBuilder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_amp, + stageOutcomes())) + .build(); + + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> { + final AuctionContext context = invocation.getArgument(2, AuctionContext.class); + final HookExecutionContext hookExecutionContext = context.getHookExecutionContext(); + hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of( + "http-response", + singletonList( + GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + givenAppliedToImpl())))))) + .build())))))); + return Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))); + }); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + final BidResponse bidResponse = auctionEvent.getBidResponse(); + final ExtModulesTraceAnalyticsTags expectedAnalyticsTags = ExtModulesTraceAnalyticsTags.of(singletonList( + ExtModulesTraceAnalyticsActivity.of( + "some-activity", + "success", + singletonList(ExtModulesTraceAnalyticsResult.of( + "success", + mapper.createObjectNode(), + givenExtModulesTraceAnalyticsAppliedTo()))))); + assertThat(bidResponse.getExt().getPrebid().getModules().getTrace()).isEqualTo(ExtModulesTrace.of( + 8L, + List.of( + ExtModulesTraceStage.of( + Stage.auction_response, + 4L, + singletonList(ExtModulesTraceStageOutcome.of( + "auction-response", + 4L, + singletonList( + ExtModulesTraceGroup.of( + 4L, + asList( + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook1") + .action(ExecutionAction.update) + .build(), + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook2") + .action(ExecutionAction.no_action) + .build())))))), + + ExtModulesTraceStage.of( + Stage.exitpoint, + 4L, + singletonList(ExtModulesTraceStageOutcome.of( + "http-response", + 4L, + singletonList( + ExtModulesTraceGroup.of( + 4L, + singletonList( + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(expectedAnalyticsTags) + .build()))))))))); + } + + @Test + public void shouldReturnSendAuctionEventWithAuctionContextBidResponseAnalyticsTagsHoldingExitpointHookOutcome() { + // given + final ObjectNode analyticsNode = mapper.createObjectNode(); + final ObjectNode optionsNode = analyticsNode.putObject("options"); + optionsNode.put("enableclientdetails", true); + + final AuctionContext givenAuctionContext = givenAuctionContext( + request -> request.ext(ExtRequest.of(ExtRequestPrebid.builder() + .analytics(analyticsNode) + .build()))).toBuilder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_amp, + stageOutcomes())) + .build(); + + given(getInterfaceRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext)); + given(getInterfaceRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> { + final AuctionContext context = invocation.getArgument(2, AuctionContext.class); + final HookExecutionContext hookExecutionContext = context.getHookExecutionContext(); + hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of( + "http-response", + singletonList( + GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + givenAppliedToImpl())))))) + .build())))))); + return Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))); + }); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + final BidResponse bidResponse = auctionEvent.getBidResponse(); + assertThat(bidResponse.getExt()) + .extracting(ExtBidResponse::getPrebid) + .extracting(ExtBidResponsePrebid::getAnalytics) + .extracting(ExtAnalytics::getTags) + .asInstanceOf(InstanceOfAssertFactories.list(ExtAnalyticsTags.class)) + .hasSize(1) + .allSatisfy(extAnalyticsTags -> { + assertThat(extAnalyticsTags.getStage()).isEqualTo(Stage.exitpoint); + assertThat(extAnalyticsTags.getModule()).isEqualTo("exitpoint-module"); + assertThat(extAnalyticsTags.getAnalyticsTags()).isNotNull(); + }); + } + + private static AppliedToImpl givenAppliedToImpl() { + return AppliedToImpl.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static ExtModulesTraceAnalyticsAppliedTo givenExtModulesTraceAnalyticsAppliedTo() { + return ExtModulesTraceAnalyticsAppliedTo.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static EnumMap> stageOutcomes() { + final Map> stageOutcomes = new HashMap<>(); + + stageOutcomes.put(Stage.auction_response, singletonList(StageExecutionOutcome.of( + "auction-response", + singletonList( + GroupExecutionOutcome.of(asList( + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook1") + .action(ExecutionAction.update) + .build(), + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .message("module1 hook2") + .status(ExecutionStatus.success) + .action(ExecutionAction.no_action) + .build())))))); + + return new EnumMap<>(stageOutcomes); + } + + private AuctionContext captureAuctionContext() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(AuctionContext.class); + verify(exchangeService).holdAuction(captor.capture()); + return captor.getValue(); + } + + private AuctionEvent captureAuctionEvent() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(AuctionEvent.class); + verify(analyticsReporterDelegator).processEvent(captor.capture(), any()); + return captor.getValue(); + } + + private void givenHoldAuction(BidResponse bidResponse) { + given(exchangeService.holdAuction(any())) + .willAnswer(inv -> Future.succeededFuture(((AuctionContext) inv.getArgument(0)).toBuilder() + .bidResponse(bidResponse) + .build())); + } + + private AuctionContext givenAuctionContext(UnaryOperator bidRequestCustomizer) { + return givenAuctionContext(bidRequestCustomizer, identity()); + } + + private AuctionContext givenAuctionContext( + UnaryOperator bidRequestCustomizer, + UnaryOperator auctionContextCustomizer) { + + final BidRequest bidRequest = bidRequestCustomizer.apply(BidRequest.builder() + .imp(emptyList())).build(); + + final AuctionContext.AuctionContextBuilder auctionContextBuilder = AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of(true, null, null)) + .build()) + .uidsCookie(uidsCookie) + .bidRequest(bidRequest) + .requestTypeMetric(MetricName.openrtb2web) + .debugContext(DebugContext.of(true, false, TraceLevel.verbose)) + .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_get_interface)) + .timeoutContext(TimeoutContext.of(0, timeout, 0)); + + return auctionContextCustomizer.apply(auctionContextBuilder) + .build(); + } + + private static HttpRequestContext givenHttpContext() { + return HttpRequestContext.builder() + .queryParams(CaseInsensitiveMultiMap.empty()) + .headers(CaseInsensitiveMultiMap.empty()) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/log/HttpInteractionLoggerTest.java b/src/test/java/org/prebid/server/log/HttpInteractionLoggerTest.java index 017cdf03bab..87f0576a6f4 100644 --- a/src/test/java/org/prebid/server/log/HttpInteractionLoggerTest.java +++ b/src/test/java/org/prebid/server/log/HttpInteractionLoggerTest.java @@ -280,6 +280,100 @@ public void maybeLogOpenrtb2AmpShouldNotLogIfSpecEndpointIsNotAmp() { verifyNoInteractions(logger); } + @Test + public void maybeLogOpenrtb2GetInterfaceShouldLogWithExpectedParams() { + // given + final AuctionContext givenAuctionContext = + givenAuctionContext(accountBuilder -> accountBuilder.id("123")); + final HttpLogSpec givenSpec = HttpLogSpec.of(null, null, "123", null, 1); + + // when + target.setSpec(givenSpec); + target.maybeLogOpenrtb2GetInterface(givenAuctionContext, routingContext, 200, "responseBody"); + + // then + verify(logger) + .info("Requested URL: \"{}\", response status: \"{}\", response body: \"{}\"", + "example.com", + 200, + "responseBody"); + } + + @Test + public void maybeLogOpenrtb2GetInterfaceShouldLimitLogBySpecLimit() { + // given + final AuctionContext givenAuctionContext = + givenAuctionContext(accountBuilder -> accountBuilder.id("123")); + final HttpLogSpec givenSpec = HttpLogSpec.of(null, null, "123", null, 1); + + // when + target.setSpec(givenSpec); + target.maybeLogOpenrtb2GetInterface(givenAuctionContext, routingContext, 200, null); + target.maybeLogOpenrtb2GetInterface(givenAuctionContext, routingContext, 200, null); + + // then + verify(logger).info(anyString(), anyString(), any(), any()); + } + + @Test + public void maybeLogOpenrtb2GetInterfaceShouldNotLogIfAccountIdNotEqualsToGivenInSpec() { + // given + final AuctionContext givenAuctionContext = + givenAuctionContext(accountBuilder -> accountBuilder.id("456")); + final HttpLogSpec givenSpec = HttpLogSpec.of(null, null, "123", null, 1); + + // when + target.setSpec(givenSpec); + target.maybeLogOpenrtb2GetInterface(givenAuctionContext, routingContext, 200, null); + + // then + verifyNoInteractions(logger); + } + + @Test + public void maybeLogOpenrtb2GetInterfaceShouldLogIfStatusEqualsToGivenInSpec() { + // given + final AuctionContext givenAuctionContext = givenAuctionContext(identity()); + final HttpLogSpec givenSpec = HttpLogSpec.of(null, 501, null, null, 1); + + // when + target.setSpec(givenSpec); + target.maybeLogOpenrtb2GetInterface(givenAuctionContext, routingContext, 200, null); + target.maybeLogOpenrtb2GetInterface(givenAuctionContext, routingContext, 501, null); + + // then + verify(logger).info(anyString(), anyString(), eq(501), any()); + verify(logger, never()).info(anyString(), anyString(), eq(200), any()); + } + + @Test + public void maybeLogOpenrtb2GetInterfaceShouldLogIfSpecEndpointIsGetInterface() { + // given + final AuctionContext givenAuctionContext = givenAuctionContext(identity()); + final HttpLogSpec givenSpec = HttpLogSpec.of(HttpLogSpec.Endpoint.get_interface, null, null, null, 1); + + // when + target.setSpec(givenSpec); + target.maybeLogOpenrtb2GetInterface(givenAuctionContext, routingContext, 200, null); + + // then + verify(logger).info(anyString(), anyString(), any(), any()); + } + + @Test + public void maybeLogOpenrtb2GetInterfaceShouldNotLogIfSpecEndpointIsNotGetInterface() { + // given + final AuctionContext givenAuctionContext = givenAuctionContext(identity()); + final HttpLogSpec givenSpec = HttpLogSpec.of(HttpLogSpec.Endpoint.auction, null, null, null, 1); + + // when + target.setSpec(givenSpec); + target.maybeLogOpenrtb2GetInterface(givenAuctionContext, routingContext, 200, null); + + // then + verifyNoInteractions(logger); + } + @Test public void maybeLogBidderRequestShouldLogWithExpectedParams() { // given diff --git a/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java b/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java index 29e092f73c8..d5381141185 100644 --- a/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java +++ b/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java @@ -1263,7 +1263,7 @@ private static OpenRtb.BidRequest.Imp.Audio givenProtobufAudio() { } private static Device givenDevice() { - final ExtDevice extDevice = ExtDevice.of(null, null); + final ExtDevice extDevice = ExtDevice.of(null, null, null); extDevice.addProperty("field", TextNode.valueOf("fieldValue")); return Device.builder() diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index e4efd99feb9..b98c0807283 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -430,7 +430,7 @@ public void validateShouldReturnValidationMessageWhenMinWidthPercIsNull() { // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(null, null)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(null, null)))) .build()) .build(); @@ -447,7 +447,7 @@ public void validateShouldReturnValidationMessageWhenMinWidthPercIsLessThanZero( // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(-1, null)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(-1, null)))) .build()) .build(); @@ -464,7 +464,7 @@ public void validateShouldReturnValidationMessageWhenMinWidthPercGreaterThanHund // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(101, null)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(101, null)))) .build()) .build(); @@ -481,7 +481,7 @@ public void validateShouldReturnValidationMessageWhenMinHeightPercIsNull() { // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, null)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(50, null)))) .build()) .build(); @@ -499,7 +499,7 @@ public void validateShouldReturnValidationMessageWhenMinHeightPercIsLessThanZero // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, -1)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(50, -1)))) .build()) .build(); @@ -517,7 +517,7 @@ public void validateShouldReturnValidationMessageWhenMinHeightPercGreaterThanHun // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, 101)))) + .ext(ExtDevice.of(null, null, ExtDevicePrebid.of(ExtDeviceInt.of(50, 101)))) .build()) .build();