diff --git a/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java b/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java index e2d353fb003..150c37fd255 100644 --- a/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java +++ b/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java @@ -1,6 +1,7 @@ package org.prebid.server.bidder.smartadserver; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Publisher; @@ -30,6 +31,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; @@ -40,38 +42,50 @@ public class SmartadserverBidder implements Bidder { }; private final String endpointUrl; + private final String secondaryEndpointUrl; private final JacksonMapper mapper; - public SmartadserverBidder(String endpointUrl, JacksonMapper mapper) { + public SmartadserverBidder(String endpointUrl, String secondaryEndpointUrl, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.secondaryEndpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(secondaryEndpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @Override public Result>> makeHttpRequests(BidRequest request) { final List errors = new ArrayList<>(); - final List imps = new ArrayList<>(); - ExtImpSmartadserver extImp = null; + final List modifiedImps = new ArrayList<>(); + final LinkedHashMap impToExtImpMap = new LinkedHashMap<>(); + + boolean isProgrammaticGuaranteed = false; for (Imp imp : request.getImp()) { try { - extImp = parseImpExt(imp); - imps.add(imp); + final ExtImpSmartadserver extImp = parseImpExt(imp); + isProgrammaticGuaranteed |= extImp.isProgrammaticGuaranteed(); + impToExtImpMap.put(imp, extImp); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } } - if (imps.isEmpty()) { + if (impToExtImpMap.isEmpty()) { return Result.withErrors(errors); } + final String extImpKey = isProgrammaticGuaranteed ? "smartadserver" : "bidder"; + impToExtImpMap.forEach((imp, extImp) -> modifiedImps.add(modifyImp(imp, extImp, extImpKey))); + + final ExtImpSmartadserver lastExtImp = impToExtImpMap.lastEntry().getValue(); final BidRequest outgoingRequest = request.toBuilder() - .imp(imps) - .site(modifySite(request.getSite(), extImp.getNetworkId())) + .imp(modifiedImps) + .site(modifySite(request.getSite(), lastExtImp.getNetworkId())) .build(); - final HttpRequest httpRequest = BidderUtil.defaultRequest(outgoingRequest, makeUrl(), mapper); + final HttpRequest httpRequest = BidderUtil.defaultRequest( + outgoingRequest, + makeUrl(isProgrammaticGuaranteed), + mapper); return Result.of(Collections.singletonList(httpRequest), errors); } @@ -83,6 +97,13 @@ private ExtImpSmartadserver parseImpExt(Imp imp) { } } + private Imp modifyImp(Imp imp, ExtImpSmartadserver extImp, String impExtKey) { + final ObjectNode impExt = imp.getExt().deepCopy(); + impExt.remove("bidder"); + impExt.set(impExtKey, mapper.mapper().valueToTree(extImp)); + return imp.toBuilder().ext(impExt).build(); + } + private static Site modifySite(Site site, Integer networkId) { final Site.SiteBuilder siteBuilder = site != null ? site.toBuilder() : Site.builder(); final Publisher sitePublisher = site != null ? site.getPublisher() : null; @@ -98,17 +119,22 @@ private static Publisher modifyPublisher(Publisher publisher, Integer networkId) return publisherBuilder.id(String.valueOf(networkId)).build(); } - private String makeUrl() { - final URI uri; + private String makeUrl(boolean isProgrammaticGuaranteed) { + final String url = isProgrammaticGuaranteed ? secondaryEndpointUrl : endpointUrl; try { - uri = new URI(endpointUrl); + final URI uri = new URI(url); + final String path = isProgrammaticGuaranteed ? "/ortb" : "/api/bid"; + final URIBuilder uriBuilder = new URIBuilder(uri) + .setPath(StringUtils.removeEnd(uri.getPath(), "/") + path); + + if (!isProgrammaticGuaranteed) { + uriBuilder.addParameter("callerId", "5"); + } + + return uriBuilder.toString(); } catch (URISyntaxException e) { - throw new PreBidException("Malformed URL: %s.".formatted(endpointUrl)); + throw new PreBidException("Malformed URL: %s.".formatted(url)); } - return new URIBuilder(uri) - .setPath(StringUtils.removeEnd(uri.getPath(), "/") + "/api/bid") - .addParameter("callerId", "5") - .toString(); } @Override diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartadserver/ExtImpSmartadserver.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartadserver/ExtImpSmartadserver.java index 0d2e3c25487..efc8de91a5f 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartadserver/ExtImpSmartadserver.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartadserver/ExtImpSmartadserver.java @@ -3,9 +3,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; -/** - * Defines the contract for bidrequest.imp[i].ext.smartadserver - */ @Value(staticConstructor = "of") public class ExtImpSmartadserver { @@ -20,4 +17,7 @@ public class ExtImpSmartadserver { @JsonProperty("networkId") Integer networkId; + + @JsonProperty(value = "programmaticGuaranteed", access = JsonProperty.Access.WRITE_ONLY) + boolean programmaticGuaranteed; } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java index 5264a6a3e77..3d831685aff 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java @@ -1,5 +1,8 @@ package org.prebid.server.spring.config.bidder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.smartadserver.SmartadserverBidder; import org.prebid.server.json.JacksonMapper; @@ -23,19 +26,28 @@ public class SmartadserverConfiguration { @Bean("smartadserverConfigurationProperties") @ConfigurationProperties("adapters.smartadserver") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); + SmartadserverConfigurationProperties configurationProperties() { + return new SmartadserverConfigurationProperties(); } @Bean - BidderDeps smartadserverBidderDeps(BidderConfigurationProperties smartadserverConfigurationProperties, + BidderDeps smartadserverBidderDeps(SmartadserverConfigurationProperties smartadserverConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { - return BidderDepsAssembler.forBidder(BIDDER_NAME) + return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(smartadserverConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new SmartadserverBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new SmartadserverBidder( + config.getEndpoint(), config.getSecondaryEndpoint(), mapper)) .assemble(); } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class SmartadserverConfigurationProperties extends BidderConfigurationProperties { + + private String secondaryEndpoint; + } } diff --git a/src/main/resources/bidder-config/smartadserver.yaml b/src/main/resources/bidder-config/smartadserver.yaml index f1233b2e754..343bec392de 100644 --- a/src/main/resources/bidder-config/smartadserver.yaml +++ b/src/main/resources/bidder-config/smartadserver.yaml @@ -1,6 +1,7 @@ adapters: smartadserver: endpoint: https://ssb-global.smartadserver.com + secondary-endpoint: https://prebid-global.smartadserver.com endpoint-compression: gzip aliases: equativ: diff --git a/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java b/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java index 4565c7581de..c9f64cd8a0c 100644 --- a/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java @@ -1,6 +1,7 @@ package org.prebid.server.bidder.smartadserver; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; @@ -37,12 +38,20 @@ public class SmartadserverBidderTest extends VertxTest { private static final String ENDPOINT_URL = "https://test.endpoint.com/path?testParam=testVal"; + private static final String SECONDARY_URL = "https://test.endpoint2.com/path?testParam=testVal"; - private final SmartadserverBidder target = new SmartadserverBidder(ENDPOINT_URL, jacksonMapper); + private final SmartadserverBidder target = new SmartadserverBidder(ENDPOINT_URL, SECONDARY_URL, jacksonMapper); @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new SmartadserverBidder("invalid_url", jacksonMapper)); + assertThatIllegalArgumentException() + .isThrownBy(() -> new SmartadserverBidder("invalid_url", SECONDARY_URL, jacksonMapper)); + } + + @Test + public void creationShouldFailOnInvalidSecondaryEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new SmartadserverBidder(ENDPOINT_URL, "invalid_url", jacksonMapper)); } @Test @@ -64,7 +73,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { } @Test - public void makeHttpRequestsShouldCreateCorrectURL() { + public void makeHttpRequestsShouldCreateCorrectPrimaryURLWhenProgrammaticGuaranteedIsAbsent() { // given final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(givenImp(identity()))) @@ -80,6 +89,46 @@ public void makeHttpRequestsShouldCreateCorrectURL() { .isEqualTo("https://test.endpoint.com/path/api/bid?testParam=testVal&callerId=5"); } + @Test + public void makeHttpRequestsShouldCreateCorrectSecondaryURLWhenProgrammaticGuaranteedIsTrue() { + // given + final ObjectNode givenImpExt = mapper.createObjectNode() + .set("bidder", mapper.createObjectNode().put("programmaticGuaranteed", true)); + + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(imp -> imp.ext(givenImpExt)))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().getFirst().getUri()) + .isEqualTo("https://test.endpoint2.com/path/ortb?testParam=testVal"); + } + + @Test + public void makeHttpRequestsShouldCreateCorrectPrimaryURLWhenProgrammaticGuaranteedIsFalse() { + // given + final ObjectNode givenImpExt = mapper.createObjectNode() + .set("bidder", mapper.createObjectNode().put("programmaticGuaranteed", false)); + + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(imp -> imp.ext(givenImpExt)))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().getFirst().getUri()) + .isEqualTo("https://test.endpoint.com/path/api/bid?testParam=testVal&callerId=5"); + } + @Test public void makeHttpRequestsShouldUpdateSiteObjectIfPresent() { // given @@ -152,6 +201,56 @@ public void makeHttpRequestsShouldCreateSingleRequestWithValidImpsOnly() { .containsExactly("123"); } + @Test + public void makeHttpRequestsShouldModifyImpWhenProgrammaticGuaranteedIsTrueAtLeastInOneValidImp() { + // given + final ObjectNode givenImpExt1 = mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .put("programmaticGuaranteed", false) + .put("networkId", 1) + .put("siteId", 2) + .put("formatId", 3) + .put("pageId", 4)); + final ObjectNode givenImpExt2 = mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .put("programmaticGuaranteed", true) + .put("networkId", 5) + .put("siteId", 6) + .put("formatId", 7) + .put("pageId", 8)); + + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of( + givenImp(imp -> imp.id("impId1").ext(givenImpExt1)), + givenImp(imp -> imp.id("impId2").ext(givenImpExt2)))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ObjectNode expectedImpExt1 = mapper.createObjectNode() + .set("smartadserver", mapper.createObjectNode() + .put("networkId", 1) + .put("siteId", 2) + .put("formatId", 3) + .put("pageId", 4)); + final ObjectNode expectedImpExt2 = mapper.createObjectNode() + .set("smartadserver", mapper.createObjectNode() + .put("networkId", 5) + .put("siteId", 6) + .put("formatId", 7) + .put("pageId", 8)); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(expectedImpExt1, expectedImpExt2); + } + @Test public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given @@ -299,10 +398,12 @@ public void makeBidsShouldReturnNativeBidIfMarkupTypeIsNative() throws JsonProce private static Imp givenImp(Function impCustomizer) { return impCustomizer.apply(Imp.builder() - .id("123")) - .banner(Banner.builder().build()) - .video(Video.builder().build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSmartadserver.of(1, 2, 3, 4)))) + .id("123") + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of( + null, + ExtImpSmartadserver.of(1, 2, 3, 4, false))))) .build(); } diff --git a/src/test/java/org/prebid/server/it/SmartadserverTest.java b/src/test/java/org/prebid/server/it/SmartadserverTest.java index 88b5e57c9cf..296883918e8 100644 --- a/src/test/java/org/prebid/server/it/SmartadserverTest.java +++ b/src/test/java/org/prebid/server/it/SmartadserverTest.java @@ -18,7 +18,7 @@ public class SmartadserverTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromSmartadserver() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smartadserver-exchange/api/bid")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smartadserver-secondary-exchange/ortb")) .withRequestBody(equalToJson(jsonFrom("openrtb2/smartadserver/test-smartadserver-bid-request.json"))) .willReturn(aResponse() .withBody(jsonFrom("openrtb2/smartadserver/test-smartadserver-bid-response.json")))); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json index bd93b91baf0..b02d4d0a642 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json @@ -14,7 +14,8 @@ "siteId": 1, "pageId": 2, "formatId": 3, - "networkId": 73 + "networkId": 73, + "programmaticGuaranteed": true } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request.json index 05fec0b445d..f53c88fe858 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request.json @@ -10,7 +10,7 @@ }, "ext": { "tid": "${json-unit.any-string}", - "bidder": { + "smartadserver": { "siteId": 1, "pageId": 2, "formatId": 3, diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 51958ab075b..046b53455d5 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -491,6 +491,7 @@ adapters.smaato.enabled=true adapters.smaato.endpoint=http://localhost:8090/smaato-exchange adapters.smartadserver.enabled=true adapters.smartadserver.endpoint=http://localhost:8090/smartadserver-exchange +adapters.smartadserver.secondary-endpoint=http://localhost:8090/smartadserver-secondary-exchange adapters.smartadserver.aliases.equativ.enabled=true adapters.smartrtb.enabled=true adapters.smartrtb.endpoint=http://localhost:8090/smartrtb-exchange/