diff --git a/docs/config-app.md b/docs/config-app.md index 71e4f55e456..48e8ba749ad 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -110,7 +110,8 @@ Removes and downloads file again if depending service cant process probably corr - `cookie-sync.coop-sync.pri` - lists of bidders prioritised in groups. ## Vtrack -- `vtrack.allow-unkonwn-bidder` - flag allows servicing requests with bidders who were not configured in Prebid Server. +- `vtrack.allow-unknown-bidder` - flag that allows servicing requests with bidders who were not configured in Prebid Server. +- `vtrack.modify-vast-for-unknown-bidder` - flag that allows modifying the VAST value and adding the impression tag to it, for bidders who were not configured in Prebid Server. ## Adapters - `adapters.*` - the section for bidder specific configuration options. diff --git a/src/main/java/org/prebid/server/handler/VtrackHandler.java b/src/main/java/org/prebid/server/handler/VtrackHandler.java index 3594fac13b6..b76155ced21 100644 --- a/src/main/java/org/prebid/server/handler/VtrackHandler.java +++ b/src/main/java/org/prebid/server/handler/VtrackHandler.java @@ -1,5 +1,6 @@ package org.prebid.server.handler; +import com.fasterxml.jackson.databind.JsonNode; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; @@ -39,9 +40,11 @@ public class VtrackHandler implements Handler { private static final String ACCOUNT_PARAMETER = "a"; private static final String INTEGRATION_PARAMETER = "int"; + private static final String TYPE_XML = "xml"; private final long defaultTimeout; private final boolean allowUnknownBidder; + private final boolean modifyVastForUnknownBidder; private final ApplicationSettings applicationSettings; private final BidderCatalog bidderCatalog; private final CacheService cacheService; @@ -50,6 +53,7 @@ public class VtrackHandler implements Handler { public VtrackHandler(long defaultTimeout, boolean allowUnknownBidder, + boolean modifyVastForUnknownBidder, ApplicationSettings applicationSettings, BidderCatalog bidderCatalog, CacheService cacheService, @@ -58,6 +62,7 @@ public VtrackHandler(long defaultTimeout, this.defaultTimeout = defaultTimeout; this.allowUnknownBidder = allowUnknownBidder; + this.modifyVastForUnknownBidder = modifyVastForUnknownBidder; this.applicationSettings = Objects.requireNonNull(applicationSettings); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.cacheService = Objects.requireNonNull(cacheService); @@ -110,16 +115,31 @@ private List vtrackPuts(RoutingContext routingContext) { final List putObjects = ListUtils.emptyIfNull(bidCacheRequest.getPuts()); for (PutObject putObject : putObjects) { - if (StringUtils.isEmpty(putObject.getBidid())) { - throw new IllegalArgumentException("'bidid' is required field and can't be empty"); - } - if (StringUtils.isEmpty(putObject.getBidder())) { - throw new IllegalArgumentException("'bidder' is required field and can't be empty"); - } + validatePutObject(putObject); } return putObjects; } + private static void validatePutObject(PutObject putObject) { + if (StringUtils.isEmpty(putObject.getBidid())) { + throw new IllegalArgumentException("'bidid' is required field and can't be empty"); + } + + if (StringUtils.isEmpty(putObject.getBidder())) { + throw new IllegalArgumentException("'bidder' is required field and can't be empty"); + } + + if (!StringUtils.equals(putObject.getType(), TYPE_XML)) { + throw new IllegalArgumentException("vtrack only accepts type xml"); + } + + final JsonNode value = putObject.getValue(); + final String valueAsString = value != null ? value.asText() : null; + if (!StringUtils.containsIgnoreCase(valueAsString, ""; return vastXml.replace(caseSpecificCloseTag, impressionTag + caseSpecificCloseTag); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 1d18611f865..51cf970cf71 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -102,7 +102,8 @@ setuid: default-timeout-ms: 2000 vtrack: default-timeout-ms: 2000 - allow-unkonwn-bidder: true + allow-unknown-bidder: true + modify-vast-for-unknown-bidder: true cookie-sync: coop-sync: default: true diff --git a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java b/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java index faa1e2543c4..5a620cd59db 100644 --- a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java @@ -34,13 +34,14 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.function.Function.identity; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; @@ -80,7 +81,7 @@ public void setUp() { given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); handler = new VtrackHandler( - 2000, true, applicationSettings, bidderCatalog, cacheService, timeoutFactory, jacksonMapper); + 2000, true, true, applicationSettings, bidderCatalog, cacheService, timeoutFactory, jacksonMapper); } @Test @@ -169,11 +170,50 @@ public void shouldRespondWithBadRequestWhenBidderIsMissing() throws JsonProcessi verify(httpResponse).end(eq("'bidder' is required field and can't be empty")); } + @Test + public void shouldRespondWithBadRequestWhenTypeIsNotXML() throws JsonProcessingException { + // given + given(routingContext.getBody()) + .willReturn(givenVtrackRequest(builder -> builder.bidid("bidId").bidder("bidder").type("json"))); + + // when + handler.handle(routingContext); + + // then + verifyZeroInteractions(applicationSettings, cacheService); + + verify(httpResponse).setStatusCode(eq(400)); + verify(httpResponse).end(eq("vtrack only accepts type xml")); + } + + @Test + public void shouldRespondWithBadRequestWhenValueDoesNotContainVast() throws JsonProcessingException { + // given + given(routingContext.getBody()) + .willReturn(givenVtrackRequest(builder -> builder.bidid("bidId") + .bidder("bidder") + .type("xml") + .value(new TextNode("invalidValue")))); + + // when + handler.handle(routingContext); + + // then + verifyZeroInteractions(applicationSettings, cacheService); + + verify(httpResponse).setStatusCode(eq(400)); + verify(httpResponse).end(eq("vtrack content must be vast")); + } + @Test public void shouldRespondWithInternalServerErrorWhenFetchingAccountFails() throws JsonProcessingException { // given given(routingContext.getBody()) - .willReturn(givenVtrackRequest(builder -> builder.bidid("bidId").bidder("bidder"))); + .willReturn(givenVtrackRequest(builder -> builder + .bidder("bidder") + .bidid("bidId") + .type("xml") + .value(new TextNode(" builder.bidid("bidId").bidder("bidder"))); + .willReturn(givenVtrackRequest(builder -> builder + .bidder("bidder") + .bidid("bidId") + .type("xml") + .value(new TextNode(" putObjects = singletonList( - PutObject.builder().bidid("bidId").bidder("bidder").value(new TextNode("value")).build()); + PutObject.builder() + .bidid("bidId") + .bidder("bidder") + .type("xml") + .value(new TextNode(" putObjects = singletonList( - PutObject.builder().bidid("bidId").bidder("bidder").value(new TextNode("value")).build()); + PutObject.builder() + .bidid("bidId") + .bidder("bidder") + .type("xml") + .value(new TextNode(" putObjects = asList( - PutObject.builder().bidid("bidId1").bidder("bidder").value(new TextNode("value1")).build(), - PutObject.builder().bidid("bidId2").bidder("updatable_bidder").value(new TextNode("value2")).build()); + PutObject.builder().bidid("bidId1") + .bidder("bidder") + .type("xml") + .value(new TextNode(" putObjects = asList( - PutObject.builder().bidid("bidId1").bidder("bidder").value(new TextNode("value1")).build(), - PutObject.builder().bidid("bidId2").bidder("updatable_bidder").value(new TextNode("value2")).build()); + PutObject.builder().bidid("bidId1") + .bidder("bidder") + .type("xml") + .value(new TextNode(" putObjects = asList( PutObject.builder() .bidid("bidId1") .bidder("bidder") - .value(new TextNode("value1")) + .type("xml") + .value(new TextNode(" putObjects = asList( + PutObject.builder() + .bidid("bidId1") + .bidder("bidder") + .type("xml") + .value(new TextNode(" putObjects = asList( + PutObject.builder() + .bidid("bidId1") + .bidder("bidder") + .type("xml") + .value(new TextNode("... customizers) diff --git a/src/test/java/org/prebid/server/vast/VastModifierTest.java b/src/test/java/org/prebid/server/vast/VastModifierTest.java index 4d9a8e6a60b..1f747cde8f9 100644 --- a/src/test/java/org/prebid/server/vast/VastModifierTest.java +++ b/src/test/java/org/prebid/server/vast/VastModifierTest.java @@ -189,6 +189,32 @@ public void createBidVastXmlShouldBeModifiedWithNewImpressionAfterExistingImpres + ""); } + @Test + public void createBidVastXmlShouldInsertImpressionTagForEmptyInLine() { + // when + final String bidAdm = ""; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + + assertThat(result).isEqualTo(""); + } + + @Test + public void createBidVastXmlShouldNotInsertImpressionTagForNoInLineCloseTag() { + // when + final String bidAdm = ""; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + + assertThat(result).isEqualTo(bidAdm); + } + @Test public void createBidVastXmlShouldModifyWrapperTagInCaseInsensitiveMode() { // when @@ -203,6 +229,33 @@ public void createBidVastXmlShouldModifyWrapperTagInCaseInsensitiveMode() { + ""); } + @Test + public void createBidVastXmlShouldInsertImpressionTagForEmptyWrapper() { + // when + final String result = target + .createBidVastXml(BIDDER, "", BID_NURL, + BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + + assertThat(result) + .isEqualTo(""); + } + + @Test + public void createBidVastXmlShouldNotInsertImpressionTagForNoWrapperCloseTag() { + // when + final String bidAdm = ""; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + + assertThat(result).isEqualTo(bidAdm); + } + @Test public void createBidVastXmlShouldModifyInlineTagInCaseInsensitiveMode() { // when @@ -218,7 +271,7 @@ public void createBidVastXmlShouldModifyInlineTagInCaseInsensitiveMode() { } @Test - public void createBidVastXmlShouldNotBeModifiedIfInLineHasNoImpressionTags() { + public void createBidVastXmlShouldBeModifiedIfInLineHasNoImpressionTags() { // when final String bidAdm = ""; final String result = target @@ -227,7 +280,7 @@ public void createBidVastXmlShouldNotBeModifiedIfInLineHasNoImpressionTags() { // then verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); - assertThat(result).isEqualTo(bidAdm); + assertThat(result).isEqualTo(""); } @Test