From 26c1a9c6a0f0dd897da0c0208614c92bcc08ce9f Mon Sep 17 00:00:00 2001 From: antonbabak Date: Tue, 17 Jun 2025 15:01:27 +0200 Subject: [PATCH 1/2] New AdupTech Adapter --- .../bidder/aduptech/AduptechBidder.java | 172 +++++++++ .../ext/request/aduptech/ExtImpAduptech.java | 21 ++ .../config/bidder/AduptechConfiguration.java | 60 ++++ .../resources/bidder-config/aduptech.yaml | 25 ++ .../static/bidder-params/aduptech.json | 38 ++ .../bidder/aduptech/AduptechBidderTest.java | 328 ++++++++++++++++++ .../org/prebid/server/it/AduptechTest.java | 34 ++ .../aduptech/test-aduptech-bid-request.json | 55 +++ .../aduptech/test-aduptech-bid-response.json | 19 + .../test-auction-aduptech-request.json | 22 ++ .../test-auction-aduptech-response.json | 40 +++ .../server/it/test-application.properties | 3 + 12 files changed, 817 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/aduptech/ExtImpAduptech.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java create mode 100644 src/main/resources/bidder-config/aduptech.yaml create mode 100644 src/main/resources/static/bidder-params/aduptech.json create mode 100644 src/test/java/org/prebid/server/bidder/aduptech/AduptechBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/AduptechTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-response.json diff --git a/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java b/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java new file mode 100644 index 00000000000..943c10a5533 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java @@ -0,0 +1,172 @@ +package org.prebid.server.bidder.aduptech; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +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.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.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.Collection; +import java.util.Collections; +import java.util.Currency; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class AduptechBidder implements Bidder { + + private static final String COMPONENT_ID_HEADER = "Componentid"; + private static final String COMPONENT_ID_HEADER_VALUE = "prebid-java"; + private static final String DEFAULT_BID_CURRENCY = "USD"; + + private final String endpointUrl; + private final JacksonMapper mapper; + private final CurrencyConversionService currencyConversionService; + private final String targetCurrency; + + public AduptechBidder(String endpointUrl, + JacksonMapper mapper, + CurrencyConversionService currencyConversionService, + String targetCurrency) { + + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.targetCurrency = validateCurrency(targetCurrency); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(request.getImp().size()); + for (Imp imp : request.getImp()) { + try { + modifiedImps.add(modifyImp(imp, request)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest outgoingRequest = request.toBuilder().imp(modifiedImps).build(); + final HttpRequest httpRequest = BidderUtil.defaultRequest( + outgoingRequest, + makeHeaders(), + endpointUrl, + mapper); + + return Result.withValue(httpRequest); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static String validateCurrency(String code) { + try { + Currency.getInstance(code); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("invalid extra info: invalid TargetCurrency %s".formatted(code)); + } + return code.toUpperCase(); + } + + private static MultiMap makeHeaders() { + return HttpUtil.headers().add(COMPONENT_ID_HEADER, COMPONENT_ID_HEADER_VALUE); + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private static List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBid(Bid bid, String currency, List errors) { + try { + return BidderBid.of(bid, getBidType(bid.getMtype()), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getBidType(Integer markupType) { + return switch (markupType) { + case 1 -> BidType.banner; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("Unknown markup type: " + markupType); + }; + } + + private Imp modifyImp(Imp imp, BidRequest bidRequest) { + Price impFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + impFloorPrice = BidderUtil.isValidPrice(impFloorPrice) + && !targetCurrency.equalsIgnoreCase(impFloorPrice.getCurrency()) + ? convertBidFloor(impFloorPrice, bidRequest) + : impFloorPrice; + + return imp.toBuilder() + .bidfloor(impFloorPrice.getValue()) + .bidfloorcur(impFloorPrice.getCurrency()) + .build(); + } + + private Price convertBidFloor(Price impFloorPrice, BidRequest bidRequest) { + try { + return convertToTargetCurrency(impFloorPrice.getValue(), bidRequest, impFloorPrice.getCurrency()); + } catch (PreBidException e) { + final BigDecimal defaultCurrencyBidFloor = currencyConversionService.convertCurrency( + impFloorPrice.getValue(), + bidRequest, + impFloorPrice.getCurrency(), + DEFAULT_BID_CURRENCY); + return convertToTargetCurrency(defaultCurrencyBidFloor, bidRequest, DEFAULT_BID_CURRENCY); + } + } + + private Price convertToTargetCurrency(BigDecimal impFloorPrice, BidRequest bidRequest, String fromCurrency) { + final BigDecimal convertedFloor = currencyConversionService.convertCurrency( + impFloorPrice, + bidRequest, + fromCurrency, + targetCurrency); + + return Price.of(targetCurrency, convertedFloor); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/aduptech/ExtImpAduptech.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/aduptech/ExtImpAduptech.java new file mode 100644 index 00000000000..6e92ad45db7 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/aduptech/ExtImpAduptech.java @@ -0,0 +1,21 @@ +package org.prebid.server.proto.openrtb.ext.request.aduptech; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAduptech { + + String publisher; + + String placement; + + String query; + + Boolean adtest; + + Boolean debug; + + ObjectNode ext; +} + diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java new file mode 100644 index 00000000000..6dfff25f396 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java @@ -0,0 +1,60 @@ +package org.prebid.server.spring.config.bidder; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.aduptech.AduptechBidder; +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.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +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; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/aduptech.yaml", factory = YamlPropertySourceFactory.class) +public class AduptechConfiguration { + + private static final String BIDDER_NAME = "aduptech"; + + @Bean("aduptechConfigurationProperties") + @ConfigurationProperties("adapters.aduptech") + AduptechConfigurationProperties configurationProperties() { + return new AduptechConfigurationProperties(); + } + + @Bean + BidderDeps aduptechBidderDeps(AduptechConfigurationProperties aduptechConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(aduptechConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AduptechBidder( + config.getEndpoint(), + mapper, + currencyConversionService, + config.getTargetCurrency()) + ).assemble(); + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class AduptechConfigurationProperties extends BidderConfigurationProperties { + + @NotNull + private String targetCurrency; + } +} diff --git a/src/main/resources/bidder-config/aduptech.yaml b/src/main/resources/bidder-config/aduptech.yaml new file mode 100644 index 00000000000..302051085bd --- /dev/null +++ b/src/main/resources/bidder-config/aduptech.yaml @@ -0,0 +1,25 @@ +adapters: + aduptech: + endpoint: https://rtb.d.adup-tech.com/rtb/bid + ortb-version: "2.6" + meta-info: + maintainer-email: support@adup-tech.com + app-media-types: + - banner + - native + site-media-types: + - banner + - native + supported-vendors: + vendor-id: 647 + usersync: + cookie-family-name: aduptech + iframe: + url: https://rtb.d.adup-tech.com/service/sync?iframe=1&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + redirect: + url: https://rtb.d.adup-tech.com/service/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + target-currency: "EUR" diff --git a/src/main/resources/static/bidder-params/aduptech.json b/src/main/resources/static/bidder-params/aduptech.json new file mode 100644 index 00000000000..b2d7a8817ea --- /dev/null +++ b/src/main/resources/static/bidder-params/aduptech.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdUp Tech adapter params", + "description": "A schema which validates params accepted by the AdUp Tech adapter", + "type": "object", + "properties": { + "publisher": { + "type": "string", + "minLength": 1, + "description": "Unique publisher identifier." + }, + "placement": { + "type": "string", + "minLength": 1, + "description": "Unique placement identifier per publisher." + }, + "query": { + "type": "string", + "description": "Semicolon separated list of keywords." + }, + "adtest": { + "type": "boolean", + "description": "Deactivates tracking of impressions and clicks. **Should only be used for testing purposes!**" + }, + "debug": { + "type": "boolean", + "description": "Enables debug mode. **Should only be used for testing purposes!**" + }, + "ext": { + "type": "object", + "description": "Additional parameters to be included in the request." + } + }, + "required": [ + "publisher", + "placement" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/aduptech/AduptechBidderTest.java b/src/test/java/org/prebid/server/bidder/aduptech/AduptechBidderTest.java new file mode 100644 index 00000000000..8b52853beba --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/aduptech/AduptechBidderTest.java @@ -0,0 +1,328 @@ +package org.prebid.server.bidder.aduptech; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +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.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +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.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.prebid.server.bidder.model.BidderError.badInput; +import static org.prebid.server.bidder.model.BidderError.badServerResponse; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +public class AduptechBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + private static final String TARGET_CURRENCY = "EUR"; + + @Mock(strictness = LENIENT) + private CurrencyConversionService currencyConversionService; + + private AduptechBidder target; + + @BeforeEach + public void setUp() { + target = new AduptechBidder(ENDPOINT_URL, jacksonMapper, currencyConversionService, TARGET_CURRENCY); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AduptechBidder("invalid_url", + jacksonMapper, + currencyConversionService, + TARGET_CURRENCY)); + } + + @Test + public void creationShouldFailOnInvalidTargetCurrency() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AduptechBidder(ENDPOINT_URL, + jacksonMapper, + currencyConversionService, + "invalid_currency")) + .withMessage("invalid extra info: invalid TargetCurrency invalid_currency"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfCurrencyConversionFails() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("USD")); + + given(currencyConversionService.convertCurrency(any(), any(), any(), any())) + .willThrow(new PreBidException("test-error")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1).containsExactly(badInput("test-error")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldConvertBidFloorIfCurrencyIsDifferent() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("USD")); + + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "USD", TARGET_CURRENCY)) + .willReturn(BigDecimal.valueOf(12.0)); + + // 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(12.0), TARGET_CURRENCY)); + } + + @Test + public void makeHttpRequestsShouldPerformTwoStepCurrencyConversionIfInitialConversionFails() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("CAD")); + + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "CAD", TARGET_CURRENCY)) + .willThrow(new PreBidException("initial conversion failed")); + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "CAD", "USD")) + .willReturn(BigDecimal.valueOf(8)); + given(currencyConversionService.convertCurrency(BigDecimal.valueOf(8), bidRequest, "USD", TARGET_CURRENCY)) + .willReturn(BigDecimal.valueOf(9.6)); + + // 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(9.6), TARGET_CURRENCY)); + } + + @Test + public void makeHttpRequestsShouldNotModifyImpIfBidFloorIsNotValid() { + // given + final Imp imp = givenImp(builder -> builder.bidfloor(BigDecimal.ZERO).bidfloorcur("USD")); + final BidRequest bidRequest = givenBidRequest(imp); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .containsExactly(imp); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldNotModifyImpIfBidFloorCurrencyIsSameAsTarget() { + // given + final Imp imp = givenImp(builder -> builder.bidfloor(BigDecimal.TEN).bidfloorcur(TARGET_CURRENCY)); + final BidRequest bidRequest = givenBidRequest(imp); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .containsExactly(imp); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldCreateSingleRequestWithAllImps() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("imp1"), + imp -> imp.id("imp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getImpIds) + .containsExactly(Set.of("imp1", "imp2")); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get("Componentid")) + .isEqualTo("prebid-java")) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedEndpointUrl() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getUri) + .isEqualTo(ENDPOINT_URL); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + 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'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidSuccessfully() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(1); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(bannerBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidSuccessfully() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(4); + final BidderCall httpCall = givenHttpCall(givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(nativeBid, BidType.xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorIfMtypeIsUnsupported() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(givenBid(2))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("Unknown markup type: 2")); + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest givenBidRequest(Imp... imps) { + return BidRequest.builder().imp(List.of(imps)).build(); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + final List imps = Arrays.stream(impCustomizers) + .map(AduptechBidderTest::givenImp) + .toList(); + return BidRequest.builder().imp(imps).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("impId")).build(); + } + + private static Bid givenBid(Integer mtype) { + return Bid.builder().mtype(mtype).build(); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/AduptechTest.java b/src/test/java/org/prebid/server/it/AduptechTest.java new file mode 100644 index 00000000000..7db6a6c1217 --- /dev/null +++ b/src/test/java/org/prebid/server/it/AduptechTest.java @@ -0,0 +1,34 @@ +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 AduptechTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAduptech() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/aduptech-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/aduptech/test-aduptech-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/aduptech/test-aduptech-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/aduptech/test-auction-aduptech-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/aduptech/test-auction-aduptech-response.json", response, List.of("aduptech")); + } + +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-request.json new file mode 100644 index 00000000000..a426963a557 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-request.json @@ -0,0 +1,55 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "placement": "placement", + "publisher": "publisher" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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}", + "cur": [ + "USD" + ], + "regs": { + "gdpr": 0 + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-response.json new file mode 100644 index 00000000000..939afc367be --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "mtype": 1, + "cid": "8048" + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-request.json new file mode 100644 index 00000000000..3c1b7c64524 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-request.json @@ -0,0 +1,22 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "aduptech": { + "placement": "placement", + "publisher": "publisher" + } + } + } + ], + "tmax": 5000, + "regs": { + "gdpr": 0 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-response.json new file mode 100644 index 00000000000..f7a658d8047 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-response.json @@ -0,0 +1,40 @@ +{ + "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": "aduptech" + } + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "aduptech", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "aduptech": "{{ aduptech.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} 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 717b0c09445..8e2044b18b0 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -91,6 +91,9 @@ adapters.adtonos.enabled=true adapters.adtonos.endpoint=http://localhost:8090/adtonos-exchange/{{PublisherId}} adapters.adtrgtme.enabled=true adapters.adtrgtme.endpoint=http://localhost:8090/adtrgtme-exchange +adapters.aduptech.enabled=true +adapters.aduptech.endpoint=http://localhost:8090/aduptech-exchange +adapters.aduptech.target-currency=EUR adapters.advangelists.enabled=true adapters.advangelists.endpoint=http://localhost:8090/advangelists-exchange?pubid={{PublisherID}} adapters.adxcg.enabled=true From b939d4633d5d59aee48b2e71f6b991639b3bfd51 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Wed, 9 Jul 2025 14:48:57 +0200 Subject: [PATCH 2/2] Fix comments --- .../bidder/aduptech/AduptechBidder.java | 98 +++++++++---------- .../config/bidder/AduptechConfiguration.java | 4 +- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java b/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java index 943c10a5533..d6a826e9977 100644 --- a/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java +++ b/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java @@ -53,6 +53,15 @@ public AduptechBidder(String endpointUrl, this.targetCurrency = validateCurrency(targetCurrency); } + private static String validateCurrency(String code) { + try { + Currency.getInstance(code); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("invalid extra info: invalid TargetCurrency %s".formatted(code)); + } + return code.toUpperCase(); + } + @Override public Result>> makeHttpRequests(BidRequest request) { final List modifiedImps = new ArrayList<>(request.getImp().size()); @@ -74,6 +83,46 @@ public Result>> makeHttpRequests(BidRequest request return Result.withValue(httpRequest); } + private Imp modifyImp(Imp imp, BidRequest bidRequest) { + Price impFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + impFloorPrice = BidderUtil.isValidPrice(impFloorPrice) + && !targetCurrency.equalsIgnoreCase(impFloorPrice.getCurrency()) + ? convertBidFloor(impFloorPrice, bidRequest) + : impFloorPrice; + + return imp.toBuilder() + .bidfloor(impFloorPrice.getValue()) + .bidfloorcur(impFloorPrice.getCurrency()) + .build(); + } + + private Price convertBidFloor(Price impFloorPrice, BidRequest bidRequest) { + try { + return convertToTargetCurrency(impFloorPrice.getValue(), bidRequest, impFloorPrice.getCurrency()); + } catch (PreBidException e) { + final BigDecimal defaultCurrencyBidFloor = currencyConversionService.convertCurrency( + impFloorPrice.getValue(), + bidRequest, + impFloorPrice.getCurrency(), + DEFAULT_BID_CURRENCY); + return convertToTargetCurrency(defaultCurrencyBidFloor, bidRequest, DEFAULT_BID_CURRENCY); + } + } + + private Price convertToTargetCurrency(BigDecimal impFloorPrice, BidRequest bidRequest, String fromCurrency) { + final BigDecimal convertedFloor = currencyConversionService.convertCurrency( + impFloorPrice, + bidRequest, + fromCurrency, + targetCurrency); + + return Price.of(targetCurrency, convertedFloor); + } + + private static MultiMap makeHeaders() { + return HttpUtil.headers().add(COMPONENT_ID_HEADER, COMPONENT_ID_HEADER_VALUE); + } + @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { @@ -85,19 +134,6 @@ public Result> makeBids(BidderCall httpCall, BidRequ } } - private static String validateCurrency(String code) { - try { - Currency.getInstance(code); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("invalid extra info: invalid TargetCurrency %s".formatted(code)); - } - return code.toUpperCase(); - } - - private static MultiMap makeHeaders() { - return HttpUtil.headers().add(COMPONENT_ID_HEADER, COMPONENT_ID_HEADER_VALUE); - } - private static List extractBids(BidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); @@ -133,40 +169,4 @@ private static BidType getBidType(Integer markupType) { case null, default -> throw new PreBidException("Unknown markup type: " + markupType); }; } - - private Imp modifyImp(Imp imp, BidRequest bidRequest) { - Price impFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); - impFloorPrice = BidderUtil.isValidPrice(impFloorPrice) - && !targetCurrency.equalsIgnoreCase(impFloorPrice.getCurrency()) - ? convertBidFloor(impFloorPrice, bidRequest) - : impFloorPrice; - - return imp.toBuilder() - .bidfloor(impFloorPrice.getValue()) - .bidfloorcur(impFloorPrice.getCurrency()) - .build(); - } - - private Price convertBidFloor(Price impFloorPrice, BidRequest bidRequest) { - try { - return convertToTargetCurrency(impFloorPrice.getValue(), bidRequest, impFloorPrice.getCurrency()); - } catch (PreBidException e) { - final BigDecimal defaultCurrencyBidFloor = currencyConversionService.convertCurrency( - impFloorPrice.getValue(), - bidRequest, - impFloorPrice.getCurrency(), - DEFAULT_BID_CURRENCY); - return convertToTargetCurrency(defaultCurrencyBidFloor, bidRequest, DEFAULT_BID_CURRENCY); - } - } - - private Price convertToTargetCurrency(BigDecimal impFloorPrice, BidRequest bidRequest, String fromCurrency) { - final BigDecimal convertedFloor = currencyConversionService.convertCurrency( - impFloorPrice, - bidRequest, - fromCurrency, - targetCurrency); - - return Price.of(targetCurrency, convertedFloor); - } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java index 6dfff25f396..5011fc46c41 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java @@ -45,8 +45,8 @@ BidderDeps aduptechBidderDeps(AduptechConfigurationProperties aduptechConfigurat config.getEndpoint(), mapper, currencyConversionService, - config.getTargetCurrency()) - ).assemble(); + config.getTargetCurrency())) + .assemble(); } @Data