From 15ef06e1323905b9572bc804dc1953245918a75c Mon Sep 17 00:00:00 2001 From: Thomas Sormonte Date: Wed, 1 Oct 2025 16:55:19 +0200 Subject: [PATCH 1/6] Sparteo: add required query params to adapter endpoint --- .../server/bidder/sparteo/SparteoBidder.java | 213 +++++- src/main/resources/bidder-config/sparteo.yaml | 2 +- .../bidder/sparteo/SparteoBidderTest.java | 723 +++++++++++------- .../org/prebid/server/it/SparteoTest.java | 3 + .../server/it/test-application.properties | 2 +- 5 files changed, 658 insertions(+), 285 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java index 6beba78b3dc..b677581c938 100644 --- a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java +++ b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; 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.request.Publisher; @@ -12,6 +13,7 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -29,6 +31,8 @@ import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -38,6 +42,12 @@ public class SparteoBidder implements Bidder { + private static final String NETWORK_ID_MACRO = "{{NetworkId}}"; + private static final String SITE_DOMAIN_QUERY_MACRO = "{{SiteDomainQuery}}"; + private static final String APP_DOMAIN_QUERY_MACRO = "{{AppDomainQuery}}"; + private static final String BUNDLE_QUERY_MACRO = "{{BundleQuery}}"; + private static final String UNKNOWN_VALUE = "unknown"; + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { }; @@ -45,7 +55,7 @@ public class SparteoBidder implements Bidder { private final JacksonMapper mapper; public SparteoBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.endpointUrl = HttpUtil.validateUrlSyntax(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @@ -53,19 +63,18 @@ public SparteoBidder(String endpointUrl, JacksonMapper mapper) { public Result>> makeHttpRequests(BidRequest request) { final List errors = new ArrayList<>(); final List modifiedImps = new ArrayList<>(); - String siteNetworkId = null; + String networkId = null; for (Imp imp : request.getImp()) { - if (siteNetworkId == null) { + if (networkId == null) { try { - siteNetworkId = parseExtImp(imp).getNetworkId(); + networkId = parseExtImp(imp).getNetworkId(); } catch (PreBidException e) { errors.add(BidderError.badInput( "ignoring imp id=%s, error processing ext: %s".formatted( imp.getId(), e.getMessage()))); } } - final ObjectNode modifiedExt = modifyImpExt(imp); modifiedImps.add(imp.toBuilder().ext(modifiedExt).build()); } @@ -74,12 +83,21 @@ public Result>> makeHttpRequests(BidRequest request return Result.withErrors(errors); } - final BidRequest outgoingRequest = request.toBuilder() - .imp(modifiedImps) - .site(modifySite(request.getSite(), siteNetworkId, mapper)) - .build(); + final BidRequest.BidRequestBuilder builder = request.toBuilder().imp(modifiedImps); + + final Site site = request.getSite(); + final App app = request.getApp(); + + if (site != null) { + builder.site(modifySite(site, networkId, mapper)); + } else if (app != null) { + builder.app(modifyApp(app, networkId, mapper)); + } + + final BidRequest outgoingRequest = builder.build(); - final HttpRequest call = BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + final String finalEndpointUrl = replaceMacros(site, app, networkId, errors); + final HttpRequest call = BidderUtil.defaultRequest(outgoingRequest, finalEndpointUrl, mapper); return Result.of(Collections.singletonList(call), errors); } @@ -92,23 +110,24 @@ private ExtImpSparteo parseExtImp(Imp imp) { } } - private static ObjectNode modifyImpExt(Imp imp) { + private ObjectNode modifyImpExt(Imp imp) { final ObjectNode modifiedImpExt = imp.getExt().deepCopy(); final ObjectNode sparteoNode = modifiedImpExt.putObject("sparteo"); final JsonNode bidderJsonNode = modifiedImpExt.remove("bidder"); sparteoNode.set("params", bidderJsonNode); - return modifiedImpExt; } - private Site modifySite(Site site, String siteNetworkId, JacksonMapper mapper) { - if (site == null || site.getPublisher() == null || siteNetworkId == null) { + private Site modifySite(Site site, String networkId, JacksonMapper mapper) { + if (site == null) { return site; } - final Publisher originalPublisher = site.getPublisher(); - final ExtPublisher originalExt = originalPublisher.getExt(); + final Publisher originalPublisher = site.getPublisher() != null + ? site.getPublisher() + : Publisher.builder().build(); + final ExtPublisher originalExt = originalPublisher.getExt(); final ExtPublisher modifiedExt = originalExt != null ? ExtPublisher.of(originalExt.getPrebid()) : ExtPublisher.empty(); @@ -117,25 +136,165 @@ private Site modifySite(Site site, String siteNetworkId, JacksonMapper mapper) { mapper.fillExtension(modifiedExt, originalExt); } - final JsonNode paramsProperty = modifiedExt.getProperty("params"); - final ObjectNode paramsNode; + final ObjectNode paramsNode = ensureParamsNode(modifiedExt); + paramsNode.put("networkId", networkId); - if (paramsProperty != null && paramsProperty.isObject()) { - paramsNode = (ObjectNode) paramsProperty; - } else { - paramsNode = mapper.mapper().createObjectNode(); - modifiedExt.addProperty("params", paramsNode); + final Publisher modifiedPublisher = originalPublisher.toBuilder() + .ext(modifiedExt) + .build(); + + return site.toBuilder().publisher(modifiedPublisher).build(); + } + + private App modifyApp(App app, String networkId, JacksonMapper mapper) { + if (app == null) { + return app; } - paramsNode.put("networkId", siteNetworkId); + final Publisher originalPublisher = app.getPublisher() != null + ? app.getPublisher() + : Publisher.builder().build(); + + final ExtPublisher originalExt = originalPublisher.getExt(); + final ExtPublisher modifiedExt = originalExt != null + ? ExtPublisher.of(originalExt.getPrebid()) + : ExtPublisher.empty(); + + if (originalExt != null) { + mapper.fillExtension(modifiedExt, originalExt); + } + + final ObjectNode paramsNode = ensureParamsNode(modifiedExt); + paramsNode.put("networkId", networkId); final Publisher modifiedPublisher = originalPublisher.toBuilder() .ext(modifiedExt) .build(); - return site.toBuilder() - .publisher(modifiedPublisher) - .build(); + return app.toBuilder().publisher(modifiedPublisher).build(); + } + + private ObjectNode ensureParamsNode(ExtPublisher extPublisher) { + final JsonNode paramsProperty = extPublisher.getProperty("params"); + if (paramsProperty != null && paramsProperty.isObject()) { + return (ObjectNode) paramsProperty; + } + final ObjectNode paramsNode = mapper.mapper().createObjectNode(); + extPublisher.addProperty("params", paramsNode); + return paramsNode; + } + + private String replaceMacros(Site site, App app, String networkId, List errors) { + final String siteDomain = resolveSiteDomain(site); + final String appDomain = resolveAppDomain(app); + final String bundle = resolveBundle(app); + + if (site != null) { + if (UNKNOWN_VALUE.equals(siteDomain)) { + errors.add(BidderError.badInput( + "Domain not found. Missing the site.domain or the site.page field")); + } + } + + if (UNKNOWN_VALUE.equals(bundle)) { + errors.add(BidderError.badInput( + "Bundle not found. Missing the app.bundle field.")); + } + return resolveEndpoint(siteDomain, appDomain, networkId, bundle); + } + + private static String normalizeHostname(String host) { + String h = StringUtils.trimToEmpty(host); + if (h.isEmpty()) { + return ""; + } + + String hostname = null; + try { + hostname = new URI(h).getHost(); + } catch (URISyntaxException e) { + } + + if (StringUtils.isNotEmpty(hostname)) { + h = hostname; + } else { + if (h.contains(":")) { + h = StringUtils.substringBefore(h, ":"); + } else { + h = StringUtils.substringBefore(h, "/"); + } + } + + h = h.toLowerCase(); + h = StringUtils.removeStart(h, "www."); + h = StringUtils.removeEnd(h, "."); + + return "null".equals(h) ? "" : h; + } + + private String resolveSiteDomain(Site site) { + if (site != null) { + final String siteDomain = normalizeHostname(site.getDomain()); + if (siteDomain != null && !siteDomain.isEmpty()) { + return siteDomain; + } else { + final String fromPage = normalizeHostname(site.getPage()); + if (fromPage != null && !fromPage.isEmpty()) { + return fromPage; + } + } + return UNKNOWN_VALUE; + } + + return null; + } + + private String resolveAppDomain(App app) { + if (app != null) { + final String appDomain = normalizeHostname(app.getDomain()); + if (appDomain != null && !appDomain.isEmpty()) { + return appDomain; + } + return UNKNOWN_VALUE; + } + + return null; + } + + private String resolveBundle(App app) { + if (app == null) { + return null; + } + + final String rawBundle = app.getBundle(); + if (rawBundle == null || rawBundle.isBlank()) { + return UNKNOWN_VALUE; + } + + final String bundle = rawBundle.trim(); + if ("null".equalsIgnoreCase(bundle)) { + return UNKNOWN_VALUE; + } + + return bundle; + } + + private String resolveEndpoint(String siteDomain, String appDomain, String networkId, String bundle) { + final String siteDomainQuery = StringUtils.isNotBlank(siteDomain) + ? "&site_domain=" + HttpUtil.encodeUrl(siteDomain) + : ""; + final String appDomainQuery = StringUtils.isNotBlank(appDomain) + ? "&app_domain=" + HttpUtil.encodeUrl(appDomain) + : ""; + final String bundleQuery = StringUtils.isNotBlank(bundle) + ? "&bundle=" + HttpUtil.encodeUrl(bundle) + : ""; + + return endpointUrl + .replace(NETWORK_ID_MACRO, StringUtils.defaultString(networkId)) + .replace(BUNDLE_QUERY_MACRO, bundleQuery) + .replace(SITE_DOMAIN_QUERY_MACRO, siteDomainQuery) + .replace(APP_DOMAIN_QUERY_MACRO, appDomainQuery); } @Override diff --git a/src/main/resources/bidder-config/sparteo.yaml b/src/main/resources/bidder-config/sparteo.yaml index 44cbe16bd86..3dbaa3e900c 100644 --- a/src/main/resources/bidder-config/sparteo.yaml +++ b/src/main/resources/bidder-config/sparteo.yaml @@ -1,6 +1,6 @@ adapters: sparteo: - endpoint: https://bid.sparteo.com/s2s-auction + endpoint: https://bid.sparteo.com/s2s-auction?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}} meta-info: maintainer-email: prebid@sparteo.com app-media-types: diff --git a/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java index 2920f94adbd..317a0f37841 100644 --- a/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.NullNode; 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.request.Publisher; @@ -20,7 +21,6 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; -import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; import org.prebid.server.proto.openrtb.ext.response.BidType; @@ -39,69 +39,17 @@ public class SparteoBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://test.sparteo.com/endpoint"; + private static final String ENDPOINT_URL = + "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; private final SparteoBidder sparteoBidder = new SparteoBidder(ENDPOINT_URL, jacksonMapper); @Test public void creationShouldFailOnInvalidEndpointUrl() { - // when and then assertThatIllegalArgumentException() .isThrownBy(() -> new SparteoBidder("invalid_url", jacksonMapper)) .withMessage("URL supplied is not valid: invalid_url"); } - @Test - public void makeHttpRequestsShouldReturnErrorWhenImpExtIsInvalid() { - // given - final BidRequest bidRequest = givenBidRequest( - givenImp(imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, "invalid"))))); - - // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> assertThat(error.getMessage()) - .startsWith("ignoring imp id=impId, error processing ext: invalid imp.ext")); - } - - @Test - public void makeHttpRequestsShouldReturnErrorWhenAllImpsAreInvalid() { - // given - final BidRequest bidRequest = givenBidRequest( - givenImp(imp -> imp.id("imp1").ext(mapper.valueToTree(ExtPrebid.of(null, "invalid"))))); - - // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1); - } - - @Test - public void makeHttpRequestsShouldReturnPartialResultWhenSomeImpsAreInvalid() { - // given - final ObjectNode validExt = mapper.createObjectNode(); - validExt.set("bidder", mapper.createObjectNode().put("key", "value")); - - final BidRequest bidRequest = givenBidRequest( - givenImp(imp -> imp.id("imp1").ext(mapper.valueToTree(ExtPrebid.of(null, "invalid")))), - givenImp(imp -> imp.id("imp2").ext(validExt))); - - // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> assertThat(error.getMessage()).startsWith("ignoring imp id=imp1")); - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getExt) - .extracting((JsonNode ext) -> ext.at("/sparteo/params/key").asText()) - .containsExactly("", "value"); - } - @Test public void makeHttpRequestsShouldSetNetworkIdOnSitePublisherExtWhenPresentInImp() { // given @@ -118,10 +66,11 @@ public void makeHttpRequestsShouldSetNetworkIdOnSitePublisherExtWhenPresentInImp final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).isEmpty(); + assertMissingSiteDomainWarning(result); assertThat(result.getValue()) .extracting(HttpRequest::getMethod, HttpRequest::getUri) - .containsExactly(tuple(HttpMethod.POST, ENDPOINT_URL)); + .containsExactly(tuple(HttpMethod.POST, + "https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=unknown")); assertThat(result.getValue()) .extracting(HttpRequest::getPayload) @@ -159,7 +108,7 @@ public void makeHttpRequestsShouldUseFirstNetworkIdWhenMultipleImpsDefineIt() { final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).isEmpty(); + assertMissingSiteDomainWarning(result); assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .extracting(BidRequest::getSite) @@ -170,10 +119,14 @@ public void makeHttpRequestsShouldUseFirstNetworkIdWhenMultipleImpsDefineIt() { } @Test - public void makeHttpRequestsShouldOverwriteSparteoParamsWithBidderParamsOnConflict() { + public void makeHttpRequestsShouldMergeSparteoParamsWithBidderParamsOnConflict() { // given + final ObjectNode bidderNode = mapper.createObjectNode(); + bidderNode.put("conflictingParam", "bidderValue"); + bidderNode.put("networkId", "id1"); + final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("conflictingParam", "bidderValue")); + impExt.set("bidder", bidderNode); final ObjectNode sparteoNode = impExt.putObject("sparteo"); sparteoNode.putObject("params").put("conflictingParam", "sparteoValue"); @@ -190,6 +143,12 @@ public void makeHttpRequestsShouldOverwriteSparteoParamsWithBidderParamsOnConfli .extracting(Imp::getExt) .extracting((JsonNode ext) -> ext.at("/sparteo/params/conflictingParam").asText()) .containsExactly("bidderValue"); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .extracting((JsonNode ext) -> ext.at("/sparteo/params/networkId").asText()) + .containsExactly("id1"); } @Test @@ -198,7 +157,8 @@ public void makeHttpRequestsShouldHandleRequestWithoutSiteOrPublisher() { final ObjectNode impExt = mapper.createObjectNode(); impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); - final BidRequest bidRequestNoSite = givenBidRequest(request -> request.site(null), + final BidRequest bidRequestNoSite = givenBidRequest( + request -> request.site(null), givenImp(imp -> imp.ext(impExt))); final BidRequest bidRequestNoPublisher = givenBidRequest( @@ -211,18 +171,21 @@ public void makeHttpRequestsShouldHandleRequestWithoutSiteOrPublisher() { sparteoBidder.makeHttpRequests(bidRequestNoPublisher); // then - assertThat(resultNoSite.getErrors()).isEmpty(); assertThat(resultNoSite.getValue()) .extracting(HttpRequest::getPayload) .extracting(BidRequest::getSite) .containsNull(); - assertThat(resultNoPublisher.getErrors()).isEmpty(); - assertThat(resultNoPublisher.getValue()) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getSite) - .extracting(Site::getPublisher) - .containsNull(); + assertThat(resultNoSite.getErrors()).isEmpty(); + final BidRequest out = resultNoPublisher.getValue().get(0).getPayload(); + assertThat(out.getSite()).isNotNull(); + assertThat(out.getSite().getPublisher()).isNotNull(); + + final ExtPublisher publisherExt = (ExtPublisher) out.getSite().getPublisher().getExt(); + assertThat(publisherExt).isNotNull(); + assertThat(publisherExt.getProperties()).containsKey("params"); + assertThat(publisherExt.getProperties().get("params").get("networkId").asText()) + .isEqualTo("testNetworkId"); } @Test @@ -244,7 +207,7 @@ public void makeHttpRequestsShouldMergeNetworkIdIntoExistingPublisherExtParams() final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).isEmpty(); + assertMissingSiteDomainWarning(result); assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .extracting(BidRequest::getSite) @@ -259,7 +222,7 @@ public void makeHttpRequestsShouldMergeNetworkIdIntoExistingPublisherExtParams() @Test public void makeHttpRequestsShouldAddParamsToPublisherExtWhenExtExistsWithoutParams() - throws JsonProcessingException { + throws JsonProcessingException { // given final ObjectNode impExt = mapper.createObjectNode(); impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); @@ -277,7 +240,7 @@ public void makeHttpRequestsShouldAddParamsToPublisherExtWhenExtExistsWithoutPar final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).isEmpty(); + assertMissingSiteDomainWarning(result); assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .extracting(BidRequest::getSite) @@ -290,147 +253,6 @@ public void makeHttpRequestsShouldAddParamsToPublisherExtWhenExtExistsWithoutPar }); } - @Test - public void makeHttpRequestsShouldCreateEmptyParamsWhenBidderExtIsEmpty() { - // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode()); - impExt.putObject("sparteo"); - - final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.ext(impExt))); - - // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getExt) - .allSatisfy(ext -> { - assertThat(ext.at("/sparteo")).isInstanceOf(ObjectNode.class); - assertThat(ext.at("/sparteo/params")).isInstanceOf(ObjectNode.class); - assertThat(ext.at("/sparteo/params").size()).isZero(); - }); - } - - @Test - public void makeHttpRequestsShouldNotAddParamsWhenNetworkIdIsMissing() { - // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("otherParam", "value")); - final BidRequest bidRequest = givenBidRequest( - request -> request.site(Site.builder().publisher(Publisher.builder().id("pub1").build()).build()), - givenImp(imp -> imp.ext(impExt))); - - // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getSite) - .extracting(Site::getPublisher) - .allSatisfy(publisher -> { - final ExtPublisher publisherExt = (ExtPublisher) publisher.getExt(); - if (publisherExt == null || publisherExt.getProperties() == null) { - assertThat(publisherExt).isNull(); - } else { - assertThat(publisherExt.getProperties()).doesNotContainKey("params"); - } - }); - } - - @Test - public void makeHttpRequestsShouldNotAddNetworkIdWhenItIsNullInExt() { - // given - final ObjectNode impExt = mapper.createObjectNode(); - final ObjectNode bidderNode = mapper.createObjectNode(); - bidderNode.putNull("networkId"); - impExt.set("bidder", bidderNode); - - final BidRequest bidRequest = givenBidRequest( - request -> request.site(Site.builder().publisher(Publisher.builder().id("pub1").build()).build()), - givenImp(imp -> imp.ext(impExt))); - - // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getSite) - .extracting(Site::getPublisher) - .allSatisfy(publisher -> { - final ExtPublisher publisherExt = (ExtPublisher) publisher.getExt(); - if (publisherExt != null && publisherExt.getProperties() != null - && publisherExt.getProperties().containsKey("params")) { - assertThat(publisherExt.getProperties().get("params")).isInstanceOf(ObjectNode.class); - assertThat(((ObjectNode) - publisherExt.getProperties().get("params")).has("networkId")).isFalse(); - } - }); - } - - @Test - public void makeHttpRequestsShouldNotModifyPublisherExtWhenNetworkIdIsMissing() throws JsonProcessingException { - // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("someOtherParam", "someValue")); - - final ObjectNode publisherExtJson = mapper.createObjectNode(); - publisherExtJson.put("existing", "value"); - final ExtPublisher originalPublisherExt = mapper.convertValue(publisherExtJson, ExtPublisher.class); - - final BidRequest bidRequest = givenBidRequest( - request -> request.site(Site.builder().publisher( - Publisher.builder().ext(originalPublisherExt).build()).build()), - givenImp(imp -> imp.ext(impExt))); - - // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getSite) - .extracting(Site::getPublisher) - .extracting(Publisher::getExt) - .extracting(ext -> ((ExtPublisher) ext).getProperties()) - .allSatisfy(properties -> { - assertThat(properties.get("existing").asText()).isEqualTo("value"); - assertThat(properties).doesNotContainKey("params"); - }); - } - - @Test - public void makeHttpRequestsShouldOverwriteInvalidSparteoExtWhenBidderExtIsValid() { - // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("param", "value")); - impExt.put("sparteo", "this_is_a_string"); - - final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.ext(impExt))); - - // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getExt) - .allSatisfy(ext -> { - assertThat(ext.at("/sparteo")).isInstanceOf(ObjectNode.class); - assertThat(ext.at("/sparteo/params/param").asText()).isEqualTo("value"); - }); - } - @Test public void makeHttpRequestsShouldReturnEmptyResultWhenRequestHasNoImps() { // given @@ -522,12 +344,10 @@ public void makeBidsShouldReturnEmptyResultWhenBidResponseIsNull() throws JsonPr } @Test - public void makeBidsShouldReturnEmptyResultWhenBidResponseHasNoSeatBids() - throws JsonProcessingException { + public void makeBidsShouldReturnEmptyResultWhenBidResponseHasNoSeatBids() throws JsonProcessingException { // given final BidResponse bidResponse = BidResponse.builder().seatbid(Collections.emptyList()).build(); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -543,8 +363,7 @@ public void makeBidsShouldReturnBannerBidWhenMediaTypeIsBanner() throws JsonProc final Bid bid = givenBid(builder -> builder.impid("imp1").price(BigDecimal.valueOf(1.23)).adm("adm-banner"), BidType.banner.getName()); final BidResponse bidResponse = givenBidResponse(bid, "EUR"); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -559,11 +378,10 @@ public void makeBidsShouldReturnBannerBidWhenMediaTypeIsBanner() throws JsonProc public void makeBidsShouldReturnVideoBidWhenMediaTypeIsVideo() throws JsonProcessingException { // given final Bid bid = givenBid(builder -> - builder.impid("imp2").price(BigDecimal.valueOf(2.34)).adm("adm-video"), + builder.impid("imp2").price(BigDecimal.valueOf(2.34)).adm("adm-video"), BidType.video.getName()); final BidResponse bidResponse = givenBidResponse(bid, "EUR"); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -578,11 +396,10 @@ public void makeBidsShouldReturnVideoBidWhenMediaTypeIsVideo() throws JsonProces public void makeBidsShouldReturnNativeBidWhenMediaTypeIsNative() throws JsonProcessingException { // given final Bid bid = givenBid(builder -> - builder.impid("imp3").price(BigDecimal.valueOf(3.45)).adm("adm-native"), + builder.impid("imp3").price(BigDecimal.valueOf(3.45)).adm("adm-native"), BidType.xNative.getName()); final BidResponse bidResponse = givenBidResponse(bid, "EUR"); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -603,8 +420,7 @@ public void makeBidsShouldReturnErrorForUnsupportedMediaTypeAndProcessOthers() t builder.impid("impBanner").price(BigDecimal.valueOf(2.0)), BidType.banner.getName()); final BidResponse bidResponse = givenBidResponse(List.of(audioBid, bannerBid), "EUR"); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -624,8 +440,7 @@ public void makeBidsShouldReturnErrorWhenBidExtIsNull() throws JsonProcessingExc // given final Bid bid = givenBid(builder -> builder.impid("imp1").ext(null), null); final BidResponse bidResponse = givenBidResponse(bid, "USD"); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -642,8 +457,7 @@ public void makeBidsShouldReturnErrorWhenPrebidIsMissingInBidExt() throws JsonPr // given final Bid bid = givenBid(builder -> builder.impid("imp1").ext(mapper.createObjectNode()), null); final BidResponse bidResponse = givenBidResponse(bid, "USD"); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -660,8 +474,7 @@ public void makeBidsShouldReturnErrorWhenPrebidTypeIsMissingInBidExt() throws Js // given final Bid bid = givenBid(builder -> builder.impid("imp1").ext(createBidExtWithEmptyPrebid()), null); final BidResponse bidResponse = givenBidResponse(bid, "USD"); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -680,8 +493,7 @@ public void makeBidsShouldReturnErrorWhenPrebidCannotBeParsed() throws JsonProce malformedExt.putArray("prebid"); final Bid bid = givenBid(builder -> builder.impid("imp1").ext(malformedExt), null); final BidResponse bidResponse = givenBidResponse(bid, "USD"); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -698,8 +510,7 @@ public void makeBidsShouldReturnErrorWhenPrebidTypeIsUnsupported() throws JsonPr // given final Bid bid = givenBid(builder -> builder.impid("imp1"), "unknown-type"); final BidResponse bidResponse = givenBidResponse(bid, "USD"); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -715,15 +526,14 @@ public void makeBidsShouldReturnErrorWhenPrebidTypeIsUnsupported() throws JsonPr public void makeBidsShouldProcessValidBidsWhenSeatBidContainsNulls() throws JsonProcessingException { // given final Bid validBid = givenBid(builder -> - builder.impid("validImp").price(BigDecimal.ONE), + builder.impid("validImp").price(BigDecimal.ONE), BidType.banner.getName()); final List bids = new ArrayList<>(); bids.add(null); bids.add(validBid); final BidResponse bidResponse = givenBidResponse(bids, "USD"); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -739,21 +549,20 @@ public void makeBidsShouldProcessValidBidsWhenSeatBidContainsNulls() throws Json public void makeBidsShouldCorrectlyProcessMultipleBidsAndSeatBids() throws JsonProcessingException { // given final Bid bid1 = givenBid(builder -> - builder.impid("imp1").price(BigDecimal.valueOf(1.0)), + builder.impid("imp1").price(BigDecimal.valueOf(1.0)), BidType.banner.getName()); final Bid bid2 = givenBid(builder -> - builder.impid("imp2").price(BigDecimal.valueOf(2.0)), + builder.impid("imp2").price(BigDecimal.valueOf(2.0)), BidType.video.getName()); final Bid bid3 = givenBid(builder -> - builder.impid("imp3").price(BigDecimal.valueOf(3.0)), + builder.impid("imp3").price(BigDecimal.valueOf(3.0)), BidType.xNative.getName()); final SeatBid seatBid1 = SeatBid.builder().bid(asList(bid1, bid2)).build(); final SeatBid seatBid2 = SeatBid.builder().bid(singletonList(bid3)).build(); final BidResponse bidResponse = BidResponse.builder().cur("USD").seatbid(asList(seatBid1, seatBid2)).build(); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -777,8 +586,7 @@ public void makeBidsShouldReturnErrorWhenPrebidExtIsNullNode() throws JsonProces final Bid bid = givenBid(builder -> builder.impid("imp1").ext(bidExtWithNullPrebid), null); final BidResponse bidResponse = givenBidResponse(bid, "USD"); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -794,10 +602,10 @@ public void makeBidsShouldReturnErrorWhenPrebidExtIsNullNode() throws JsonProces public void makeBidsShouldProcessValidSeatBidsWhenResponseContainsNulls() throws JsonProcessingException { // given final Bid validBid1 = givenBid(builder -> - builder.impid("validImp1").price(BigDecimal.TEN), + builder.impid("validImp1").price(BigDecimal.TEN), BidType.banner.getName()); final Bid validBid2 = givenBid(builder -> - builder.impid("validImp2").price(BigDecimal.ONE), + builder.impid("validImp2").price(BigDecimal.ONE), BidType.banner.getName()); final SeatBid validSeatBid1 = SeatBid.builder().bid(singletonList(validBid1)).build(); @@ -809,8 +617,7 @@ public void makeBidsShouldProcessValidSeatBidsWhenResponseContainsNulls() throws seatBidsWithNull.add(validSeatBid2); final BidResponse bidResponse = BidResponse.builder().cur("USD").seatbid(seatBidsWithNull).build(); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -829,8 +636,7 @@ public void makeBidsShouldReturnEmptyResultWhenSeatBidHasNullBidList() throws Js final SeatBid seatBidWithNullBids = SeatBid.builder().bid(null).build(); final BidResponse bidResponse = BidResponse.builder().cur("USD").seatbid(singletonList(seatBidWithNullBids)).build(); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -846,8 +652,7 @@ public void makeBidsShouldReturnEmptyResultWhenSeatBidHasEmptyBidList() throws J final SeatBid seatBidWithEmptyBids = SeatBid.builder().bid(Collections.emptyList()).build(); final BidResponse bidResponse = BidResponse.builder().cur("USD").seatbid(singletonList(seatBidWithEmptyBids)).build(); - final BidderCall httpCall = - givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); @@ -857,6 +662,384 @@ public void makeBidsShouldReturnEmptyResultWhenSeatBidHasEmptyBidList() throws J assertThat(result.getValue()).isEmpty(); } + @Test + public void makeHttpRequestsShouldAppendSiteDomainAndNetworkIdAsQueryParams() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r.site(Site.builder() + .domain("dev.sparteo.com") + .publisher(Publisher.builder().build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).isEqualTo( + "https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); + } + + @Test + public void makeHttpRequestsShouldAppendSitePageDomainAndNetworkIdAsQueryParams() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r.site(Site.builder() + .domain(null) + .page("https://www.dev.sparteo.com:3000/p") + .publisher(Publisher.builder().build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).isEqualTo( + "https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); + } + + @Test + public void makeHttpRequestsShouldUseSiteDomainWhenPublisherDomainIsMissing() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r.site(Site.builder() + .domain("dev.sparteo.com") + .publisher(Publisher.builder().build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); + } + + @Test + public void makeHttpRequestsShouldPreferSiteDomainOverPublisherDomain() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r.site(Site.builder() + .domain("site.sparteo.com") + .publisher(Publisher.builder().domain("dev.sparteo.com").build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().get(0).getUri()) + .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&site_domain=site.sparteo.com"); + } + + @Test + public void makeHttpRequestsShouldUseAppDomainWhenNoSite() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("com.sparteo.app") + .publisher(Publisher.builder().id("p1").build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertMissingBundleWarning(result); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().get(0).getUri()) + .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=com.sparteo.app&bundle=unknown"); + } + + @Test + public void makeHttpRequestsShouldSetNetworkIdOnAppPublisherExtWhenNoSite() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("com.sparteo.app") + .publisher(Publisher.builder().build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertMissingBundleWarning(result); + final BidRequest out = result.getValue().get(0).getPayload(); + assertThat(out.getApp()).isNotNull(); + assertThat(out.getApp().getPublisher()).isNotNull(); + + final ExtPublisher ext = (ExtPublisher) out.getApp().getPublisher().getExt(); + assertThat(ext).isNotNull(); + assertThat(ext.getProperties()).containsKey("params"); + assertThat(ext.getProperties().get("params").get("networkId").asText()).isEqualTo("networkId"); + } + + @Test + public void makeHttpRequestsShouldCreateAppPublisherWhenMissingAndSetNetworkId() { + // given + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("com.sparteo.app") + .publisher(null) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertMissingBundleWarning(result); + final BidRequest out = result.getValue().get(0).getPayload(); + assertThat(out.getApp()).isNotNull(); + assertThat(out.getApp().getPublisher()).isNotNull(); + + final ExtPublisher ext = (ExtPublisher) out.getApp().getPublisher().getExt(); + assertThat(ext).isNotNull(); + assertThat(ext.getProperties()).containsKey("params"); + assertThat(ext.getProperties().get("params").get("networkId").asText()).isEqualTo("networkId"); + } + + @Test + public void makeHttpRequestsShouldAppendBundleWhenAppBundlePresent() { + // given + final String endpointWithBundle = + "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; + final SparteoBidder bidder = new SparteoBidder(endpointWithBundle, jacksonMapper); + + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("dev.sparteo.com") + .bundle("com.sparteo.app") + .publisher(Publisher.builder().id("p1").build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = bidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().get(0).getUri()) + .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=com.sparteo.app"); + } + + @Test + public void makeHttpRequestsShouldNotAppendBundleWhenNoAppBundle() { + // given + final String endpointWithBundle = + "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; + final SparteoBidder bidder = new SparteoBidder(endpointWithBundle, jacksonMapper); + + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(Site.builder().domain("dev.sparteo.com").publisher(Publisher.builder().build()).build()) + .app(null), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = bidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().get(0).getUri()) + .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&site_domain=dev.sparteo.com"); + } + + @Test + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppPresentButBundleMissing() { + // given + final String endpointWithBundle = + "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; + final SparteoBidder bidder = new SparteoBidder(endpointWithBundle, jacksonMapper); + + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("dev.sparteo.com") + .publisher(Publisher.builder().id("p1").build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = bidder.makeHttpRequests(bidRequest); + + // then + assertMissingBundleWarning(result); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().get(0).getUri()) + .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=unknown"); + } + + @Test + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleBlankOrLiteralNull() { + // given + final String endpointWithBundle = + "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; + final SparteoBidder bidder = new SparteoBidder(endpointWithBundle, jacksonMapper); + + for (String rawBundle : new String[] { null, "", " ", "null", "NuLl" }) { + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("dev.sparteo.com") + .bundle(rawBundle) + .publisher(Publisher.builder().id("p1").build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = bidder.makeHttpRequests(bidRequest); + + // then + assertMissingBundleWarning(result); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().get(0).getUri()) + .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=unknown"); + } + } + + @Test + public void makeHttpRequestsShouldWarnAndSetUnknownAppDomainWhenAppDomainMissing() { + // given + final String endpoint = + "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; + final SparteoBidder bidder = new SparteoBidder(endpoint, jacksonMapper); + + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "nid")); + + final BidRequest bidRequest = givenBidRequest( + r -> r.site(null) + .app(App.builder() + .domain(null) // missing app domain → warning + unknown + .bundle("com.example.bundle") + .publisher(Publisher.builder().build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = bidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).isEqualTo( + "https://test.sparteo.com/endpoint?network_id=nid&app_domain=unknown&bundle=com.example.bundle"); + } + + @Test + public void normalizeHostnameShouldReturnBaseDomain() { + final String base = "dev.sparteo.com"; + + // Already normalized + assertThat(normalize(base)).isEqualTo(base); + + // Case folding + trimming + assertThat(normalize("DeV.SpArTeO.CoM")).isEqualTo(base); + assertThat(normalize(" " + base + " ")).isEqualTo(base); + + // Strip leading "www." + assertThat(normalize("www." + base)).isEqualTo(base); + assertThat(normalize("WWW." + base)).isEqualTo(base); + + // Strip exactly ONE trailing dot + assertThat(normalize(base + ".")).isEqualTo(base); + // Pathological: multiple trailing dots → only one removed by the method + assertThat(normalize(base + "..")).isEqualTo(base + "."); + + // Literal "null" (any case) -> empty string + assertThat(normalize("null")).isEqualTo(""); + assertThat(normalize("NuLl")).isEqualTo(""); + + // Empty / whitespace-only -> empty string + assertThat(normalize("")).isEqualTo(""); + assertThat(normalize(" ")).isEqualTo(""); + + // Non-"www" prefixes are NOT stripped + assertThat(normalize("www2." + base)).isEqualTo("www2." + base); + + // Bare host + port + assertThat(normalize("www." + base + ":8080")).isEqualTo(base); + assertThat(normalize("DEV.SPARTEO.COM:443")).isEqualTo(base); + assertThat(normalize(base + ".:8443")).isEqualTo(base); // trailing dot + port + + // Bare host + path + assertThat(normalize(base + "/some/path?x=1")).isEqualTo(base); + assertThat(normalize("www." + base + "/p")).isEqualTo(base); + + // Bare host + port + path + assertThat(normalize(base + ":8080/p?q=1")).isEqualTo(base); + assertThat(normalize("www." + base + ":3000/some/path")).isEqualTo(base); + + // Absolute URLs + assertThat(normalize("https://www." + base + "/x")).isEqualTo(base); + assertThat(normalize("http://WWW." + base + ":8080/abc")).isEqualTo(base); + + // Odd spacing around URL + assertThat(normalize(" https://www." + base + ":3000/x ")).isEqualTo(base); + } + private BidRequest givenBidRequest(UnaryOperator customizer, Imp... imps) { return customizer.apply(BidRequest.builder().imp(asList(imps))).build(); } @@ -872,11 +1055,9 @@ private Imp givenImp(UnaryOperator impCustomizer) { private Bid givenBid(UnaryOperator bidCustomizer, String mediaType) { final Bid.BidBuilder builder = Bid.builder(); bidCustomizer.apply(builder); - if (builder.build().getExt() == null && mediaType != null) { builder.ext(createBidExtWithType(mediaType)); } - return builder.build(); } @@ -916,4 +1097,34 @@ private ObjectNode createBidExtWithEmptyPrebid() { bidExt.set("prebid", mapper.createObjectNode()); return bidExt; } + + private static void assertMissingSiteDomainWarning(Result result) { + assertThat(result.getErrors()) + .hasSize(1) + .allSatisfy(err -> { + assertThat(err.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(err.getMessage()) + .contains("Domain not found. Missing the site.domain or the site.page field"); + }); + } + + private static void assertMissingBundleWarning(Result result) { + assertThat(result.getErrors()) + .hasSize(1) + .allSatisfy(err -> { + assertThat(err.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(err.getMessage()) + .contains("Bundle not found. Missing the app.bundle field."); + }); + } + + private static String normalize(String in) { + try { + final var m = SparteoBidder.class.getDeclaredMethod("normalizeHostname", String.class); + m.setAccessible(true); + return (String) m.invoke(null, in); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Unable to call normalizeHostname via reflection", e); + } + } } diff --git a/src/test/java/org/prebid/server/it/SparteoTest.java b/src/test/java/org/prebid/server/it/SparteoTest.java index f169a486e50..713cf35fee1 100644 --- a/src/test/java/org/prebid/server/it/SparteoTest.java +++ b/src/test/java/org/prebid/server/it/SparteoTest.java @@ -5,6 +5,7 @@ import org.prebid.server.model.Endpoint; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; @@ -15,6 +16,8 @@ public class SparteoTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromSparteoBanner() throws Exception { WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/sparteo-exchange")) + .withQueryParam("network_id", equalTo("networkId")) + .withQueryParam("site_domain", equalTo("dev.sparteo.com")) .withRequestBody(equalToJson( jsonFrom("openrtb2/sparteo/test-sparteo-bid-request.json"))) .willReturn(aResponse().withBody( 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 3d57277ff9d..8834873ce28 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -534,7 +534,7 @@ adapters.sovrn.endpoint=http://localhost:8090/sovrn-exchange adapters.sovrnXsp.enabled=true adapters.sovrnXsp.endpoint=http://localhost:8090/sovrnxsp-exchange adapters.sparteo.enabled=true -adapters.sparteo.endpoint=http://localhost:8090/sparteo-exchange +adapters.sparteo.endpoint=http://localhost:8090/sparteo-exchange?network_id={{NetworkId}}{{SiteDomainQuery}} adapters.sspbc.enabled=true adapters.sspbc.endpoint=http://localhost:8090/sspbc-exchange adapters.sharethrough.enabled=true From 506fe8351685f89cffac04dbd2e8a483dd539561 Mon Sep 17 00:00:00 2001 From: Thomas Sormonte Date: Tue, 4 Nov 2025 10:48:48 +0100 Subject: [PATCH 2/6] AntoxaAntoxic code review --- .../server/bidder/sparteo/SparteoBidder.java | 167 +++++-------- src/main/resources/bidder-config/sparteo.yaml | 2 +- .../bidder/sparteo/SparteoBidderTest.java | 235 ++++++++---------- .../server/it/test-application.properties | 2 +- 4 files changed, 174 insertions(+), 232 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java index b677581c938..228c2584ebf 100644 --- a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java +++ b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java @@ -30,6 +30,7 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import org.apache.http.client.utils.URIBuilder; import java.net.URI; import java.net.URISyntaxException; @@ -43,9 +44,6 @@ public class SparteoBidder implements Bidder { private static final String NETWORK_ID_MACRO = "{{NetworkId}}"; - private static final String SITE_DOMAIN_QUERY_MACRO = "{{SiteDomainQuery}}"; - private static final String APP_DOMAIN_QUERY_MACRO = "{{AppDomainQuery}}"; - private static final String BUNDLE_QUERY_MACRO = "{{BundleQuery}}"; private static final String UNKNOWN_VALUE = "unknown"; private static final TypeReference> TYPE_REFERENCE = @@ -83,18 +81,14 @@ public Result>> makeHttpRequests(BidRequest request return Result.withErrors(errors); } - final BidRequest.BidRequestBuilder builder = request.toBuilder().imp(modifiedImps); - final Site site = request.getSite(); final App app = request.getApp(); - if (site != null) { - builder.site(modifySite(site, networkId, mapper)); - } else if (app != null) { - builder.app(modifyApp(app, networkId, mapper)); - } - - final BidRequest outgoingRequest = builder.build(); + final BidRequest outgoingRequest = request.toBuilder() + .imp(modifiedImps) + .site(modifySite(site, networkId)) + .app(modifyApp(app, networkId)) + .build(); final String finalEndpointUrl = replaceMacros(site, app, networkId, errors); final HttpRequest call = BidderUtil.defaultRequest(outgoingRequest, finalEndpointUrl, mapper); @@ -118,7 +112,7 @@ private ObjectNode modifyImpExt(Imp imp) { return modifiedImpExt; } - private Site modifySite(Site site, String networkId, JacksonMapper mapper) { + private Site modifySite(Site site, String networkId) { if (site == null) { return site; } @@ -127,26 +121,12 @@ private Site modifySite(Site site, String networkId, JacksonMapper mapper) { ? site.getPublisher() : Publisher.builder().build(); - final ExtPublisher originalExt = originalPublisher.getExt(); - final ExtPublisher modifiedExt = originalExt != null - ? ExtPublisher.of(originalExt.getPrebid()) - : ExtPublisher.empty(); - - if (originalExt != null) { - mapper.fillExtension(modifiedExt, originalExt); - } - - final ObjectNode paramsNode = ensureParamsNode(modifiedExt); - paramsNode.put("networkId", networkId); - - final Publisher modifiedPublisher = originalPublisher.toBuilder() - .ext(modifiedExt) - .build(); + final Publisher modifiedPublisher = modifiedPublisher(originalPublisher, networkId); return site.toBuilder().publisher(modifiedPublisher).build(); } - private App modifyApp(App app, String networkId, JacksonMapper mapper) { + private App modifyApp(App app, String networkId) { if (app == null) { return app; } @@ -155,23 +135,23 @@ private App modifyApp(App app, String networkId, JacksonMapper mapper) { ? app.getPublisher() : Publisher.builder().build(); - final ExtPublisher originalExt = originalPublisher.getExt(); - final ExtPublisher modifiedExt = originalExt != null - ? ExtPublisher.of(originalExt.getPrebid()) - : ExtPublisher.empty(); + final Publisher modifiedPublisher = modifiedPublisher(originalPublisher, networkId); - if (originalExt != null) { - mapper.fillExtension(modifiedExt, originalExt); - } + return app.toBuilder().publisher(modifiedPublisher).build(); + } + + private Publisher modifiedPublisher(Publisher originalPublisher, String networkId) { + final ExtPublisher originalExt = originalPublisher.getExt(); + final ExtPublisher modifiedExt = originalExt == null + ? ExtPublisher.empty() + : mapper.mapper().convertValue(originalExt, ExtPublisher.class); final ObjectNode paramsNode = ensureParamsNode(modifiedExt); paramsNode.put("networkId", networkId); - final Publisher modifiedPublisher = originalPublisher.toBuilder() + return originalPublisher.toBuilder() .ext(modifiedExt) .build(); - - return app.toBuilder().publisher(modifiedPublisher).build(); } private ObjectNode ensureParamsNode(ExtPublisher extPublisher) { @@ -196,14 +176,44 @@ private String replaceMacros(Site site, App app, String networkId, List { + final String domain = normalizeHostname(s.getDomain()); + return StringUtils.isNotEmpty(domain) ? domain : normalizeHostname(s.getPage()); + }) + .filter(StringUtils::isNotEmpty) + .orElse(site != null ? UNKNOWN_VALUE : null); + } + + private String resolveAppDomain(App app) { + return Optional.ofNullable(app) + .map(App::getDomain) + .map(SparteoBidder::normalizeHostname) + .filter(StringUtils::isNotEmpty) + .orElse(app != null ? UNKNOWN_VALUE : null); + } + + private String resolveBundle(App app) { + if (app == null) { + return null; + } + + return Optional.ofNullable(app.getBundle()) + .filter(rawBundle -> !rawBundle.isBlank()) + .map(String::trim) + .filter(bundle -> !"null".equalsIgnoreCase(bundle)) + .orElse(UNKNOWN_VALUE); + } + + public static String normalizeHostname(String host) { String h = StringUtils.trimToEmpty(host); if (h.isEmpty()) { return ""; @@ -232,69 +242,24 @@ private static String normalizeHostname(String host) { return "null".equals(h) ? "" : h; } - private String resolveSiteDomain(Site site) { - if (site != null) { - final String siteDomain = normalizeHostname(site.getDomain()); - if (siteDomain != null && !siteDomain.isEmpty()) { - return siteDomain; - } else { - final String fromPage = normalizeHostname(site.getPage()); - if (fromPage != null && !fromPage.isEmpty()) { - return fromPage; - } - } - return UNKNOWN_VALUE; - } - - return null; - } + private String resolveEndpoint(String siteDomain, String appDomain, String networkId, String bundle) { + final String baseUrl = endpointUrl.replace(NETWORK_ID_MACRO, StringUtils.defaultString(networkId)); - private String resolveAppDomain(App app) { - if (app != null) { - final String appDomain = normalizeHostname(app.getDomain()); - if (appDomain != null && !appDomain.isEmpty()) { - return appDomain; + try { + final URIBuilder uriBuilder = new URIBuilder(baseUrl); + if (StringUtils.isNotBlank(siteDomain)) { + uriBuilder.addParameter("site_domain", siteDomain); } - return UNKNOWN_VALUE; - } - - return null; - } - - private String resolveBundle(App app) { - if (app == null) { - return null; - } - - final String rawBundle = app.getBundle(); - if (rawBundle == null || rawBundle.isBlank()) { - return UNKNOWN_VALUE; - } - - final String bundle = rawBundle.trim(); - if ("null".equalsIgnoreCase(bundle)) { - return UNKNOWN_VALUE; + if (StringUtils.isNotBlank(appDomain)) { + uriBuilder.addParameter("app_domain", appDomain); + } + if (StringUtils.isNotBlank(bundle)) { + uriBuilder.addParameter("bundle", bundle); + } + return uriBuilder.build().toString(); + } catch (URISyntaxException e) { + throw new PreBidException("Failed to build endpoint URL", e); } - - return bundle; - } - - private String resolveEndpoint(String siteDomain, String appDomain, String networkId, String bundle) { - final String siteDomainQuery = StringUtils.isNotBlank(siteDomain) - ? "&site_domain=" + HttpUtil.encodeUrl(siteDomain) - : ""; - final String appDomainQuery = StringUtils.isNotBlank(appDomain) - ? "&app_domain=" + HttpUtil.encodeUrl(appDomain) - : ""; - final String bundleQuery = StringUtils.isNotBlank(bundle) - ? "&bundle=" + HttpUtil.encodeUrl(bundle) - : ""; - - return endpointUrl - .replace(NETWORK_ID_MACRO, StringUtils.defaultString(networkId)) - .replace(BUNDLE_QUERY_MACRO, bundleQuery) - .replace(SITE_DOMAIN_QUERY_MACRO, siteDomainQuery) - .replace(APP_DOMAIN_QUERY_MACRO, appDomainQuery); } @Override diff --git a/src/main/resources/bidder-config/sparteo.yaml b/src/main/resources/bidder-config/sparteo.yaml index 3dbaa3e900c..89f1de2517f 100644 --- a/src/main/resources/bidder-config/sparteo.yaml +++ b/src/main/resources/bidder-config/sparteo.yaml @@ -1,6 +1,6 @@ adapters: sparteo: - endpoint: https://bid.sparteo.com/s2s-auction?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}} + endpoint: https://bid.sparteo.com/s2s-auction?network_id={{NetworkId}} meta-info: maintainer-email: prebid@sparteo.com app-media-types: diff --git a/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java index 317a0f37841..6d961a8bddd 100644 --- a/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java @@ -40,7 +40,7 @@ public class SparteoBidderTest extends VertxTest { private static final String ENDPOINT_URL = - "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; + "https://test.sparteo.com/endpoint?network_id={{NetworkId}}"; private final SparteoBidder sparteoBidder = new SparteoBidder(ENDPOINT_URL, jacksonMapper); @Test @@ -177,15 +177,13 @@ public void makeHttpRequestsShouldHandleRequestWithoutSiteOrPublisher() { .containsNull(); assertThat(resultNoSite.getErrors()).isEmpty(); - final BidRequest out = resultNoPublisher.getValue().get(0).getPayload(); - assertThat(out.getSite()).isNotNull(); - assertThat(out.getSite().getPublisher()).isNotNull(); - - final ExtPublisher publisherExt = (ExtPublisher) out.getSite().getPublisher().getExt(); - assertThat(publisherExt).isNotNull(); - assertThat(publisherExt.getProperties()).containsKey("params"); - assertThat(publisherExt.getProperties().get("params").get("networkId").asText()) - .isEqualTo("testNetworkId"); + assertThat(resultNoPublisher.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .extracting(Publisher::getExt) + .extracting(ext -> ((ExtPublisher) ext).getProperties().get("params").get("networkId").asText()) + .containsExactly("testNetworkId"); } @Test @@ -681,9 +679,9 @@ public void makeHttpRequestsShouldAppendSiteDomainAndNetworkIdAsQueryParams() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1); - final String uri = result.getValue().get(0).getUri(); - assertThat(uri).isEqualTo( - "https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); } @Test @@ -705,10 +703,9 @@ public void makeHttpRequestsShouldAppendSitePageDomainAndNetworkIdAsQueryParams( // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1); - final String uri = result.getValue().get(0).getUri(); - assertThat(uri).isEqualTo( - "https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); } @Test @@ -752,9 +749,9 @@ public void makeHttpRequestsShouldPreferSiteDomainOverPublisherDomain() { // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1); - assertThat(result.getValue().get(0).getUri()) - .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&site_domain=site.sparteo.com"); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&site_domain=site.sparteo.com"); } @Test @@ -777,9 +774,9 @@ public void makeHttpRequestsShouldUseAppDomainWhenNoSite() { // then assertMissingBundleWarning(result); - assertThat(result.getValue()).hasSize(1); - assertThat(result.getValue().get(0).getUri()) - .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=com.sparteo.app&bundle=unknown"); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=com.sparteo.app&bundle=unknown"); } @Test @@ -845,10 +842,6 @@ public void makeHttpRequestsShouldCreateAppPublisherWhenMissingAndSetNetworkId() @Test public void makeHttpRequestsShouldAppendBundleWhenAppBundlePresent() { // given - final String endpointWithBundle = - "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; - final SparteoBidder bidder = new SparteoBidder(endpointWithBundle, jacksonMapper); - final ObjectNode impExt = mapper.createObjectNode(); impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); @@ -863,22 +856,18 @@ public void makeHttpRequestsShouldAppendBundleWhenAppBundlePresent() { givenImp(i -> i.ext(impExt))); // when - final Result>> result = bidder.makeHttpRequests(bidRequest); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1); - assertThat(result.getValue().get(0).getUri()) - .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=com.sparteo.app"); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=com.sparteo.app"); } @Test public void makeHttpRequestsShouldNotAppendBundleWhenNoAppBundle() { // given - final String endpointWithBundle = - "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; - final SparteoBidder bidder = new SparteoBidder(endpointWithBundle, jacksonMapper); - final ObjectNode impExt = mapper.createObjectNode(); impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); @@ -889,22 +878,18 @@ public void makeHttpRequestsShouldNotAppendBundleWhenNoAppBundle() { givenImp(i -> i.ext(impExt))); // when - final Result>> result = bidder.makeHttpRequests(bidRequest); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1); - assertThat(result.getValue().get(0).getUri()) - .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&site_domain=dev.sparteo.com"); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&site_domain=dev.sparteo.com"); } @Test public void makeHttpRequestsShouldAppendUnknownBundleWhenAppPresentButBundleMissing() { // given - final String endpointWithBundle = - "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; - final SparteoBidder bidder = new SparteoBidder(endpointWithBundle, jacksonMapper); - final ObjectNode impExt = mapper.createObjectNode(); impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); @@ -918,126 +903,128 @@ public void makeHttpRequestsShouldAppendUnknownBundleWhenAppPresentButBundleMiss givenImp(i -> i.ext(impExt))); // when - final Result>> result = bidder.makeHttpRequests(bidRequest); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then assertMissingBundleWarning(result); - assertThat(result.getValue()).hasSize(1); - assertThat(result.getValue().get(0).getUri()) - .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=unknown"); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=unknown"); + } + + @Test + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsNull() { + testAppendUnknownBundle(null); + } + + @Test + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsEmpty() { + testAppendUnknownBundle(""); + } + + @Test + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsBlank() { + testAppendUnknownBundle(" "); } @Test - public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleBlankOrLiteralNull() { + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsLiteralNull() { + testAppendUnknownBundle("null"); + } + + @Test + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsLiteralNullIgnoreCase() { + testAppendUnknownBundle("NuLl"); + } + + private void testAppendUnknownBundle(String rawBundle) { // given - final String endpointWithBundle = - "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; - final SparteoBidder bidder = new SparteoBidder(endpointWithBundle, jacksonMapper); - - for (String rawBundle : new String[] { null, "", " ", "null", "NuLl" }) { - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); - - final BidRequest bidRequest = givenBidRequest( - r -> r - .site(null) - .app(App.builder() - .domain("dev.sparteo.com") - .bundle(rawBundle) - .publisher(Publisher.builder().id("p1").build()) - .build()), - givenImp(i -> i.ext(impExt))); - - // when - final Result>> result = bidder.makeHttpRequests(bidRequest); - - // then - assertMissingBundleWarning(result); - assertThat(result.getValue()).hasSize(1); - assertThat(result.getValue().get(0).getUri()) - .isEqualTo("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=unknown"); - } + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("dev.sparteo.com") + .bundle(rawBundle) + .publisher(Publisher.builder().id("p1").build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertMissingBundleWarning(result); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=unknown"); } @Test public void makeHttpRequestsShouldWarnAndSetUnknownAppDomainWhenAppDomainMissing() { // given - final String endpoint = - "https://test.sparteo.com/endpoint?network_id={{NetworkId}}{{SiteDomainQuery}}{{AppDomainQuery}}{{BundleQuery}}"; - final SparteoBidder bidder = new SparteoBidder(endpoint, jacksonMapper); - final ObjectNode impExt = mapper.createObjectNode(); impExt.set("bidder", mapper.createObjectNode().put("networkId", "nid")); final BidRequest bidRequest = givenBidRequest( r -> r.site(null) .app(App.builder() - .domain(null) // missing app domain → warning + unknown + .domain(null) .bundle("com.example.bundle") .publisher(Publisher.builder().build()) .build()), givenImp(i -> i.ext(impExt))); // when - final Result>> result = bidder.makeHttpRequests(bidRequest); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - final String uri = result.getValue().get(0).getUri(); - assertThat(uri).isEqualTo( - "https://test.sparteo.com/endpoint?network_id=nid&app_domain=unknown&bundle=com.example.bundle"); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly( + "https://test.sparteo.com/endpoint?network_id=nid&app_domain=unknown&bundle=com.example.bundle"); } @Test public void normalizeHostnameShouldReturnBaseDomain() { final String base = "dev.sparteo.com"; - // Already normalized - assertThat(normalize(base)).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname(base)).isEqualTo(base); - // Case folding + trimming - assertThat(normalize("DeV.SpArTeO.CoM")).isEqualTo(base); - assertThat(normalize(" " + base + " ")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname("DeV.SpArTeO.CoM")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname(" " + base + " ")).isEqualTo(base); - // Strip leading "www." - assertThat(normalize("www." + base)).isEqualTo(base); - assertThat(normalize("WWW." + base)).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname("www." + base)).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname("WWW." + base)).isEqualTo(base); - // Strip exactly ONE trailing dot - assertThat(normalize(base + ".")).isEqualTo(base); - // Pathological: multiple trailing dots → only one removed by the method - assertThat(normalize(base + "..")).isEqualTo(base + "."); + assertThat(SparteoBidder.normalizeHostname(base + ".")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname(base + "..")).isEqualTo(base + "."); - // Literal "null" (any case) -> empty string - assertThat(normalize("null")).isEqualTo(""); - assertThat(normalize("NuLl")).isEqualTo(""); + assertThat(SparteoBidder.normalizeHostname("null")).isEqualTo(""); + assertThat(SparteoBidder.normalizeHostname("NuLl")).isEqualTo(""); - // Empty / whitespace-only -> empty string - assertThat(normalize("")).isEqualTo(""); - assertThat(normalize(" ")).isEqualTo(""); + assertThat(SparteoBidder.normalizeHostname("")).isEqualTo(""); + assertThat(SparteoBidder.normalizeHostname(" ")).isEqualTo(""); - // Non-"www" prefixes are NOT stripped - assertThat(normalize("www2." + base)).isEqualTo("www2." + base); + assertThat(SparteoBidder.normalizeHostname("www2." + base)).isEqualTo("www2." + base); - // Bare host + port - assertThat(normalize("www." + base + ":8080")).isEqualTo(base); - assertThat(normalize("DEV.SPARTEO.COM:443")).isEqualTo(base); - assertThat(normalize(base + ".:8443")).isEqualTo(base); // trailing dot + port + assertThat(SparteoBidder.normalizeHostname("www." + base + ":8080")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname("DEV.SPARTEO.COM:443")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname(base + ".:8443")).isEqualTo(base); - // Bare host + path - assertThat(normalize(base + "/some/path?x=1")).isEqualTo(base); - assertThat(normalize("www." + base + "/p")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname(base + "/some/path?x=1")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname("www." + base + "/p")).isEqualTo(base); - // Bare host + port + path - assertThat(normalize(base + ":8080/p?q=1")).isEqualTo(base); - assertThat(normalize("www." + base + ":3000/some/path")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname(base + ":8080/p?q=1")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname("www." + base + ":3000/some/path")).isEqualTo(base); - // Absolute URLs - assertThat(normalize("https://www." + base + "/x")).isEqualTo(base); - assertThat(normalize("http://WWW." + base + ":8080/abc")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname("https://www." + base + "/x")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname("http://WWW." + base + ":8080/abc")).isEqualTo(base); - // Odd spacing around URL - assertThat(normalize(" https://www." + base + ":3000/x ")).isEqualTo(base); + assertThat(SparteoBidder.normalizeHostname(" https://www." + base + ":3000/x ")).isEqualTo(base); } private BidRequest givenBidRequest(UnaryOperator customizer, Imp... imps) { @@ -1117,14 +1104,4 @@ private static void assertMissingBundleWarning(Result result) { .contains("Bundle not found. Missing the app.bundle field."); }); } - - private static String normalize(String in) { - try { - final var m = SparteoBidder.class.getDeclaredMethod("normalizeHostname", String.class); - m.setAccessible(true); - return (String) m.invoke(null, in); - } catch (ReflectiveOperationException e) { - throw new RuntimeException("Unable to call normalizeHostname via reflection", e); - } - } } 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 8834873ce28..cab7dd7dba4 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -534,7 +534,7 @@ adapters.sovrn.endpoint=http://localhost:8090/sovrn-exchange adapters.sovrnXsp.enabled=true adapters.sovrnXsp.endpoint=http://localhost:8090/sovrnxsp-exchange adapters.sparteo.enabled=true -adapters.sparteo.endpoint=http://localhost:8090/sparteo-exchange?network_id={{NetworkId}}{{SiteDomainQuery}} +adapters.sparteo.endpoint=http://localhost:8090/sparteo-exchange?network_id={{NetworkId}} adapters.sspbc.enabled=true adapters.sspbc.endpoint=http://localhost:8090/sspbc-exchange adapters.sharethrough.enabled=true From c7235eba69db8492b6f0a75fdf1f032419e9076c Mon Sep 17 00:00:00 2001 From: Thomas Sormonte Date: Thu, 6 Nov 2025 16:18:09 +0100 Subject: [PATCH 3/6] AntoxaAntoxic code review 2 --- .../server/bidder/sparteo/SparteoBidder.java | 78 +- .../bidder/sparteo/util/SparteoUtil.java | 41 + src/main/resources/bidder-config/sparteo.yaml | 2 +- .../bidder/sparteo/SparteoBidderTest.java | 921 +++++++++--------- .../bidder/sparteo/SparteoUtilTest.java | 49 + 5 files changed, 561 insertions(+), 530 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/sparteo/util/SparteoUtil.java create mode 100644 src/test/java/org/prebid/server/bidder/sparteo/SparteoUtilTest.java diff --git a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java index 228c2584ebf..0e735a346b1 100644 --- a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java +++ b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java @@ -20,6 +20,7 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.sparteo.util.SparteoUtil; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -32,7 +33,6 @@ import org.prebid.server.util.HttpUtil; import org.apache.http.client.utils.URIBuilder; -import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; @@ -121,7 +121,7 @@ private Site modifySite(Site site, String networkId) { ? site.getPublisher() : Publisher.builder().build(); - final Publisher modifiedPublisher = modifiedPublisher(originalPublisher, networkId); + final Publisher modifiedPublisher = modifyPublisher(originalPublisher, networkId); return site.toBuilder().publisher(modifiedPublisher).build(); } @@ -135,12 +135,12 @@ private App modifyApp(App app, String networkId) { ? app.getPublisher() : Publisher.builder().build(); - final Publisher modifiedPublisher = modifiedPublisher(originalPublisher, networkId); + final Publisher modifiedPublisher = modifyPublisher(originalPublisher, networkId); return app.toBuilder().publisher(modifiedPublisher).build(); } - private Publisher modifiedPublisher(Publisher originalPublisher, String networkId) { + private Publisher modifyPublisher(Publisher originalPublisher, String networkId) { final ExtPublisher originalExt = originalPublisher.getExt(); final ExtPublisher modifiedExt = originalExt == null ? ExtPublisher.empty() @@ -161,6 +161,7 @@ private ObjectNode ensureParamsNode(ExtPublisher extPublisher) { } final ObjectNode paramsNode = mapper.mapper().createObjectNode(); extPublisher.addProperty("params", paramsNode); + return paramsNode; } @@ -169,36 +170,44 @@ private String replaceMacros(Site site, App app, String networkId, List { - final String domain = normalizeHostname(s.getDomain()); - return StringUtils.isNotEmpty(domain) ? domain : normalizeHostname(s.getPage()); - }) + if (site == null) { + return null; + } + + return Optional.of(site) + .map(Site::getDomain) + .map(SparteoUtil::normalizeHostname) .filter(StringUtils::isNotEmpty) - .orElse(site != null ? UNKNOWN_VALUE : null); + .or(() -> Optional.ofNullable(site.getPage()) + .map(SparteoUtil::normalizeHostname) + .filter(StringUtils::isNotEmpty)) + .orElse(UNKNOWN_VALUE); } private String resolveAppDomain(App app) { - return Optional.ofNullable(app) + if (app == null) { + return null; + } + + return Optional.of(app) .map(App::getDomain) - .map(SparteoBidder::normalizeHostname) + .map(SparteoUtil::normalizeHostname) .filter(StringUtils::isNotEmpty) - .orElse(app != null ? UNKNOWN_VALUE : null); + .orElse(UNKNOWN_VALUE); } private String resolveBundle(App app) { @@ -207,41 +216,12 @@ private String resolveBundle(App app) { } return Optional.ofNullable(app.getBundle()) - .filter(rawBundle -> !rawBundle.isBlank()) + .filter(StringUtils::isNotBlank) .map(String::trim) .filter(bundle -> !"null".equalsIgnoreCase(bundle)) .orElse(UNKNOWN_VALUE); } - public static String normalizeHostname(String host) { - String h = StringUtils.trimToEmpty(host); - if (h.isEmpty()) { - return ""; - } - - String hostname = null; - try { - hostname = new URI(h).getHost(); - } catch (URISyntaxException e) { - } - - if (StringUtils.isNotEmpty(hostname)) { - h = hostname; - } else { - if (h.contains(":")) { - h = StringUtils.substringBefore(h, ":"); - } else { - h = StringUtils.substringBefore(h, "/"); - } - } - - h = h.toLowerCase(); - h = StringUtils.removeStart(h, "www."); - h = StringUtils.removeEnd(h, "."); - - return "null".equals(h) ? "" : h; - } - private String resolveEndpoint(String siteDomain, String appDomain, String networkId, String bundle) { final String baseUrl = endpointUrl.replace(NETWORK_ID_MACRO, StringUtils.defaultString(networkId)); diff --git a/src/main/java/org/prebid/server/bidder/sparteo/util/SparteoUtil.java b/src/main/java/org/prebid/server/bidder/sparteo/util/SparteoUtil.java new file mode 100644 index 00000000000..bcb098f687d --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/sparteo/util/SparteoUtil.java @@ -0,0 +1,41 @@ +package org.prebid.server.bidder.sparteo.util; + +import org.apache.commons.lang3.StringUtils; + +import java.net.URI; +import java.net.URISyntaxException; + +public final class SparteoUtil { + + private SparteoUtil() { + } + + public static String normalizeHostname(String host) { + String h = StringUtils.trimToEmpty(host); + if (h.isEmpty()) { + return ""; + } + + String hostname = null; + try { + hostname = new URI(h).getHost(); + } catch (URISyntaxException e) { + } + + if (StringUtils.isNotEmpty(hostname)) { + h = hostname; + } else { + if (h.contains(":")) { + h = StringUtils.substringBefore(h, ":"); + } else { + h = StringUtils.substringBefore(h, "/"); + } + } + + h = h.toLowerCase(); + h = StringUtils.removeStart(h, "www."); + h = StringUtils.removeEnd(h, "."); + + return "null".equals(h) ? "" : h; + } +} diff --git a/src/main/resources/bidder-config/sparteo.yaml b/src/main/resources/bidder-config/sparteo.yaml index 89f1de2517f..d092d78db3c 100644 --- a/src/main/resources/bidder-config/sparteo.yaml +++ b/src/main/resources/bidder-config/sparteo.yaml @@ -16,5 +16,5 @@ adapters: usersync: cookie-family-name: sparteo iframe: - url: "https://sync.sparteo.com/s2s_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect_url={{redirect_url}}" + url: "https://sync.sparteo.com/s2s_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&&gpp_sid={{gpp_sid}}&redirect_url={{redirect_url}}" support-cors: true diff --git a/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java index 6d961a8bddd..f8ac6fba75e 100644 --- a/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java @@ -87,7 +87,7 @@ public void makeHttpRequestsShouldSetNetworkIdOnSitePublisherExtWhenPresentInImp .extracting(BidRequest::getSite) .extracting(Site::getPublisher) .extracting(Publisher::getExt) - .extracting(ext -> ((ExtPublisher) ext).getProperties().get("params").get("networkId").asText()) + .extracting(ext -> ext.getProperties().get("params").get("networkId").asText()) .containsExactly("testNetworkId"); } @@ -114,7 +114,7 @@ public void makeHttpRequestsShouldUseFirstNetworkIdWhenMultipleImpsDefineIt() { .extracting(BidRequest::getSite) .extracting(Site::getPublisher) .extracting(Publisher::getExt) - .extracting(ext -> ((ExtPublisher) ext).getProperties().get("params").get("networkId").asText()) + .extracting(ext -> ext.getProperties().get("params").get("networkId").asText()) .containsExactly("id1"); } @@ -182,7 +182,7 @@ public void makeHttpRequestsShouldHandleRequestWithoutSiteOrPublisher() { .extracting(BidRequest::getSite) .extracting(Site::getPublisher) .extracting(Publisher::getExt) - .extracting(ext -> ((ExtPublisher) ext).getProperties().get("params").get("networkId").asText()) + .extracting(ext -> ext.getProperties().get("params").get("networkId").asText()) .containsExactly("testNetworkId"); } @@ -211,7 +211,7 @@ public void makeHttpRequestsShouldMergeNetworkIdIntoExistingPublisherExtParams() .extracting(BidRequest::getSite) .extracting(Site::getPublisher) .extracting(Publisher::getExt) - .extracting(ext -> ((ExtPublisher) ext).getProperties().get("params")) + .extracting(ext -> ext.getProperties().get("params")) .allSatisfy(params -> { assertThat(params.get("networkId").asText()).isEqualTo("testNetworkId"); assertThat(params.get("existingParam").asText()).isEqualTo("existingValue"); @@ -244,7 +244,7 @@ public void makeHttpRequestsShouldAddParamsToPublisherExtWhenExtExistsWithoutPar .extracting(BidRequest::getSite) .extracting(Site::getPublisher) .extracting(Publisher::getExt) - .extracting(ext -> ((ExtPublisher) ext).getProperties()) + .extracting(ext -> ext.getProperties()) .allSatisfy(properties -> { assertThat(properties.get("params").get("networkId").asText()).isEqualTo("testNetworkId"); assertThat(properties.get("otherField").asText()).isEqualTo("otherValue"); @@ -265,356 +265,430 @@ public void makeHttpRequestsShouldReturnEmptyResultWhenRequestHasNoImps() { } @Test - public void makeBidsShouldReturnErrorWhenResponseStatusIs204() { + public void makeHttpRequestsShouldAppendSiteDomainAndNetworkIdAsQueryParams() { // given - final BidderCall httpCall = BidderCall.succeededHttp( - HttpRequest.builder().payload(givenBidRequest()).build(), - HttpResponse.of(204, null, ""), - null); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r.site(Site.builder() + .domain("dev.sparteo.com") + .publisher(Publisher.builder().build()) + .build()), + givenImp(i -> i.ext(impExt))); // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> { - assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); - assertThat(error.getMessage()) - .startsWith("Failed to decode: No content to map due to end-of-input"); - }); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); } @Test - public void makeBidsShouldReturnErrorWhenResponseStatusIsNot200Or204() { + public void makeHttpRequestsShouldAppendSitePageDomainAndNetworkIdAsQueryParams() { // given - final BidderCall httpCall = BidderCall.succeededHttp( - HttpRequest.builder().payload(givenBidRequest()).build(), - HttpResponse.of(400, null, "Bad Request"), - null); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r.site(Site.builder() + .domain(null) + .page("https://www.dev.sparteo.com:3000/p") + .publisher(Publisher.builder().build()) + .build()), + givenImp(i -> i.ext(impExt))); // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> { - assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); - assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'Bad'"); - }); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); } @Test - public void makeBidsShouldReturnErrorWhenResponseBodyIsInvalidJson() { + public void makeHttpRequestsShouldUseSiteDomainWhenPublisherDomainIsMissing() { // given - final BidderCall httpCall = BidderCall.succeededHttp( - HttpRequest.builder().payload(givenBidRequest()).build(), - HttpResponse.of(200, null, "invalid_json"), - null); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r.site(Site.builder() + .domain("dev.sparteo.com") + .publisher(Publisher.builder().build()) + .build()), + givenImp(i -> i.ext(impExt))); // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> { - assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); - assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid_json'"); - }); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); } @Test - public void makeBidsShouldReturnEmptyResultWhenBidResponseIsNull() throws JsonProcessingException { + public void makeHttpRequestsShouldPreferSiteDomainOverPublisherDomain() { // given - final BidderCall httpCall = BidderCall.succeededHttp( - HttpRequest.builder().payload(givenBidRequest()).build(), - HttpResponse.of(400, null, "null"), - null); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r.site(Site.builder() + .domain("site.sparteo.com") + .publisher(Publisher.builder().domain("dev.sparteo.com").build()) + .build()), + givenImp(i -> i.ext(impExt))); // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&site_domain=site.sparteo.com"); } @Test - public void makeBidsShouldReturnEmptyResultWhenBidResponseHasNoSeatBids() throws JsonProcessingException { + public void makeHttpRequestsShouldUseAppDomainWhenNoSite() { // given - final BidResponse bidResponse = BidResponse.builder().seatbid(Collections.emptyList()).build(); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("com.sparteo.app") + .publisher(Publisher.builder().id("p1").build()) + .build()), + givenImp(i -> i.ext(impExt))); // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertMissingBundleWarning(result); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=com.sparteo.app&bundle=unknown"); } @Test - public void makeBidsShouldReturnBannerBidWhenMediaTypeIsBanner() throws JsonProcessingException { + public void makeHttpRequestsShouldSetNetworkIdOnAppPublisherExtWhenNoSite() { // given - final Bid bid = givenBid(builder -> builder.impid("imp1").price(BigDecimal.valueOf(1.23)).adm("adm-banner"), - BidType.banner.getName()); - final BidResponse bidResponse = givenBidResponse(bid, "EUR"); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("com.sparteo.app") + .publisher(Publisher.builder().build()) + .build()), + givenImp(i -> i.ext(impExt))); // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .containsExactly(BidderBid.of(bid.toBuilder().mtype(1).build(), BidType.banner, "EUR")); + assertMissingBundleWarning(result); + final BidRequest out = result.getValue().get(0).getPayload(); + assertThat(out.getApp()).isNotNull(); + assertThat(out.getApp().getPublisher()).isNotNull(); + + final ExtPublisher ext = out.getApp().getPublisher().getExt(); + assertThat(ext).isNotNull(); + assertThat(ext.getProperties()).containsKey("params"); + assertThat(ext.getProperties().get("params").get("networkId").asText()).isEqualTo("networkId"); } @Test - public void makeBidsShouldReturnVideoBidWhenMediaTypeIsVideo() throws JsonProcessingException { + public void makeHttpRequestsShouldCreateAppPublisherWhenMissingAndSetNetworkId() { // given - final Bid bid = givenBid(builder -> - builder.impid("imp2").price(BigDecimal.valueOf(2.34)).adm("adm-video"), - BidType.video.getName()); - final BidResponse bidResponse = givenBidResponse(bid, "EUR"); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("com.sparteo.app") + .publisher(null) + .build()), + givenImp(i -> i.ext(impExt))); // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .containsExactly(BidderBid.of(bid.toBuilder().mtype(2).build(), BidType.video, "EUR")); + assertMissingBundleWarning(result); + final BidRequest out = result.getValue().get(0).getPayload(); + assertThat(out.getApp()).isNotNull(); + assertThat(out.getApp().getPublisher()).isNotNull(); + + final ExtPublisher ext = out.getApp().getPublisher().getExt(); + assertThat(ext).isNotNull(); + assertThat(ext.getProperties()).containsKey("params"); + assertThat(ext.getProperties().get("params").get("networkId").asText()).isEqualTo("networkId"); } @Test - public void makeBidsShouldReturnNativeBidWhenMediaTypeIsNative() throws JsonProcessingException { + public void makeHttpRequestsShouldAppendBundleWhenAppBundlePresent() { // given - final Bid bid = givenBid(builder -> - builder.impid("imp3").price(BigDecimal.valueOf(3.45)).adm("adm-native"), - BidType.xNative.getName()); - final BidResponse bidResponse = givenBidResponse(bid, "EUR"); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("dev.sparteo.com") + .bundle("com.sparteo.app") + .publisher(Publisher.builder().id("p1").build()) + .build()), + givenImp(i -> i.ext(impExt))); // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsExactly(BidderBid.of(bid.toBuilder().mtype(4).build(), BidType.xNative, "EUR")); + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=com.sparteo.app"); } @Test - public void makeBidsShouldReturnErrorForUnsupportedMediaTypeAndProcessOthers() throws JsonProcessingException { + public void makeHttpRequestsShouldNotAppendBundleWhenNoAppBundle() { // given - final Bid audioBid = givenBid(builder -> - builder.impid("impAudio").price(BigDecimal.ONE), - BidType.audio.getName()); - final Bid bannerBid = givenBid(builder -> - builder.impid("impBanner").price(BigDecimal.valueOf(2.0)), - BidType.banner.getName()); - final BidResponse bidResponse = givenBidResponse(List.of(audioBid, bannerBid), "EUR"); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(Site.builder().domain("dev.sparteo.com").publisher(Publisher.builder().build()).build()) + .app(null), + givenImp(i -> i.ext(impExt))); // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()) - .extracting(BidderError::getMessage) - .containsExactly("Audio bid type not supported by this adapter for impression id: impAudio"); - + assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .extracting(BidderBid::getBid) - .containsExactly(bannerBid.toBuilder().mtype(1).build()); + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&site_domain=dev.sparteo.com"); } @Test - public void makeBidsShouldReturnErrorWhenBidExtIsNull() throws JsonProcessingException { + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppPresentButBundleMissing() { // given - final Bid bid = givenBid(builder -> builder.impid("imp1").ext(null), null); - final BidResponse bidResponse = givenBidResponse(bid, "USD"); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); - - // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()) - .extracting(BidderError::getMessage) - .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("dev.sparteo.com") + .publisher(Publisher.builder().id("p1").build()) + .build()), + givenImp(i -> i.ext(impExt))); + + // when + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + + // then + assertMissingBundleWarning(result); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=unknown"); } @Test - public void makeBidsShouldReturnErrorWhenPrebidIsMissingInBidExt() throws JsonProcessingException { + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsNull() { + testAppendUnknownBundle(null); + } + + @Test + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsEmpty() { + testAppendUnknownBundle(""); + } + + @Test + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsBlank() { + testAppendUnknownBundle(" "); + } + + @Test + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsLiteralNull() { + testAppendUnknownBundle("null"); + } + + @Test + public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsLiteralNullIgnoreCase() { + testAppendUnknownBundle("NuLl"); + } + + private void testAppendUnknownBundle(String rawBundle) { // given - final Bid bid = givenBid(builder -> builder.impid("imp1").ext(mapper.createObjectNode()), null); - final BidResponse bidResponse = givenBidResponse(bid, "USD"); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + + final BidRequest bidRequest = givenBidRequest( + r -> r + .site(null) + .app(App.builder() + .domain("dev.sparteo.com") + .bundle(rawBundle) + .publisher(Publisher.builder().id("p1").build()) + .build()), + givenImp(i -> i.ext(impExt))); // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()) - .extracting(BidderError::getMessage) - .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); + assertMissingBundleWarning(result); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=unknown"); } @Test - public void makeBidsShouldReturnErrorWhenPrebidTypeIsMissingInBidExt() throws JsonProcessingException { + public void makeHttpRequestsShouldWarnAndSetUnknownAppDomainWhenAppDomainMissing() { // given - final Bid bid = givenBid(builder -> builder.impid("imp1").ext(createBidExtWithEmptyPrebid()), null); - final BidResponse bidResponse = givenBidResponse(bid, "USD"); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); + final ObjectNode impExt = mapper.createObjectNode(); + impExt.set("bidder", mapper.createObjectNode().put("networkId", "nid")); + + final BidRequest bidRequest = givenBidRequest( + r -> r.site(null) + .app(App.builder() + .domain(null) + .bundle("com.example.bundle") + .publisher(Publisher.builder().build()) + .build()), + givenImp(i -> i.ext(impExt))); // when - final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); + final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()) - .extracting(BidderError::getMessage) - .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly( + "https://test.sparteo.com/endpoint?network_id=nid&app_domain=unknown&bundle=com.example.bundle"); } @Test - public void makeBidsShouldReturnErrorWhenPrebidCannotBeParsed() throws JsonProcessingException { + public void makeBidsShouldReturnErrorWhenResponseStatusIs204() { // given - final ObjectNode malformedExt = mapper.createObjectNode(); - malformedExt.putArray("prebid"); - final Bid bid = givenBid(builder -> builder.impid("imp1").ext(malformedExt), null); - final BidResponse bidResponse = givenBidResponse(bid, "USD"); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = BidderCall.succeededHttp( + HttpRequest.builder().payload(givenBidRequest()).build(), + HttpResponse.of(204, null, ""), + null); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()) + .startsWith("Failed to decode: No content to map due to end-of-input"); + }); assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()) - .extracting(BidderError::getMessage) - .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); } @Test - public void makeBidsShouldReturnErrorWhenPrebidTypeIsUnsupported() throws JsonProcessingException { + public void makeBidsShouldReturnErrorWhenResponseStatusIsNot200Or204() { // given - final Bid bid = givenBid(builder -> builder.impid("imp1"), "unknown-type"); - final BidResponse bidResponse = givenBidResponse(bid, "USD"); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = BidderCall.succeededHttp( + HttpRequest.builder().payload(givenBidRequest()).build(), + HttpResponse.of(400, null, "Bad Request"), + null); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()) - .extracting(BidderError::getMessage) - .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'Bad'"); + }); } @Test - public void makeBidsShouldProcessValidBidsWhenSeatBidContainsNulls() throws JsonProcessingException { + public void makeBidsShouldReturnErrorWhenResponseBodyIsInvalidJson() { // given - final Bid validBid = givenBid(builder -> - builder.impid("validImp").price(BigDecimal.ONE), - BidType.banner.getName()); - final List bids = new ArrayList<>(); - bids.add(null); - bids.add(validBid); - - final BidResponse bidResponse = givenBidResponse(bids, "USD"); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = BidderCall.succeededHttp( + HttpRequest.builder().payload(givenBidRequest()).build(), + HttpResponse.of(200, null, "invalid_json"), + null); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .extracting(BidderBid::getBid) - .containsExactly(validBid.toBuilder().mtype(1).build()); + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid_json'"); + }); } @Test - public void makeBidsShouldCorrectlyProcessMultipleBidsAndSeatBids() throws JsonProcessingException { + public void makeBidsShouldReturnEmptyResultWhenBidResponseIsNull() throws JsonProcessingException { // given - final Bid bid1 = givenBid(builder -> - builder.impid("imp1").price(BigDecimal.valueOf(1.0)), - BidType.banner.getName()); - final Bid bid2 = givenBid(builder -> - builder.impid("imp2").price(BigDecimal.valueOf(2.0)), - BidType.video.getName()); - final Bid bid3 = givenBid(builder -> - builder.impid("imp3").price(BigDecimal.valueOf(3.0)), - BidType.xNative.getName()); - - final SeatBid seatBid1 = SeatBid.builder().bid(asList(bid1, bid2)).build(); - final SeatBid seatBid2 = SeatBid.builder().bid(singletonList(bid3)).build(); - - final BidResponse bidResponse = BidResponse.builder().cur("USD").seatbid(asList(seatBid1, seatBid2)).build(); - final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); + final BidderCall httpCall = BidderCall.succeededHttp( + HttpRequest.builder().payload(givenBidRequest()).build(), + HttpResponse.of(400, null, "null"), + null); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .hasSize(3) - .extracting((BidderBid bidderBid) -> bidderBid.getBid().getImpid(), BidderBid::getType) - .containsExactlyInAnyOrder( - tuple("imp1", BidType.banner), - tuple("imp2", BidType.video), - tuple("imp3", BidType.xNative)); + assertThat(result.getValue()).isEmpty(); } @Test - public void makeBidsShouldReturnErrorWhenPrebidExtIsNullNode() throws JsonProcessingException { + public void makeBidsShouldReturnEmptyResultWhenBidResponseHasNoSeatBids() throws JsonProcessingException { // given - final ObjectNode bidExtWithNullPrebid = mapper.createObjectNode(); - bidExtWithNullPrebid.set("prebid", NullNode.getInstance()); - - final Bid bid = givenBid(builder -> builder.impid("imp1").ext(bidExtWithNullPrebid), null); - final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidResponse bidResponse = BidResponse.builder().seatbid(Collections.emptyList()).build(); final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then + assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()) - .extracting(BidderError::getMessage) - .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); } @Test - public void makeBidsShouldProcessValidSeatBidsWhenResponseContainsNulls() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBidWhenMediaTypeIsBanner() throws JsonProcessingException { // given - final Bid validBid1 = givenBid(builder -> - builder.impid("validImp1").price(BigDecimal.TEN), - BidType.banner.getName()); - final Bid validBid2 = givenBid(builder -> - builder.impid("validImp2").price(BigDecimal.ONE), + final Bid bid = givenBid(builder -> builder.impid("imp1").price(BigDecimal.valueOf(1.23)).adm("adm-banner"), BidType.banner.getName()); - - final SeatBid validSeatBid1 = SeatBid.builder().bid(singletonList(validBid1)).build(); - final SeatBid validSeatBid2 = SeatBid.builder().bid(singletonList(validBid2)).build(); - - final List seatBidsWithNull = new ArrayList<>(); - seatBidsWithNull.add(validSeatBid1); - seatBidsWithNull.add(null); - seatBidsWithNull.add(validSeatBid2); - - final BidResponse bidResponse = BidResponse.builder().cur("USD").seatbid(seatBidsWithNull).build(); + final BidResponse bidResponse = givenBidResponse(bid, "EUR"); final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when @@ -623,17 +697,16 @@ public void makeBidsShouldProcessValidSeatBidsWhenResponseContainsNulls() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .extracting(BidderBid::getBid) - .extracting(Bid::getImpid) - .containsExactlyInAnyOrder("validImp1", "validImp2"); + .containsExactly(BidderBid.of(bid.toBuilder().mtype(1).build(), BidType.banner, "EUR")); } @Test - public void makeBidsShouldReturnEmptyResultWhenSeatBidHasNullBidList() throws JsonProcessingException { + public void makeBidsShouldReturnVideoBidWhenMediaTypeIsVideo() throws JsonProcessingException { // given - final SeatBid seatBidWithNullBids = SeatBid.builder().bid(null).build(); - final BidResponse bidResponse = - BidResponse.builder().cur("USD").seatbid(singletonList(seatBidWithNullBids)).build(); + final Bid bid = givenBid(builder -> + builder.impid("imp2").price(BigDecimal.valueOf(2.34)).adm("adm-video"), + BidType.video.getName()); + final BidResponse bidResponse = givenBidResponse(bid, "EUR"); final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when @@ -641,15 +714,17 @@ public void makeBidsShouldReturnEmptyResultWhenSeatBidHasNullBidList() throws Js // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(bid.toBuilder().mtype(2).build(), BidType.video, "EUR")); } @Test - public void makeBidsShouldReturnEmptyResultWhenSeatBidHasEmptyBidList() throws JsonProcessingException { + public void makeBidsShouldReturnNativeBidWhenMediaTypeIsNative() throws JsonProcessingException { // given - final SeatBid seatBidWithEmptyBids = SeatBid.builder().bid(Collections.emptyList()).build(); - final BidResponse bidResponse = - BidResponse.builder().cur("USD").seatbid(singletonList(seatBidWithEmptyBids)).build(); + final Bid bid = givenBid(builder -> + builder.impid("imp3").price(BigDecimal.valueOf(3.45)).adm("adm-native"), + BidType.xNative.getName()); + final BidResponse bidResponse = givenBidResponse(bid, "EUR"); final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when @@ -657,374 +732,260 @@ public void makeBidsShouldReturnEmptyResultWhenSeatBidHasEmptyBidList() throws J // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(bid.toBuilder().mtype(4).build(), BidType.xNative, "EUR")); } @Test - public void makeHttpRequestsShouldAppendSiteDomainAndNetworkIdAsQueryParams() { + public void makeBidsShouldReturnErrorForUnsupportedMediaTypeAndProcessOthers() throws JsonProcessingException { // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); - - final BidRequest bidRequest = givenBidRequest( - r -> r.site(Site.builder() - .domain("dev.sparteo.com") - .publisher(Publisher.builder().build()) - .build()), - givenImp(i -> i.ext(impExt))); + final Bid audioBid = givenBid(builder -> + builder.impid("impAudio").price(BigDecimal.ONE), + BidType.audio.getName()); + final Bid bannerBid = givenBid(builder -> + builder.impid("impBanner").price(BigDecimal.valueOf(2.0)), + BidType.banner.getName()); + final BidResponse bidResponse = givenBidResponse(List.of(audioBid, bannerBid), "EUR"); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1); - assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly("https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); - } - - @Test - public void makeHttpRequestsShouldAppendSitePageDomainAndNetworkIdAsQueryParams() { - // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); - - final BidRequest bidRequest = givenBidRequest( - r -> r.site(Site.builder() - .domain(null) - .page("https://www.dev.sparteo.com:3000/p") - .publisher(Publisher.builder().build()) - .build()), - givenImp(i -> i.ext(impExt))); - - // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Audio bid type not supported by this adapter for impression id: impAudio"); - // then - assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly("https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); + .extracting(BidderBid::getBid) + .containsExactly(bannerBid.toBuilder().mtype(1).build()); } @Test - public void makeHttpRequestsShouldUseSiteDomainWhenPublisherDomainIsMissing() { + public void makeBidsShouldReturnErrorWhenBidExtIsNull() throws JsonProcessingException { // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "testNetworkId")); - - final BidRequest bidRequest = givenBidRequest( - r -> r.site(Site.builder() - .domain("dev.sparteo.com") - .publisher(Publisher.builder().build()) - .build()), - givenImp(i -> i.ext(impExt))); + final Bid bid = givenBid(builder -> builder.impid("imp1").ext(null), null); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .extracting(HttpRequest::getUri) - .containsExactly("https://test.sparteo.com/endpoint?network_id=testNetworkId&site_domain=dev.sparteo.com"); + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); } @Test - public void makeHttpRequestsShouldPreferSiteDomainOverPublisherDomain() { + public void makeBidsShouldReturnErrorWhenPrebidIsMissingInBidExt() throws JsonProcessingException { // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); - - final BidRequest bidRequest = givenBidRequest( - r -> r.site(Site.builder() - .domain("site.sparteo.com") - .publisher(Publisher.builder().domain("dev.sparteo.com").build()) - .build()), - givenImp(i -> i.ext(impExt))); + final Bid bid = givenBid(builder -> builder.impid("imp1").ext(mapper.createObjectNode()), null); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&site_domain=site.sparteo.com"); + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); } @Test - public void makeHttpRequestsShouldUseAppDomainWhenNoSite() { + public void makeBidsShouldReturnErrorWhenPrebidTypeIsMissingInBidExt() throws JsonProcessingException { // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); - - final BidRequest bidRequest = givenBidRequest( - r -> r - .site(null) - .app(App.builder() - .domain("com.sparteo.app") - .publisher(Publisher.builder().id("p1").build()) - .build()), - givenImp(i -> i.ext(impExt))); + final Bid bid = givenBid(builder -> builder.impid("imp1").ext(createBidExtWithEmptyPrebid()), null); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then - assertMissingBundleWarning(result); - assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=com.sparteo.app&bundle=unknown"); + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); } @Test - public void makeHttpRequestsShouldSetNetworkIdOnAppPublisherExtWhenNoSite() { + public void makeBidsShouldReturnErrorWhenPrebidCannotBeParsed() throws JsonProcessingException { // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); - - final BidRequest bidRequest = givenBidRequest( - r -> r - .site(null) - .app(App.builder() - .domain("com.sparteo.app") - .publisher(Publisher.builder().build()) - .build()), - givenImp(i -> i.ext(impExt))); + final ObjectNode malformedExt = mapper.createObjectNode(); + malformedExt.putArray("prebid"); + final Bid bid = givenBid(builder -> builder.impid("imp1").ext(malformedExt), null); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then - assertMissingBundleWarning(result); - final BidRequest out = result.getValue().get(0).getPayload(); - assertThat(out.getApp()).isNotNull(); - assertThat(out.getApp().getPublisher()).isNotNull(); - - final ExtPublisher ext = (ExtPublisher) out.getApp().getPublisher().getExt(); - assertThat(ext).isNotNull(); - assertThat(ext.getProperties()).containsKey("params"); - assertThat(ext.getProperties().get("params").get("networkId").asText()).isEqualTo("networkId"); + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); } @Test - public void makeHttpRequestsShouldCreateAppPublisherWhenMissingAndSetNetworkId() { + public void makeBidsShouldReturnErrorWhenPrebidTypeIsUnsupported() throws JsonProcessingException { // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); - - final BidRequest bidRequest = givenBidRequest( - r -> r - .site(null) - .app(App.builder() - .domain("com.sparteo.app") - .publisher(null) - .build()), - givenImp(i -> i.ext(impExt))); + final Bid bid = givenBid(builder -> builder.impid("imp1"), "unknown-type"); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then - assertMissingBundleWarning(result); - final BidRequest out = result.getValue().get(0).getPayload(); - assertThat(out.getApp()).isNotNull(); - assertThat(out.getApp().getPublisher()).isNotNull(); - - final ExtPublisher ext = (ExtPublisher) out.getApp().getPublisher().getExt(); - assertThat(ext).isNotNull(); - assertThat(ext.getProperties()).containsKey("params"); - assertThat(ext.getProperties().get("params").get("networkId").asText()).isEqualTo("networkId"); + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); } @Test - public void makeHttpRequestsShouldAppendBundleWhenAppBundlePresent() { + public void makeBidsShouldProcessValidBidsWhenSeatBidContainsNulls() throws JsonProcessingException { // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + final Bid validBid = givenBid(builder -> + builder.impid("validImp").price(BigDecimal.ONE), + BidType.banner.getName()); + final List bids = new ArrayList<>(); + bids.add(null); + bids.add(validBid); - final BidRequest bidRequest = givenBidRequest( - r -> r - .site(null) - .app(App.builder() - .domain("dev.sparteo.com") - .bundle("com.sparteo.app") - .publisher(Publisher.builder().id("p1").build()) - .build()), - givenImp(i -> i.ext(impExt))); + final BidResponse bidResponse = givenBidResponse(bids, "USD"); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=com.sparteo.app"); + .extracting(BidderBid::getBid) + .containsExactly(validBid.toBuilder().mtype(1).build()); } @Test - public void makeHttpRequestsShouldNotAppendBundleWhenNoAppBundle() { + public void makeBidsShouldCorrectlyProcessMultipleBidsAndSeatBids() throws JsonProcessingException { // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + final Bid bid1 = givenBid(builder -> + builder.impid("imp1").price(BigDecimal.valueOf(1.0)), + BidType.banner.getName()); + final Bid bid2 = givenBid(builder -> + builder.impid("imp2").price(BigDecimal.valueOf(2.0)), + BidType.video.getName()); + final Bid bid3 = givenBid(builder -> + builder.impid("imp3").price(BigDecimal.valueOf(3.0)), + BidType.xNative.getName()); - final BidRequest bidRequest = givenBidRequest( - r -> r - .site(Site.builder().domain("dev.sparteo.com").publisher(Publisher.builder().build()).build()) - .app(null), - givenImp(i -> i.ext(impExt))); + final SeatBid seatBid1 = SeatBid.builder().bid(asList(bid1, bid2)).build(); + final SeatBid seatBid2 = SeatBid.builder().bid(singletonList(bid3)).build(); + + final BidResponse bidResponse = BidResponse.builder().cur("USD").seatbid(asList(seatBid1, seatBid2)).build(); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&site_domain=dev.sparteo.com"); + .hasSize(3) + .extracting((BidderBid bidderBid) -> bidderBid.getBid().getImpid(), BidderBid::getType) + .containsExactlyInAnyOrder( + tuple("imp1", BidType.banner), + tuple("imp2", BidType.video), + tuple("imp3", BidType.xNative)); } @Test - public void makeHttpRequestsShouldAppendUnknownBundleWhenAppPresentButBundleMissing() { + public void makeBidsShouldReturnErrorWhenPrebidExtIsNullNode() throws JsonProcessingException { // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + final ObjectNode bidExtWithNullPrebid = mapper.createObjectNode(); + bidExtWithNullPrebid.set("prebid", NullNode.getInstance()); - final BidRequest bidRequest = givenBidRequest( - r -> r - .site(null) - .app(App.builder() - .domain("dev.sparteo.com") - .publisher(Publisher.builder().id("p1").build()) - .build()), - givenImp(i -> i.ext(impExt))); + final Bid bid = givenBid(builder -> builder.impid("imp1").ext(bidExtWithNullPrebid), null); + final BidResponse bidResponse = givenBidResponse(bid, "USD"); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then - assertMissingBundleWarning(result); - assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=unknown"); - } - - @Test - public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsNull() { - testAppendUnknownBundle(null); - } - - @Test - public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsEmpty() { - testAppendUnknownBundle(""); - } - - @Test - public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsBlank() { - testAppendUnknownBundle(" "); + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Failed to parse bid mediatype for impression \"imp1\""); } @Test - public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsLiteralNull() { - testAppendUnknownBundle("null"); - } + public void makeBidsShouldProcessValidSeatBidsWhenResponseContainsNulls() throws JsonProcessingException { + // given + final Bid validBid1 = givenBid(builder -> + builder.impid("validImp1").price(BigDecimal.TEN), + BidType.banner.getName()); + final Bid validBid2 = givenBid(builder -> + builder.impid("validImp2").price(BigDecimal.ONE), + BidType.banner.getName()); - @Test - public void makeHttpRequestsShouldAppendUnknownBundleWhenAppBundleIsLiteralNullIgnoreCase() { - testAppendUnknownBundle("NuLl"); - } + final SeatBid validSeatBid1 = SeatBid.builder().bid(singletonList(validBid1)).build(); + final SeatBid validSeatBid2 = SeatBid.builder().bid(singletonList(validBid2)).build(); - private void testAppendUnknownBundle(String rawBundle) { - // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "networkId")); + final List seatBidsWithNull = new ArrayList<>(); + seatBidsWithNull.add(validSeatBid1); + seatBidsWithNull.add(null); + seatBidsWithNull.add(validSeatBid2); - final BidRequest bidRequest = givenBidRequest( - r -> r - .site(null) - .app(App.builder() - .domain("dev.sparteo.com") - .bundle(rawBundle) - .publisher(Publisher.builder().id("p1").build()) - .build()), - givenImp(i -> i.ext(impExt))); + final BidResponse bidResponse = BidResponse.builder().cur("USD").seatbid(seatBidsWithNull).build(); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then - assertMissingBundleWarning(result); + assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly("https://test.sparteo.com/endpoint?network_id=networkId&app_domain=dev.sparteo.com&bundle=unknown"); + .extracting(BidderBid::getBid) + .extracting(Bid::getImpid) + .containsExactlyInAnyOrder("validImp1", "validImp2"); } @Test - public void makeHttpRequestsShouldWarnAndSetUnknownAppDomainWhenAppDomainMissing() { + public void makeBidsShouldReturnEmptyResultWhenSeatBidHasNullBidList() throws JsonProcessingException { // given - final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.createObjectNode().put("networkId", "nid")); - - final BidRequest bidRequest = givenBidRequest( - r -> r.site(null) - .app(App.builder() - .domain(null) - .bundle("com.example.bundle") - .publisher(Publisher.builder().build()) - .build()), - givenImp(i -> i.ext(impExt))); + final SeatBid seatBidWithNullBids = SeatBid.builder().bid(null).build(); + final BidResponse bidResponse = + BidResponse.builder().cur("USD").seatbid(singletonList(seatBidWithNullBids)).build(); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); // when - final Result>> result = sparteoBidder.makeHttpRequests(bidRequest); + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly( - "https://test.sparteo.com/endpoint?network_id=nid&app_domain=unknown&bundle=com.example.bundle"); + assertThat(result.getValue()).isEmpty(); } @Test - public void normalizeHostnameShouldReturnBaseDomain() { - final String base = "dev.sparteo.com"; - - assertThat(SparteoBidder.normalizeHostname(base)).isEqualTo(base); - - assertThat(SparteoBidder.normalizeHostname("DeV.SpArTeO.CoM")).isEqualTo(base); - assertThat(SparteoBidder.normalizeHostname(" " + base + " ")).isEqualTo(base); - - assertThat(SparteoBidder.normalizeHostname("www." + base)).isEqualTo(base); - assertThat(SparteoBidder.normalizeHostname("WWW." + base)).isEqualTo(base); - - assertThat(SparteoBidder.normalizeHostname(base + ".")).isEqualTo(base); - assertThat(SparteoBidder.normalizeHostname(base + "..")).isEqualTo(base + "."); - - assertThat(SparteoBidder.normalizeHostname("null")).isEqualTo(""); - assertThat(SparteoBidder.normalizeHostname("NuLl")).isEqualTo(""); - - assertThat(SparteoBidder.normalizeHostname("")).isEqualTo(""); - assertThat(SparteoBidder.normalizeHostname(" ")).isEqualTo(""); - - assertThat(SparteoBidder.normalizeHostname("www2." + base)).isEqualTo("www2." + base); - - assertThat(SparteoBidder.normalizeHostname("www." + base + ":8080")).isEqualTo(base); - assertThat(SparteoBidder.normalizeHostname("DEV.SPARTEO.COM:443")).isEqualTo(base); - assertThat(SparteoBidder.normalizeHostname(base + ".:8443")).isEqualTo(base); - - assertThat(SparteoBidder.normalizeHostname(base + "/some/path?x=1")).isEqualTo(base); - assertThat(SparteoBidder.normalizeHostname("www." + base + "/p")).isEqualTo(base); - - assertThat(SparteoBidder.normalizeHostname(base + ":8080/p?q=1")).isEqualTo(base); - assertThat(SparteoBidder.normalizeHostname("www." + base + ":3000/some/path")).isEqualTo(base); + public void makeBidsShouldReturnEmptyResultWhenSeatBidHasEmptyBidList() throws JsonProcessingException { + // given + final SeatBid seatBidWithEmptyBids = SeatBid.builder().bid(Collections.emptyList()).build(); + final BidResponse bidResponse = + BidResponse.builder().cur("USD").seatbid(singletonList(seatBidWithEmptyBids)).build(); + final BidderCall httpCall = givenHttpCall(givenBidRequest(), bidResponse); - assertThat(SparteoBidder.normalizeHostname("https://www." + base + "/x")).isEqualTo(base); - assertThat(SparteoBidder.normalizeHostname("http://WWW." + base + ":8080/abc")).isEqualTo(base); + // when + final Result> result = sparteoBidder.makeBids(httpCall, givenBidRequest()); - assertThat(SparteoBidder.normalizeHostname(" https://www." + base + ":3000/x ")).isEqualTo(base); + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); } private BidRequest givenBidRequest(UnaryOperator customizer, Imp... imps) { diff --git a/src/test/java/org/prebid/server/bidder/sparteo/SparteoUtilTest.java b/src/test/java/org/prebid/server/bidder/sparteo/SparteoUtilTest.java new file mode 100644 index 00000000000..d735c0d7e89 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/sparteo/SparteoUtilTest.java @@ -0,0 +1,49 @@ +package org.prebid.server.bidder.sparteo; + +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.sparteo.util.SparteoUtil; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SparteoUtilTest extends VertxTest { + + @Test + public void normalizeHostnameShouldReturnBaseDomain() { + final String base = "dev.sparteo.com"; + + assertThat(SparteoUtil.normalizeHostname(base)).isEqualTo(base); + + assertThat(SparteoUtil.normalizeHostname("DeV.SpArTeO.CoM")).isEqualTo(base); + assertThat(SparteoUtil.normalizeHostname(" " + base + " ")).isEqualTo(base); + + assertThat(SparteoUtil.normalizeHostname("www." + base)).isEqualTo(base); + assertThat(SparteoUtil.normalizeHostname("WWW." + base)).isEqualTo(base); + + assertThat(SparteoUtil.normalizeHostname(base + ".")).isEqualTo(base); + assertThat(SparteoUtil.normalizeHostname(base + "..")).isEqualTo(base + "."); + + assertThat(SparteoUtil.normalizeHostname("null")).isEqualTo(""); + assertThat(SparteoUtil.normalizeHostname("NuLl")).isEqualTo(""); + + assertThat(SparteoUtil.normalizeHostname("")).isEqualTo(""); + assertThat(SparteoUtil.normalizeHostname(" ")).isEqualTo(""); + + assertThat(SparteoUtil.normalizeHostname("www2." + base)).isEqualTo("www2." + base); + + assertThat(SparteoUtil.normalizeHostname("www." + base + ":8080")).isEqualTo(base); + assertThat(SparteoUtil.normalizeHostname("DEV.SPARTEO.COM:443")).isEqualTo(base); + assertThat(SparteoUtil.normalizeHostname(base + ".:8443")).isEqualTo(base); + + assertThat(SparteoUtil.normalizeHostname(base + "/some/path?x=1")).isEqualTo(base); + assertThat(SparteoUtil.normalizeHostname("www." + base + "/p")).isEqualTo(base); + + assertThat(SparteoUtil.normalizeHostname(base + ":8080/p?q=1")).isEqualTo(base); + assertThat(SparteoUtil.normalizeHostname("www." + base + ":3000/some/path")).isEqualTo(base); + + assertThat(SparteoUtil.normalizeHostname("https://www." + base + "/x")).isEqualTo(base); + assertThat(SparteoUtil.normalizeHostname("http://WWW." + base + ":8080/abc")).isEqualTo(base); + + assertThat(SparteoUtil.normalizeHostname(" https://www." + base + ":3000/x ")).isEqualTo(base); + } +} From 8a973ddd05979d8ca9c4417732723673740eade0 Mon Sep 17 00:00:00 2001 From: Thomas Sormonte Date: Wed, 12 Nov 2025 17:13:23 +0100 Subject: [PATCH 4/6] AntoxaAntoxic code review 3 --- .../org/prebid/server/bidder/sparteo/SparteoBidder.java | 8 ++++---- src/main/resources/bidder-config/sparteo.yaml | 2 +- .../prebid/server/bidder/sparteo/SparteoBidderTest.java | 2 +- .../org/prebid/server/it/test-application.properties | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java index 0e735a346b1..fa222204caf 100644 --- a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java +++ b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java @@ -43,7 +43,6 @@ public class SparteoBidder implements Bidder { - private static final String NETWORK_ID_MACRO = "{{NetworkId}}"; private static final String UNKNOWN_VALUE = "unknown"; private static final TypeReference> TYPE_REFERENCE = @@ -223,10 +222,11 @@ private String resolveBundle(App app) { } private String resolveEndpoint(String siteDomain, String appDomain, String networkId, String bundle) { - final String baseUrl = endpointUrl.replace(NETWORK_ID_MACRO, StringUtils.defaultString(networkId)); - try { - final URIBuilder uriBuilder = new URIBuilder(baseUrl); + final URIBuilder uriBuilder = new URIBuilder(endpointUrl); + if (StringUtils.isNotBlank(networkId)) { + uriBuilder.addParameter("network_id", networkId); + } if (StringUtils.isNotBlank(siteDomain)) { uriBuilder.addParameter("site_domain", siteDomain); } diff --git a/src/main/resources/bidder-config/sparteo.yaml b/src/main/resources/bidder-config/sparteo.yaml index d092d78db3c..5dd9dd19b1d 100644 --- a/src/main/resources/bidder-config/sparteo.yaml +++ b/src/main/resources/bidder-config/sparteo.yaml @@ -1,6 +1,6 @@ adapters: sparteo: - endpoint: https://bid.sparteo.com/s2s-auction?network_id={{NetworkId}} + endpoint: https://bid.sparteo.com/s2s-auction meta-info: maintainer-email: prebid@sparteo.com app-media-types: diff --git a/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java index f8ac6fba75e..3eacb6e48fd 100644 --- a/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java @@ -40,7 +40,7 @@ public class SparteoBidderTest extends VertxTest { private static final String ENDPOINT_URL = - "https://test.sparteo.com/endpoint?network_id={{NetworkId}}"; + "https://test.sparteo.com/endpoint"; private final SparteoBidder sparteoBidder = new SparteoBidder(ENDPOINT_URL, jacksonMapper); @Test 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 cab7dd7dba4..3d57277ff9d 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -534,7 +534,7 @@ adapters.sovrn.endpoint=http://localhost:8090/sovrn-exchange adapters.sovrnXsp.enabled=true adapters.sovrnXsp.endpoint=http://localhost:8090/sovrnxsp-exchange adapters.sparteo.enabled=true -adapters.sparteo.endpoint=http://localhost:8090/sparteo-exchange?network_id={{NetworkId}} +adapters.sparteo.endpoint=http://localhost:8090/sparteo-exchange adapters.sspbc.enabled=true adapters.sspbc.endpoint=http://localhost:8090/sspbc-exchange adapters.sharethrough.enabled=true From 802eb22d6c772d667d23d0b4c783c0efe6fbfcd2 Mon Sep 17 00:00:00 2001 From: Thomas Sormonte Date: Thu, 13 Nov 2025 11:26:22 +0100 Subject: [PATCH 5/6] fix url validation --- .../java/org/prebid/server/bidder/sparteo/SparteoBidder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java index fa222204caf..8d7932ab5c0 100644 --- a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java +++ b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java @@ -52,7 +52,7 @@ public class SparteoBidder implements Bidder { private final JacksonMapper mapper; public SparteoBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrlSyntax(Objects.requireNonNull(endpointUrl)); + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); } From 54fa63e03e5d27754a06134ad0c35f94efb939df Mon Sep 17 00:00:00 2001 From: Thomas Sormonte Date: Tue, 18 Nov 2025 11:38:53 +0100 Subject: [PATCH 6/6] Code review CTMBNara --- .../prebid/server/bidder/sparteo/SparteoBidder.java | 2 +- .../server/bidder/sparteo/SparteoBidderTest.java | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java index 8d7932ab5c0..42c12a48d1c 100644 --- a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java +++ b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java @@ -103,7 +103,7 @@ private ExtImpSparteo parseExtImp(Imp imp) { } } - private ObjectNode modifyImpExt(Imp imp) { + private static ObjectNode modifyImpExt(Imp imp) { final ObjectNode modifiedImpExt = imp.getExt().deepCopy(); final ObjectNode sparteoNode = modifiedImpExt.putObject("sparteo"); final JsonNode bidderJsonNode = modifiedImpExt.remove("bidder"); diff --git a/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java index 3eacb6e48fd..e741c5e29c6 100644 --- a/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/sparteo/SparteoBidderTest.java @@ -39,15 +39,18 @@ public class SparteoBidderTest extends VertxTest { - private static final String ENDPOINT_URL = - "https://test.sparteo.com/endpoint"; + private static final String ENDPOINT_URL = "https://test.sparteo.com/endpoint"; private final SparteoBidder sparteoBidder = new SparteoBidder(ENDPOINT_URL, jacksonMapper); @Test public void creationShouldFailOnInvalidEndpointUrl() { + // given + final String invalidUrl = "invalid_url"; + + // when and then assertThatIllegalArgumentException() - .isThrownBy(() -> new SparteoBidder("invalid_url", jacksonMapper)) - .withMessage("URL supplied is not valid: invalid_url"); + .isThrownBy(() -> new SparteoBidder(invalidUrl, jacksonMapper)) + .withMessage("URL supplied is not valid: " + invalidUrl); } @Test