From 209c1d2901622108d8239de7ac9fcdc88269bb76 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Tue, 11 Aug 2026 00:44:29 +0300 Subject: [PATCH 1/3] Port Scalibur: New Adapter --- .../bidder/scalibur/ScaliburBidder.java | 297 ++++++ .../proto/request/ExtImpScalibur.java | 19 + .../config/bidder/ScaliburConfiguration.java | 37 + .../resources/bidder-config/scalibur.yaml | 20 + .../static/bidder-params/scalibur.json | 25 + .../bidder/scalibur/ScaliburBidderTest.java | 868 ++++++++++++++++++ .../org/prebid/server/it/ScaliburTest.java | 33 + .../test-auction-scalibur-request.json | 24 + .../test-auction-scalibur-response.json | 41 + .../scalibur/test-scalibur-bid-request.json | 40 + .../scalibur/test-scalibur-bid-response.json | 19 + .../server/it/test-application.properties | 2 + 12 files changed, 1425 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java create mode 100644 src/main/java/org/prebid/server/bidder/scalibur/proto/request/ExtImpScalibur.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/ScaliburConfiguration.java create mode 100644 src/main/resources/bidder-config/scalibur.yaml create mode 100644 src/main/resources/static/bidder-params/scalibur.json create mode 100644 src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/ScaliburTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-auction-scalibur-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-auction-scalibur-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-scalibur-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-scalibur-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java b/src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java new file mode 100644 index 00000000000..7fa1b527a35 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java @@ -0,0 +1,297 @@ +package org.prebid.server.bidder.scalibur; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +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; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.scalibur.proto.request.ExtImpScalibur; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class ScaliburBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String DEFAULT_BID_CURRENCY = "USD"; + private static final String VAST_XML = """ + + """; + + private final String endpointUrl; + private final CurrencyConversionService currencyConversionService; + private final JacksonMapper mapper; + + public ScaliburBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final List errors = new ArrayList<>(); + final List validImps = new ArrayList<>(); + + for (Imp imp : bidRequest.getImp()) { + try { + final ExtImpScalibur scaliburExt = parseImpExt(imp); + validImps.add(modifyImp(imp, scaliburExt, bidRequest)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (validImps.isEmpty()) { + return Result.withErrors(errors); + } + + final BidRequest modifiedBidRequest = bidRequest.toBuilder() + .imp(validImps) + .cur(null) + .ext(isDebugEnabled(bidRequest) + ? createDebugExt() + : null) + .build(); + + final HttpRequest httpRequest = BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper); + return Result.of(Collections.singletonList(httpRequest), errors); + } + + private ExtImpScalibur parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, + ExtImpScalibur extImpScalibur, + BidRequest bidRequest) { + + final Price resolvedBidFloor = resolveBidFloor(imp, extImpScalibur, bidRequest); + final JsonNode gpidNode = imp.getExt().get("gpid"); + + return imp.toBuilder() + .bidfloor(resolvedBidFloor.getValue()) + .bidfloorcur(resolvedBidFloor.getCurrency()) + .video(resolveVideo(imp.getVideo())) + .ext(resolveImpExt(extImpScalibur, resolvedBidFloor, gpidNode)) + .build(); + } + + private Price resolveBidFloor(Imp imp, + ExtImpScalibur extImpScalibur, + BidRequest bidRequest) { + + final BigDecimal extPrice = extImpScalibur.getBidFloor(); + final Price price = BidderUtil.isValidPrice(extPrice) + ? Price.of(StringUtils.defaultIfBlank(extImpScalibur.getBidFloorCur(), imp.getBidfloorcur()), extPrice) + : Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + if (BidderUtil.shouldConvertBidFloor(price, DEFAULT_BID_CURRENCY)) { + return convertBidFloor(price, bidRequest); + } + return Price.of( + StringUtils.defaultIfBlank(price.getCurrency(), DEFAULT_BID_CURRENCY), + price.getValue()); + } + + private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency(bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + DEFAULT_BID_CURRENCY); + + return Price.of(DEFAULT_BID_CURRENCY, convertedPrice); + } + + private ObjectNode resolveImpExt(ExtImpScalibur extImpScalibur, + Price bidFloor, + JsonNode gpidNode) { + + final ObjectNode ext = mapper.mapper().createObjectNode(); + ext.set("placementId", TextNode.valueOf(extImpScalibur.getPlacementId())); + if (BidderUtil.isValidPrice(bidFloor)) { + ext.set("bidfloor", mapper.mapper().valueToTree(bidFloor.getValue())); + } + ext.set("bidfloorcur", TextNode.valueOf(bidFloor.getCurrency())); + if (gpidNode != null && !gpidNode.isNull()) { + ext.set("gpid", gpidNode); + } + return ext; + } + + private Video resolveVideo(Video video) { + if (video == null) { + return null; + } + + final Video.VideoBuilder builder = video.toBuilder(); + + if (CollectionUtils.isEmpty(video.getMimes())) { + builder.mimes(Collections.singletonList("video/mp4")); + } + if (BidderUtil.isNullOrZero(video.getMinduration())) { + builder.minduration(1); + } + if (BidderUtil.isNullOrZero(video.getMaxduration())) { + builder.maxduration(180); + } + if (BidderUtil.isNullOrZero(video.getMaxbitrate())) { + builder.maxbitrate(30_000); + } + if (CollectionUtils.isEmpty(video.getProtocols())) { + builder.protocols(List.of(2, 3, 5, 6)); + } + if (BidderUtil.isNullOrZero(video.getW())) { + builder.w(640); + } + if (BidderUtil.isNullOrZero(video.getH())) { + builder.h(480); + } + if (BidderUtil.isNullOrZero(video.getPlacement())) { + builder.placement(1); + } + if (BidderUtil.isNullOrZero(video.getLinearity())) { + builder.linearity(1); + } + + return builder.build(); + } + + private boolean isDebugEnabled(BidRequest bidRequest) { + if (Objects.equals(bidRequest.getTest(), 1)) { + return true; + } + final ExtRequest ext = bidRequest.getExt(); + if (ext == null || ext.getPrebid() == null) { + return false; + } + return Objects.equals(ext.getPrebid().getDebug(), 1); + } + + private ExtRequest createDebugExt() { + final ExtRequest extRequest = ExtRequest.empty(); + extRequest.addProperty("isDebug", IntNode.valueOf(1)); + return extRequest; + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return extractBids(httpCall.getRequest().getPayload(), bidResponse); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private Result> extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Result.empty(); + } + final List errors = new ArrayList<>(); + final List bidderBids = new ArrayList<>(); + for (SeatBid seatBid : bidResponse.getSeatbid()) { + if (CollectionUtils.isEmpty(seatBid.getBid())) { + continue; + } + + for (Bid bid : seatBid.getBid()) { + try { + bidderBids.add(createBidderBid(bid, bidRequest, bidResponse)); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + } + } + } + + return Result.of(bidderBids, errors); + } + + private BidderBid createBidderBid(Bid bid, BidRequest bidRequest, BidResponse bidResponse) { + final Imp imp = bidRequest.getImp().stream() + .filter(candidate -> Objects.equals(candidate.getId(), bid.getImpid())) + .findFirst() + .orElseThrow(() -> new PreBidException("Invalid bid imp ID %s".formatted(bid.getImpid()))); + + final BidType bidType = resolveBidType(bid, imp); + final Bid resolvedBid = bidType == BidType.video ? resolveVideoBid(bid) : bid; + final String resolvedCurrency = StringUtils.defaultIfEmpty(bidResponse.getCur(), DEFAULT_BID_CURRENCY); + + return BidderBid.of(resolvedBid, bidType, resolvedCurrency); + } + + private Bid resolveVideoBid(Bid bid) { + if (bid.getExt() == null) { + return bid; + } + + final JsonNode vastXml = bid.getExt().get("vastXml"); + if (isNonEmptyText(vastXml)) { + return bid.toBuilder().adm(vastXml.asText()).build(); + } + + final JsonNode vastUrl = bid.getExt().get("vastUrl"); + if (isNonEmptyText(vastUrl) && StringUtils.isEmpty(bid.getAdm())) { + return bid.toBuilder() + .adm(VAST_XML.formatted(vastUrl.asText()).trim()) + .build(); + } + return bid; + } + + private boolean isNonEmptyText(JsonNode node) { + return node != null && node.isTextual() && StringUtils.isNotEmpty(node.asText()); + } + + private BidType resolveBidType(Bid bid, Imp imp) { + if (Objects.equals(bid.getMtype(), 1)) { + return BidType.banner; + } + if (Objects.equals(bid.getMtype(), 2)) { + return BidType.video; + } + if (imp.getBanner() != null && imp.getVideo() == null) { + return BidType.banner; + } + if (imp.getVideo() != null && imp.getBanner() == null) { + return BidType.video; + } + throw new PreBidException( + "Unsupported or ambiguous media type for bid id=%s".formatted(bid.getId())); + } +} diff --git a/src/main/java/org/prebid/server/bidder/scalibur/proto/request/ExtImpScalibur.java b/src/main/java/org/prebid/server/bidder/scalibur/proto/request/ExtImpScalibur.java new file mode 100644 index 00000000000..cddf2690f37 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/scalibur/proto/request/ExtImpScalibur.java @@ -0,0 +1,19 @@ +package org.prebid.server.bidder.scalibur.proto.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpScalibur { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("bidfloor") + BigDecimal bidFloor; + + @JsonProperty("bidfloorcur") + String bidFloorCur; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ScaliburConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ScaliburConfiguration.java new file mode 100644 index 00000000000..4e69427e3ef --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ScaliburConfiguration.java @@ -0,0 +1,37 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.scalibur.ScaliburBidder; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/scalibur.yaml", factory = YamlPropertySourceFactory.class) +public class ScaliburConfiguration { + + private static final String BIDDER_NAME = "scalibur"; + + @Bean("scaliburConfigurationProperties") + @ConfigurationProperties("adapters.scalibur") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps scaliburBidderDeps(BidderConfigurationProperties scaliburConfigurationProperties, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(scaliburConfigurationProperties) + .bidderCreator(config -> new ScaliburBidder(config.getEndpoint(), currencyConversionService, mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/scalibur.yaml b/src/main/resources/bidder-config/scalibur.yaml new file mode 100644 index 00000000000..6c5260b2534 --- /dev/null +++ b/src/main/resources/bidder-config/scalibur.yaml @@ -0,0 +1,20 @@ +adapters: + scalibur: + endpoint: https://srv.scalibur.io/adserver/ortb?type=prebid-server + ortb-version: "2.6" + meta-info: + maintainer-email: support@scalibur.io + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 1471 + endpoint-compression: gzip + usersync: + cookie-family-name: scalibur + iframe: + url: https://srv.scalibur.io/adserver/sync?type=iframe&gdpr={gdpr}&gdpr_consent={gdpr_consent}&us_privacy={us_privacy}&redirect={redirect_url} + uid-macro: '[PBS_UID]' diff --git a/src/main/resources/static/bidder-params/scalibur.json b/src/main/resources/static/bidder-params/scalibur.json new file mode 100644 index 00000000000..638c79696de --- /dev/null +++ b/src/main/resources/static/bidder-params/scalibur.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Scalibur Adapter Params", + "description": "A valid BidderParams object for the Scalibur adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "description": "Placement ID", + "minLength": 1 + }, + "bidfloor": { + "type": "number", + "description": "The minimum price acceptable for a bid", + "minimum": 0 + }, + "bidfloorcur": { + "type": "string", + "description": "currency floor price" + } + }, + "required": [ + "placementId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java b/src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java new file mode 100644 index 00000000000..27131d96949 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java @@ -0,0 +1,868 @@ +package org.prebid.server.bidder.scalibur; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +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.bidder.scalibur.proto.request.ExtImpScalibur; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class ScaliburBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + private static final String DEFAULT_BID_CURRENCY = "USD"; + private static final String VAST_XML = """ + """; + + @Mock + private CurrencyConversionService currencyConversionService; + + private ScaliburBidder target; + + @BeforeEach + public void setUp() { + target = new ScaliburBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ScaliburBidder("invalid_url", currencyConversionService, jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorsWithoutRequestWhenAllImpsAreInvalid() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1); + } + + @Test + public void makeHttpRequestsShouldAddErrorsOnInvalidImps() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))), + // valid imps + givenImp(identity()), + givenImp(imp -> imp.ext(givenImpExt(ExtImpScalibur.of("placementId", null, DEFAULT_BID_CURRENCY))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .hasSize(2); + assertThat(result.getErrors()) + .extracting(BidderError::getMessage) + .hasSize(1) + .satisfies(errors -> + assertThat(errors.getFirst()).startsWith("Cannot deserialize value of type")); + } + + @Test + public void makeHttpRequestsShouldConvertBidFloorIfCurrencyIsDifferent() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("EUR"))); + + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "EUR", DEFAULT_BID_CURRENCY)) + .willReturn(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) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsExactly(tuple(BigDecimal.ONE, DEFAULT_BID_CURRENCY)); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorAndAssignUSDCurrencyWhenBidfloorHasEmptyCurrency() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur(null))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsExactly(tuple(BigDecimal.TEN, DEFAULT_BID_CURRENCY)); + } + + @Test + public void makeHttpRequestsShouldOverrideBidfloorAndCurrencyWhenScaliburBidFloorIsValid() { + // given + + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("EUR") + .ext(givenImpExt(ExtImpScalibur.of("placementId", BigDecimal.ONE, DEFAULT_BID_CURRENCY))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsExactly(tuple(BigDecimal.ONE, DEFAULT_BID_CURRENCY)); + } + + @Test + public void makeHttpRequestsShouldNotOverrideBidfloorAndCurrencyWhenScaliburBidFloorIsInvalid() { + // given + + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur(DEFAULT_BID_CURRENCY) + .ext(givenImpExt(ExtImpScalibur.of("placementId", null, "EUR"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsExactly(tuple(BigDecimal.TEN, DEFAULT_BID_CURRENCY)); + } + + @Test + public void makeHttpRequestsShouldFallbackToImpCurrencyWhenScaliburCurrencyIsMissing() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("EUR") + .ext(givenImpExt(ExtImpScalibur.of("placementId", BigDecimal.ONE, null))))); + + given(currencyConversionService.convertCurrency(BigDecimal.ONE, bidRequest, "EUR", DEFAULT_BID_CURRENCY)) + .willReturn(BigDecimal.valueOf(2)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsExactly(tuple(BigDecimal.valueOf(2), DEFAULT_BID_CURRENCY)); + } + + @Test + public void makeHttpRequestsShouldSetPlacementIdInImpExt() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp + .ext(givenImpExt(ExtImpScalibur.of("placementId", null, null))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(mapper.valueToTree(ExtImpScalibur.of("placementId", null, DEFAULT_BID_CURRENCY))); + } + + @Test + public void makeHttpRequestsShouldBuildImpExtWithScaliburFieldsAndGpid() { + // given + final ObjectNode impExt = givenImpExt(ExtImpScalibur.of("placementId", BigDecimal.ONE, DEFAULT_BID_CURRENCY)); + impExt.put("gpid", "test-gpid"); + impExt.put("random", "test-random"); + + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.ext(impExt))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly( + mapper.createObjectNode() + .put("placementId", "placementId") + .put("bidfloor", BigDecimal.ONE) + .put("bidfloorcur", DEFAULT_BID_CURRENCY) + .put("gpid", "test-gpid")); + } + + @Test + public void makeHttpRequestsShouldFillVideoDefaultValues() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp + .banner(null) + .video(Video.builder().build()))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(HttpRequest::getPayload) + .satisfies(request -> assertThat(request.getImp()) + .singleElement() + .extracting(Imp::getVideo) + .satisfies(video -> { + assertThat(video.getMimes()).containsExactly("video/mp4"); + assertThat(video.getMinduration()).isEqualTo(1); + assertThat(video.getMaxduration()).isEqualTo(180); + assertThat(video.getMaxbitrate()).isEqualTo(30_000); + assertThat(video.getProtocols()).containsExactly(2, 3, 5, 6); + assertThat(video.getW()).isEqualTo(640); + assertThat(video.getH()).isEqualTo(480); + assertThat(video.getPlacement()).isEqualTo(1); + assertThat(video.getLinearity()).isEqualTo(1); + })); + } + + @Test + public void makeHttpRequestsShouldPreserveProvidedVideoValues() { + // given + final Video video = Video.builder() + .mimes(List.of("video/webm")) + .minduration(10) + .maxduration(30) + .maxbitrate(5000) + .protocols(List.of(1, 2)) + .w(1280) + .h(720) + .placement(2) + .linearity(2) + .build(); + + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp + .banner(null) + .video(video))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(HttpRequest::getPayload) + .satisfies() + .satisfies(request -> assertThat(request.getImp()) + .singleElement() + .extracting(Imp::getVideo) + .isEqualTo(video)); + } + + @Test + public void makeHttpRequestsShouldSetIsDebugWhenTestIsEnabled() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(identity())) + .toBuilder() + .test(1) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .satisfies(ext -> assertThat(ext.getProperty("isDebug").asInt()).isEqualTo(1)); + } + + @Test + public void makeHttpRequestsShouldSetIsDebugWhenPrebidDebugIsEnabled() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(identity())) + .toBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().debug(1).build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .satisfies(ext -> assertThat(ext.getProperty("isDebug").asInt()).isEqualTo(1)); + } + + @Test + public void makeHttpRequestsShouldRemoveRequestExtWhenDebugIsDisabled() { + // given + final ExtRequest extRequest = ExtRequest.empty(); + extRequest.addProperty("random", TextNode.valueOf("test-random")); + + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp)) + .toBuilder() + .ext(extRequest) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .isNull(); + } + + @Test + public void makeBidsShouldReturnBidsFromAllSeatBids() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(identity()), + givenImp(imp -> imp.id("456"))); + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder.id("bid-1").impid("123"), + bidBuilder -> bidBuilder.id("bid-2").impid("456") + ))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getId) + .containsExactly("bid-1", "bid-2"); + } + + @Test + public void makeBidsShouldUseUsdCurrencyWhenResponseCurrencyIsMissing() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(givenImp(identity())); + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.id("bid-1").impid("123")))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(BidderBid::getBidCurrency) + .isEqualTo(DEFAULT_BID_CURRENCY); + } + + @Test + public void makeBidsShouldUseResponseCurrency() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(givenImp(identity())); + final BidResponse bidResponse = givenBidResponse(bidBuilder -> bidBuilder.id("bid-1").impid("123")).toBuilder() + .cur("EUR") + .build(); + + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(BidderBid::getBidCurrency) + .isEqualTo("EUR"); + } + + @Test + public void makeBidsShouldRejectInvalidBidImpId() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(givenImp(identity())); + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("invalid-imp")))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .singleElement() + .extracting(BidderError::getMessage) + .isEqualTo("Invalid bid imp ID invalid-imp"); + } + + @Test + public void makeBidsShouldResolveBannerBidTypeFromMtype() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(givenImp(identity())); + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("123") + .mtype(1)))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(BidderBid::getType) + .isEqualTo(BidType.banner); + } + + @Test + public void makeBidsShouldResolveVideoBidTypeFromMtype() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.banner(null).video(Video.builder().build()))); + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("123") + .mtype(2)))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(BidderBid::getType) + .isEqualTo(BidType.video); + } + + @Test + public void makeBidsShouldResolveBidTypeFromImpWhenMtypeIsMissing() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.banner(Banner.builder().w(1).h(1).build()).video(null))); + + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("123") + .mtype(null)))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(BidderBid::getType) + .isEqualTo(BidType.banner); + } + + @Test + public void makeBidsShouldResolveVideoBidTypeFromVideoImpWhenMtypeIsMissing() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.banner(null).video(Video.builder().build()))); + + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("123") + .mtype(null)))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(BidderBid::getType) + .isEqualTo(BidType.video); + } + + @Test + public void makeBidsShouldRecoverMediaTypeFromImpWhenMtypeIsInvalid() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(givenImp(identity())); + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("123") + .mtype(3)))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(BidderBid::getType) + .isEqualTo(BidType.banner); + } + + @Test + public void makeBidsShouldRejectAmbiguousImpMediaType() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp + .banner(Banner.builder().w(1).h(1).build()) + .video(Video.builder().build()))); + + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("123") + .mtype(null)))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .singleElement() + .extracting(BidderError::getMessage) + .isEqualTo("Unsupported or ambiguous media type for bid id=bid-1"); + } + + @Test + public void makeBidsShouldRejectInvalidMediaType() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp + .banner(null) + .audio(Audio.builder().build()))); + + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("123") + .mtype(3)))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .singleElement() + .extracting(BidderError::getMessage) + .isEqualTo("Unsupported or ambiguous media type for bid id=bid-1"); + } + + @Test + public void makeBidsShouldReplaceAdmWithVastXml() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.banner(null).video(Video.builder().build()))); + + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("123") + .ext(mapper.createObjectNode().put("vastXml", "test"))))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(BidderBid::getBid) + .extracting(Bid::getAdm) + .isEqualTo("test"); + } + + @Test + public void makeBidsShouldReplaceNotPresentAdmWithVastUrlWrapper() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.banner(null).video(Video.builder().build()))); + + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("123") + .ext(mapper.createObjectNode().put("vastUrl", "https://test.com"))))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(BidderBid::getBid) + .extracting(Bid::getAdm) + .isEqualTo(VAST_XML.formatted("https://test.com")); + } + + + @Test + public void makeBidsShouldPrioritizeVastXmlOverVastUrlWhenBothArePresent() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.banner(null).video(Video.builder().build()))); + + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("123") + .ext(mapper.createObjectNode() + .put("vastXml", "test") + .put("vastUrl", "https://test.com"))))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(BidderBid::getBid) + .extracting(Bid::getAdm) + .isEqualTo("test"); + } + + @Test + public void makeBidsShouldPreserveAdmWhenVastUrlIsPresent() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.banner(null).video(Video.builder().build()))); + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder + .id("bid-1") + .impid("123") + .adm("test-adm") + .ext(mapper.createObjectNode().put("vastUrl", "https://test.com"))))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .singleElement() + .extracting(BidderBid::getBid) + .extracting(Bid::getAdm) + .isEqualTo("test-adm"); + } + + @Test + public void makeBidsShouldReturnEmptyResultWhenResponseHasNoSeatBids() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(givenImp(identity())); + final BidResponse bidResponse = BidResponse.builder() + .seatbid(Collections.emptyList()) + .build(); + + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(givenImp(identity())), "invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_server_response + && error.getMessage().startsWith("Failed to decode: Unrecognized token")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnValidBidsAndErrorsForInvalidBids() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(givenImp(identity())); + final BidderCall httpCall = givenHttpCall( + bidRequest, + mapper.writeValueAsString( + givenBidResponse( + bidBuilder -> bidBuilder.id("valid-bid").impid("123"), + bidBuilder -> bidBuilder.id("invalid-bid").impid("invalid-imp")))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getId) + .containsExactly("valid-bid"); + + assertThat(result.getErrors()) + .singleElement() + .extracting(BidderError::getMessage) + .isEqualTo("Invalid bid imp ID invalid-imp"); + } + + private static BidRequest givenBidRequest(Imp... imps) { + return BidRequest.builder() + .cur(List.of("USD")) + .imp(Arrays.stream(imps).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .banner(Banner.builder().w(1).h(1).build()) + .ext(givenImpExt(ExtImpScalibur.of("placementId", null, null)))) + .build(); + } + + private static ObjectNode givenImpExt(ExtImpScalibur extImpScalibur) { + return mapper.valueToTree(ExtPrebid.of(null, extImpScalibur)); + } + + @SafeVarargs + @SneakyThrows + private BidResponse givenBidResponse(UnaryOperator... bidCustomizers) { + return BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(Arrays.stream(bidCustomizers) + .map(bidCustomizer -> bidCustomizer.apply(Bid.builder()).build()) + .toList()) + .build())) + .build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/ScaliburTest.java b/src/test/java/org/prebid/server/it/ScaliburTest.java new file mode 100644 index 00000000000..cd33f769458 --- /dev/null +++ b/src/test/java/org/prebid/server/it/ScaliburTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; + +public class ScaliburTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromScalibur() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/scalibur-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/scalibur/test-scalibur-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/scalibur/test-scalibur-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/scalibur/test-auction-scalibur-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/scalibur/test-auction-scalibur-response.json", response, List.of("scalibur")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-auction-scalibur-request.json b/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-auction-scalibur-request.json new file mode 100644 index 00000000000..d31a1b8ffdf --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-auction-scalibur-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloorcur": "USD", + "ext": { + "scalibur": { + "placementId": "placementid" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-auction-scalibur-response.json b/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-auction-scalibur-response.json new file mode 100644 index 00000000000..37da829cd13 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-auction-scalibur-response.json @@ -0,0 +1,41 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "scalibur" + } + }, + "origbidcpm": 0.01, + "origbidcur": "USD" + } + } + ], + "seat": "scalibur", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "scalibur": "{{ scalibur.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-scalibur-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-scalibur-bid-request.json new file mode 100644 index 00000000000..47df08faa07 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-scalibur-bid-request.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloorcur": "USD", + "secure" : 1, + "ext" : { + "placementId" : "placementid", + "bidfloorcur" : "USD" + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "source": { + "tid": "${json-unit.any-string}" + }, + "regs": { + "gdpr": 0 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-scalibur-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-scalibur-bid-response.json new file mode 100644 index 00000000000..47d4f8718ea --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/scalibur/test-scalibur-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "8048", + "mtype": 1 + } + ], + "type": "banner" + } + ] +} 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 c8dc5ef74fa..fc2647f3aae 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -500,6 +500,8 @@ adapters.magnite.usersync.redirect.url=http://localhost:8090/magnite-usersync?gd adapters.magnite.usersync.redirect.uid-macro=magnite-macro adapters.salunamedia.enabled=true adapters.salunamedia.endpoint=http://localhost:8090/salunamedia-exchange +adapters.scalibur.enabled=true +adapters.scalibur.endpoint=http://localhost:8090/scalibur-exchange adapters.screencore.enabled=true adapters.screencore.endpoint=http://localhost:8090/screencore-exchange adapters.seedingAlliance.enabled=true From 178af57743d877edcdb20b2f94677de640842ba8 Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Tue, 11 Aug 2026 12:20:17 +0300 Subject: [PATCH 2/3] fix code style --- .../java/org/prebid/server/bidder/scalibur/ScaliburBidder.java | 1 - .../org/prebid/server/bidder/scalibur/ScaliburBidderTest.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java b/src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java index 7fa1b527a35..e8e51bb9967 100644 --- a/src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java +++ b/src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java @@ -12,7 +12,6 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; diff --git a/src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java b/src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java index 27131d96949..7c5f86fe391 100644 --- a/src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java @@ -711,7 +711,6 @@ public void makeBidsShouldReplaceNotPresentAdmWithVastUrlWrapper() throws JsonPr .isEqualTo(VAST_XML.formatted("https://test.com")); } - @Test public void makeBidsShouldPrioritizeVastXmlOverVastUrlWhenBothArePresent() throws JsonProcessingException { // given From e13fa00f11af873e4ee34c2c8478bd1e5b8058ea Mon Sep 17 00:00:00 2001 From: osulzhenko Date: Tue, 11 Aug 2026 14:57:56 +0300 Subject: [PATCH 3/3] update after review --- .../org/prebid/server/bidder/scalibur/ScaliburBidder.java | 6 ++---- .../openrtb/ext/request/scalibur}/ExtImpScalibur.java | 2 +- src/main/resources/bidder-config/scalibur.yaml | 2 +- .../prebid/server/bidder/scalibur/ScaliburBidderTest.java | 5 ++--- 4 files changed, 6 insertions(+), 9 deletions(-) rename src/main/java/org/prebid/server/{bidder/scalibur/proto/request => proto/openrtb/ext/request/scalibur}/ExtImpScalibur.java (84%) diff --git a/src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java b/src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java index e8e51bb9967..099c09d1e17 100644 --- a/src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java +++ b/src/main/java/org/prebid/server/bidder/scalibur/ScaliburBidder.java @@ -20,7 +20,7 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Price; import org.prebid.server.bidder.model.Result; -import org.prebid.server.bidder.scalibur.proto.request.ExtImpScalibur; +import org.prebid.server.proto.openrtb.ext.request.scalibur.ExtImpScalibur; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; @@ -80,9 +80,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ final BidRequest modifiedBidRequest = bidRequest.toBuilder() .imp(validImps) .cur(null) - .ext(isDebugEnabled(bidRequest) - ? createDebugExt() - : null) + .ext(isDebugEnabled(bidRequest) ? createDebugExt() : null) .build(); final HttpRequest httpRequest = BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper); diff --git a/src/main/java/org/prebid/server/bidder/scalibur/proto/request/ExtImpScalibur.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/scalibur/ExtImpScalibur.java similarity index 84% rename from src/main/java/org/prebid/server/bidder/scalibur/proto/request/ExtImpScalibur.java rename to src/main/java/org/prebid/server/proto/openrtb/ext/request/scalibur/ExtImpScalibur.java index cddf2690f37..ece97e08095 100644 --- a/src/main/java/org/prebid/server/bidder/scalibur/proto/request/ExtImpScalibur.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/scalibur/ExtImpScalibur.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.scalibur.proto.request; +package org.prebid.server.proto.openrtb.ext.request.scalibur; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; diff --git a/src/main/resources/bidder-config/scalibur.yaml b/src/main/resources/bidder-config/scalibur.yaml index 6c5260b2534..1ffbbadc2ac 100644 --- a/src/main/resources/bidder-config/scalibur.yaml +++ b/src/main/resources/bidder-config/scalibur.yaml @@ -2,6 +2,7 @@ adapters: scalibur: endpoint: https://srv.scalibur.io/adserver/ortb?type=prebid-server ortb-version: "2.6" + endpoint-compression: gzip meta-info: maintainer-email: support@scalibur.io app-media-types: @@ -12,7 +13,6 @@ adapters: - video supported-vendors: vendor-id: 1471 - endpoint-compression: gzip usersync: cookie-family-name: scalibur iframe: diff --git a/src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java b/src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java index 7c5f86fe391..90c5a1dd6a1 100644 --- a/src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/scalibur/ScaliburBidderTest.java @@ -24,7 +24,7 @@ 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.bidder.scalibur.proto.request.ExtImpScalibur; +import org.prebid.server.proto.openrtb.ext.request.scalibur.ExtImpScalibur; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -397,8 +397,7 @@ public void makeBidsShouldReturnBidsFromAllSeatBids() throws JsonProcessingExcep mapper.writeValueAsString( givenBidResponse( bidBuilder -> bidBuilder.id("bid-1").impid("123"), - bidBuilder -> bidBuilder.id("bid-2").impid("456") - ))); + bidBuilder -> bidBuilder.id("bid-2").impid("456")))); // when final Result> result = target.makeBids(httpCall, bidRequest);