diff --git a/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java b/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java index 4c64992184b..6380b55e595 100644 --- a/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java +++ b/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java @@ -7,8 +7,10 @@ import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.vertx.core.MultiMap; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; @@ -57,8 +59,13 @@ public Result>> makeHttpRequests(BidRequest request for (Imp imp : request.getImp()) { try { + // Validate that at least one media type (banner, video, native, or audio) is present. + // This extends the previous banner-only validation to support the new media types. + if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() == null && imp.getAudio() == null) { + throw new PreBidException("We need a Banner, Video, Native or Audio Object in the request"); + } final ExtImpConnectAd impExt = parseImpExt(imp); - final Imp updatedImp = updateImp(imp, secure, impExt.getSiteId(), impExt.getBidFloor()); + final Imp updatedImp = updateImp(imp, secure, impExt); processedImps.add(updatedImp); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); @@ -98,20 +105,59 @@ private ExtImpConnectAd parseImpExt(Imp imp) { return extImpConnectAd; } - private Imp updateImp(Imp imp, Integer secure, String siteId, BigDecimal bidFloor) { + private Imp updateImp(Imp imp, Integer secure, ExtImpConnectAd extImpConnectAd) { + final BigDecimal bidFloor = extImpConnectAd.getBidFloor(); final boolean isValidBidFloor = BidderUtil.isValidPrice(bidFloor); return imp.toBuilder() .banner(updateBanner(imp.getBanner())) - .tagid(siteId) + .tagid(extImpConnectAd.getSiteId()) .secure(secure) .bidfloor(isValidBidFloor ? bidFloor : imp.getBidfloor()) .bidfloorcur(isValidBidFloor ? "USD" : imp.getBidfloorcur()) + .ext(modifyImpExt(imp.getExt(), extImpConnectAd)) .build(); } + /** + * Modifies the impression extension to propagate networkId and siteId to the root level. + * + * The ConnectAd server endpoint requires these values to be present at the root level + * of the imp.ext object for proper request routing and processing. This method extracts + * networkId and siteId from the adapter parameters and adds them to the root level of + * the extension object. Values are parsed as integers when possible, falling back to + * string representation if parsing fails (e.g., for non-numeric IDs). + * + * @param impExt the original impression extension object + * @param extImpConnectAd the parsed adapter parameters containing networkId and siteId + * @return the modified impression extension object with networkId and siteId at root level + */ + private ObjectNode modifyImpExt(ObjectNode impExt, ExtImpConnectAd extImpConnectAd) { + final ObjectNode modifiedExt = impExt != null ? impExt.deepCopy() : mapper.mapper().createObjectNode(); + final String networkId = extImpConnectAd.getNetworkId(); + final String siteId = extImpConnectAd.getSiteId(); + + if (networkId != null) { + try { + modifiedExt.put("networkId", Integer.parseInt(networkId)); + } catch (NumberFormatException e) { + modifiedExt.put("networkId", networkId); + } + } + + if (siteId != null) { + try { + modifiedExt.put("siteId", Integer.parseInt(siteId)); + } catch (NumberFormatException e) { + modifiedExt.put("siteId", siteId); + } + } + + return modifiedExt; + } + private static Banner updateBanner(Banner banner) { if (banner == null) { - throw new PreBidException("We need a Banner Object in the request"); + return null; } if (banner.getW() != null || banner.getH() != null) { @@ -153,13 +199,13 @@ private static MultiMap resolveHeaders(Device device) { public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(bidResponse)); + return Result.withValues(extractBids(bidResponse, bidRequest)); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(BidResponse bidResponse) { + private List extractBids(BidResponse bidResponse, BidRequest bidRequest) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } @@ -170,7 +216,49 @@ private List extractBids(BidResponse bidResponse) { .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(Objects::nonNull) - .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) + .map(bid -> BidderBid.of(bid, getBidType(bid, bidRequest), bidResponse.getCur())) .toList(); } + + /** + * Determines the bid type based on the bid's mtype field or falls back to the impression type. + * + * The method first checks if the bid contains an explicit mtype value and converts it to the + * corresponding BidType (1=banner, 2=video, 3=audio, 4=native). If mtype is not present, + * it looks up the impression by ID in the bidRequest and infers the type from the impression's + * media type fields (banner, video, native, audio). + * + * @param bid the bid response object + * @param bidRequest the original bid request (guaranteed to be non-null when called from makeBids) + * @return the determined BidType, defaulting to banner if no type can be determined + */ + private static BidType getBidType(Bid bid, BidRequest bidRequest) { + final Integer mType = bid.getMtype(); + if (mType != null) { + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> BidType.banner; + }; + } + + // bidRequest is guaranteed to be non-null at this point (passed from makeBids method) + for (Imp imp : bidRequest.getImp()) { + if (imp.getId().equals(bid.getImpid())) { + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } else if (imp.getAudio() != null) { + return BidType.audio; + } + } + } + + return BidType.banner; + } } diff --git a/src/main/resources/bidder-config/connectad.yaml b/src/main/resources/bidder-config/connectad.yaml index e51102b891c..628286cb662 100644 --- a/src/main/resources/bidder-config/connectad.yaml +++ b/src/main/resources/bidder-config/connectad.yaml @@ -12,8 +12,14 @@ adapters: maintainer-email: support@connectad.io app-media-types: - banner + - video + - native + - audio site-media-types: - banner + - video + - native + - audio supported-vendors: vendor-id: 138 usersync: diff --git a/src/main/resources/static/bidder-params/connectad.json b/src/main/resources/static/bidder-params/connectad.json index e36410928da..5a6f6ec9294 100644 --- a/src/main/resources/static/bidder-params/connectad.json +++ b/src/main/resources/static/bidder-params/connectad.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "ConnectAd S2S dapter Params", + "title": "ConnectAd S2S Adapter Params", "description": "A schema which validates params accepted by the ConnectAd Adapter", "type": "object", "properties": { @@ -20,7 +20,11 @@ }, "bidfloor": { "type": "number", - "description": "Requests Floorprice" + "description": "Requested Floorprice" + }, + "endpointUrl": { + "type": "string", + "description": "Client-side only (Prebid.js): override the bid endpoint URL for testing or a custom datacenter. Ignored by Prebid Server, which always uses its configured endpoint." } }, "required": [ diff --git a/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java b/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java index 3ac89edc67a..6665203d9df 100644 --- a/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java @@ -49,6 +49,7 @@ public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .id("123") + .banner(Banner.builder().w(300).h(250).build()) .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) .build())) .build(); @@ -157,6 +158,26 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtHasNoSiteId() { BidderError.badInput("Error in preprocess of Imp"))); } + @Test + public void makeHttpRequestsShouldReturnErrorIfNoMediaTypePresent() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder + .banner(null) + .video(null) + .xNative(null) + .audio(null)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(2); + assertThat(result.getErrors(), + containsInAnyOrder(BidderError.badInput("We need a Banner, Video, Native or Audio Object in the request"), + BidderError.badInput("Error in preprocess of Imp"))); + } + @Test public void impSecureShouldBeOneIfSitePageStartsFromHttps() { // given @@ -180,6 +201,58 @@ public void impSecureShouldBeOneIfSitePageStartsFromHttps() { .containsOnly(1); } + @Test + public void makeHttpRequestsShouldPropagateSiteIdAndNetworkId() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpConnectAd.of("12345", "67890", BigDecimal.ONE))))); + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .hasSize(1) + .first() + .satisfies(imp -> { + assertThat(imp.getTagid()).isEqualTo("67890"); + assertThat(imp.getExt().get("networkId").asInt()).isEqualTo(12345); + assertThat(imp.getExt().get("siteId").asInt()).isEqualTo(67890); + }); + } + + @Test + public void makeHttpRequestsShouldPropagateSiteIdAndNetworkIdAsStringsIfNonNumeric() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpConnectAd.of("net_abc", "site_xyz", BigDecimal.ONE))))); + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .hasSize(1) + .first() + .satisfies(imp -> { + assertThat(imp.getTagid()).isEqualTo("site_xyz"); + assertThat(imp.getExt().get("networkId").asText()).isEqualTo("net_abc"); + assertThat(imp.getExt().get("siteId").asText()).isEqualTo("site_xyz"); + }); + } + private static BidRequest givenBidRequest( Function bidRequestCustomizer, Function impCustomizer) { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json index c511c5b8e65..ad7a3eda658 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json @@ -3,7 +3,7 @@ "imp": [ { "id": "imp_id", - "secure": 1, + "secure": 0, "banner": { "w": 300, "h": 250 @@ -11,14 +11,15 @@ "tagid": "15", "bidfloor": 14.7, "bidfloorcur": "USD", - "secure": 0, "ext": { "tid": "${json-unit.any-string}", "bidder": { "networkId": "12", "siteId": "15", "bidfloor": 14.7 - } + }, + "networkId": 12, + "siteId": 15 } } ],