From e79eabd007d955af4acde87d86ed58a43ac5146a Mon Sep 17 00:00:00 2001 From: antonbabak Date: Mon, 26 May 2025 15:25:54 +0200 Subject: [PATCH 1/2] New Madsense Adapter --- .../bidder/madsense/MadsenseBidder.java | 185 ++++++++ .../ext/request/madsense/ExtImpMadsense.java | 9 + .../config/bidder/MadsenseConfiguration.java | 41 ++ .../resources/bidder-config/madsense.yaml | 13 + .../static/bidder-params/madsense.json | 16 + .../bidder/madsense/MadsenseBidderTest.java | 399 ++++++++++++++++++ .../org/prebid/server/it/MadsenseTest.java | 33 ++ .../test-auction-madsense-request.json | 23 + .../test-auction-madsense-response.json | 40 ++ .../madsense/test-madsense-bid-request.json | 56 +++ .../madsense/test-madsense-bid-response.json | 19 + .../server/it/test-application.properties | 2 + 12 files changed, 836 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/madsense/ExtImpMadsense.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/MadsenseConfiguration.java create mode 100644 src/main/resources/bidder-config/madsense.yaml create mode 100644 src/main/resources/static/bidder-params/madsense.json create mode 100644 src/test/java/org/prebid/server/bidder/madsense/MadsenseBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/MadsenseTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java b/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java new file mode 100644 index 00000000000..b97594d5019 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java @@ -0,0 +1,185 @@ +package org.prebid.server.bidder.madsense; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +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 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.Result; +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.madsense.ExtImpMadsense; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class MadsenseBidder implements Bidder { + + private static final String X_OPENRTB_VERSION_HEADER_VALUE = "2.6"; + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MadsenseBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> httpRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + final List videoImps = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + if (imp.getBanner() != null) { + try { + httpRequests.add(makeHttpRequest(request, Collections.singletonList(imp))); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } else if (imp.getVideo() != null) { + videoImps.add(imp); + } + } + + if (CollectionUtils.isNotEmpty(videoImps)) { + try { + httpRequests.add(makeHttpRequest(request, videoImps)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpMadsense parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing imp.ext parameters"); + } + } + + private HttpRequest makeHttpRequest(BidRequest request, List imps) { + final Imp firstImp = request.getImp().getFirst(); + final ExtImpMadsense extImp = parseImpExt(firstImp); + final String companyId = Objects.equals(request.getTest(), 1) ? "test" : extImp.getCompanyId(); + return BidderUtil.defaultRequest( + request.toBuilder().imp(imps).build(), + makeHeaders(request), + getEndpointUrlInternal(companyId), + mapper); + } + + private static MultiMap makeHeaders(BidRequest request) { + final MultiMap headers = HttpUtil.headers() + .set(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION_HEADER_VALUE); + + final Device device = request.getDevice(); + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + } + + final Site site = request.getSite(); + if (site != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ORIGIN_HEADER, site.getDomain()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getRef()); + } + + return headers; + } + + private String getEndpointUrlInternal(String companyId) { + return endpointUrl + "?company_id=" + HttpUtil.encodeUrl(companyId); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final List errors = new ArrayList<>(); + try { + 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 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) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, List errors) { + try { + final BidType bidType = getBidType(bid); + return BidderBid.builder() + .bid(bid) + .bidCurrency(currency) + .videoInfo(bidType == BidType.video + ? ExtBidPrebidVideo.of(resolveDuration(bid), resolveCategory(bid)) + : null) + .type(bidType) + .build(); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case null, default -> throw new PreBidException( + "Unsupported bid mediaType: %s for impression: %s".formatted(bid.getMtype(), bid.getImpid())); + }; + } + + private static String resolveCategory(Bid bid) { + final List categories = bid.getCat(); + return CollectionUtils.isEmpty(categories) ? null : categories.getFirst(); + } + + private static Integer resolveDuration(Bid bid) { + final Integer duration = bid.getDur(); + return duration != null && duration > 0 ? duration : null; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/madsense/ExtImpMadsense.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/madsense/ExtImpMadsense.java new file mode 100644 index 00000000000..6725007f682 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/madsense/ExtImpMadsense.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.openrtb.ext.request.madsense; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMadsense { + + String companyId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MadsenseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MadsenseConfiguration.java new file mode 100644 index 00000000000..a1cc34057e3 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MadsenseConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.madsense.MadsenseBidder; +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; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/madsense.yaml", factory = YamlPropertySourceFactory.class) +public class MadsenseConfiguration { + + private static final String BIDDER_NAME = "madsense"; + + @Bean("madsenseConfigurationProperties") + @ConfigurationProperties("adapters.madsense") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps madsenseBidderDeps(BidderConfigurationProperties madsenseConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(madsenseConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MadsenseBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/madsense.yaml b/src/main/resources/bidder-config/madsense.yaml new file mode 100644 index 00000000000..29dc214b093 --- /dev/null +++ b/src/main/resources/bidder-config/madsense.yaml @@ -0,0 +1,13 @@ +adapters: + madsense: + endpoint: https://ads.madsense.io/pbs + meta-info: + maintainer-email: prebid@madsense.io + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/static/bidder-params/madsense.json b/src/main/resources/static/bidder-params/madsense.json new file mode 100644 index 00000000000..f45ac81f3ed --- /dev/null +++ b/src/main/resources/static/bidder-params/madsense.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "madSense Adapter Params", + "description": "A schema which validates params accepted by the madSense adapter", + "type": "object", + "properties": { + "company_id": { + "type": "string", + "description": "An id used to identify madSense company", + "minLength": 1 + } + }, + "required": [ + "company_id" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/madsense/MadsenseBidderTest.java b/src/test/java/org/prebid/server/bidder/madsense/MadsenseBidderTest.java new file mode 100644 index 00000000000..1fcd3106a43 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/madsense/MadsenseBidderTest.java @@ -0,0 +1,399 @@ +package org.prebid.server.bidder.madsense; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Site; +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.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.madsense.ExtImpMadsense; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; + +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.emptyList; +import static java.util.Collections.singletonList; +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.prebid.server.bidder.model.BidderError.Type; +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.prebid.server.util.HttpUtil.ORIGIN_HEADER; +import static org.prebid.server.util.HttpUtil.REFERER_HEADER; +import static org.prebid.server.util.HttpUtil.USER_AGENT_HEADER; +import static org.prebid.server.util.HttpUtil.X_FORWARDED_FOR_HEADER; +import static org.prebid.server.util.HttpUtil.X_OPENRTB_VERSION_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +public class MadsenseBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://ads.madsense.io/pbs"; + + private MadsenseBidder target; + + @BeforeEach + public void setUp() { + target = new MadsenseBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new MadsenseBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenFirstImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp + .banner(Banner.builder().build()) + .ext(givenInvalidImpExt())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1).containsExactly(badInput("Error parsing imp.ext parameters")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldCreateSeparateRequestForEachBannerImp() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder.id("bannerImp1").banner(Banner.builder().build()) + .ext(givenImpExt("testCompanyId")), + builder -> builder.id("bannerImp2").banner(Banner.builder().build()) + .ext(givenImpExt("otherCompanyId"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2); + assertThat(result.getValue()) + .extracting(HttpRequest::getImpIds, r -> r.getPayload().getImp().getFirst().getId()) + .containsExactlyInAnyOrder( + tuple(Set.of("bannerImp1"), "bannerImp1"), + tuple(Set.of("bannerImp2"), "bannerImp2") + ); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsOnly(ENDPOINT_URL + "?company_id=testCompanyId"); + } + + @Test + public void makeHttpRequestsShouldCreateOneRequestForAllVideoImps() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder.id("videoImp1").video(Video.builder().build()) + .ext(givenImpExt("testCompanyId")), + builder -> builder.id("videoImp2").video(Video.builder().build()) + .ext(givenImpExt("otherCompanyId"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().getFirst().getImpIds()).containsExactlyInAnyOrder("videoImp1", "videoImp2"); + assertThat(result.getValue().getFirst().getPayload().getImp()) + .extracting(Imp::getId) + .containsExactlyInAnyOrder("videoImp1", "videoImp2"); + assertThat(result.getValue().getFirst().getUri()) + .isEqualTo(ENDPOINT_URL + "?company_id=testCompanyId"); + } + + @Test + public void makeHttpRequestsShouldHandleMixedBannerAndVideoImps() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder.id("bannerImp1").banner(Banner.builder().build()) + .ext(givenImpExt("testCompanyId")), + builder -> builder.id("videoImp1").video(Video.builder().build()) + .ext(givenImpExt("otherCompanyId1")), + builder -> builder.id("bannerImp2").banner(Banner.builder().build()) + .ext(givenImpExt("otherCompanyId2")), + builder -> builder.id("videoImp2").video(Video.builder().build()) + .ext(givenImpExt("otherCompanyId3"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(3); + + assertThat(result.getValue()) + .filteredOn(req -> req.getPayload().getImp().getFirst().getBanner() != null) + .hasSize(2) + .extracting( + HttpRequest::getImpIds, + request -> request.getPayload().getImp().getFirst().getId()) + .containsExactlyInAnyOrder( + tuple(Set.of("bannerImp1"), "bannerImp1"), + tuple(Set.of("bannerImp2"), "bannerImp2")); + + assertThat(result.getValue()) + .filteredOn(req -> req.getPayload().getImp().getFirst().getVideo() != null) + .hasSize(1) + .first() + .satisfies(videoReq -> { + assertThat(videoReq.getImpIds()).containsExactlyInAnyOrder("videoImp1", "videoImp2"); + assertThat(videoReq.getPayload().getImp()) + .extracting(Imp::getId) + .containsExactlyInAnyOrder("videoImp1", "videoImp2"); + }); + + assertThat(result.getValue()) + .allSatisfy(req -> assertThat(req.getUri()) + .isEqualTo(ENDPOINT_URL + "?company_id=testCompanyId")); + } + + @Test + public void makeHttpRequestsShouldUseTestCompanyIdWhenRequestTestIsOne() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder.banner(Banner.builder().build()).ext(givenImpExt("testCompanyId"))) + .toBuilder() + .test(1) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly(ENDPOINT_URL + "?company_id=test"); + } + + @Test + public void makeHttpRequestsShouldSetCorrectHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.banner(Banner.builder().build())) + .toBuilder() + .device(Device.builder().ua("ua").ip("ip").ipv6("ipv6").build()) + .site(Site.builder().domain("domain").ref("referrer").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> { + assertThat(headers.get(CONTENT_TYPE_HEADER)).isEqualTo(APPLICATION_JSON_CONTENT_TYPE); + assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE); + assertThat(headers.get(X_OPENRTB_VERSION_HEADER)).isEqualTo("2.6"); + assertThat(headers.get(USER_AGENT_HEADER)).isEqualTo("ua"); + assertThat(headers.get(X_FORWARDED_FOR_HEADER)).isEqualTo("ip"); + assertThat(headers.get(ORIGIN_HEADER)).isEqualTo("domain"); + assertThat(headers.get(REFERER_HEADER)).isEqualTo("referrer"); + }); + } + + @Test + public void makeHttpRequestsShouldSetIpv6WhenIpIsMissing() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.banner(Banner.builder().build())) + .toBuilder() + .device(Device.builder().ipv6("ipv6").ip(null).build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue().getFirst().getHeaders().get(X_FORWARDED_FOR_HEADER)) + .isEqualTo("ipv6"); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid_json"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid_json'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListWhenBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListWhenSeatBidIsEmpty() throws JsonProcessingException { + // given + final BidResponse bidResponseEmptySeatBid = BidResponse.builder().seatbid(emptyList()).build(); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponseEmptySeatBid)); + + // when + final Result> resultEmptySeatBid = target.makeBids(httpCall, null); + + // then + assertThat(resultEmptySeatBid.getErrors()).isEmpty(); + assertThat(resultEmptySeatBid.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidSuccessfully() throws JsonProcessingException { + // given + final Bid bannerBid = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE) + .adm("adm1").mtype(1).cat(singletonList("cat1")).build(); + final BidResponse bidResponse = BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(singletonList(bannerBid)).build())) + .build(); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse)); + + // 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 makeBidsShouldReturnVideoBidSuccessfullyWithVideoInfo() throws JsonProcessingException { + // given + final Bid videoBid = Bid.builder().id("bidId1").mtype(2).cat(singletonList("cat-video")).dur(30).build(); + final BidResponse bidResponse = BidResponse.builder() + .cur("EUR") + .seatbid(singletonList(SeatBid.builder().bid(singletonList(videoBid)).build())) + .build(); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + final BidderBid expectedBidderBid = BidderBid.builder() + .bid(videoBid) + .type(BidType.video) + .bidCurrency("EUR") + .videoInfo(ExtBidPrebidVideo.of(30, "cat-video")) + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).containsExactly(expectedBidderBid); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidMtypeIsMissing() throws JsonProcessingException { + // given + final Bid bid = Bid.builder().id("bidId1").impid("impId1").build(); + final BidResponse bidResponse = BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(singletonList(bid)).build())) + .build(); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("Unsupported bid mediaType: null for impression: impId1")); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidMtypeIsUnsupported() throws JsonProcessingException { + // given + final Bid bid = Bid.builder().id("bidId1").impid("impId1").mtype(3).build(); + final BidResponse bidResponse = BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bid)).build())) + .build(); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("Unsupported bid mediaType: 3 for impression: impId1")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + final List imps = Arrays.stream(impCustomizers) + .map(MadsenseBidderTest::givenImp) + .toList(); + return BidRequest.builder().imp(imps).cur(singletonList("USD")).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("test-imp-id") + .ext(givenImpExt("testCompanyId"))) + .build(); + } + + private static ObjectNode givenImpExt(String companyId) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpMadsense.of(companyId))); + } + + private static ObjectNode givenInvalidImpExt() { + return mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())); + } + + 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/MadsenseTest.java b/src/test/java/org/prebid/server/it/MadsenseTest.java new file mode 100644 index 00000000000..fd7366fafb6 --- /dev/null +++ b/src/test/java/org/prebid/server/it/MadsenseTest.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 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; +import static java.util.Collections.singletonList; + +public class MadsenseTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMadsense() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/madsense-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/madsense/test-madsense-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/madsense/test-madsense-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/madsense/test-auction-madsense-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/madsense/test-auction-madsense-response.json", response, + singletonList("madsense")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-request.json b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-request.json new file mode 100644 index 00000000000..6dd62c37a6d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "madsense": { + "company_id": "companyId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-response.json b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-response.json new file mode 100644 index 00000000000..cab85202f9e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-response.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 0.01, + "adid": "adid", + "cid": "cid", + "crid": "crid", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "madsense" + } + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "madsense", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "madsense": "{{ madsense.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-request.json new file mode 100644 index 00000000000..69b9eb55ae2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "company_id": "companyId" + } + } + } + ], + "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": { + "ext": { + "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/madsense/test-madsense-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-response.json new file mode 100644 index 00000000000..0720cdf8a5e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "crid": "crid", + "adid": "adid", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "cid", + "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 450ae419cb5..c2d134d9bf0 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -333,6 +333,8 @@ adapters.lunamedia.enabled=true adapters.lunamedia.endpoint=http://localhost:8090/lunamedia-exchange?pubid= adapters.mabidder.enabled=true adapters.mabidder.endpoint=http://localhost:8090/mabidder-exchange +adapters.madsense.enabled=true +adapters.madsense.endpoint=http://localhost:8090/madsense-exchange adapters.madvertise.enabled=true adapters.madvertise.endpoint=http://localhost:8090/madvertise-exchange adapters.marsmedia.enabled=true From c3e11119c488da79a2abe8c7e56a17ebfea25a77 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Fri, 6 Jun 2025 09:55:53 +0200 Subject: [PATCH 2/2] Fix comments --- .../bidder/madsense/MadsenseBidder.java | 5 ++- .../bidder/madsense/MadsenseBidderTest.java | 43 +++---------------- 2 files changed, 9 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java b/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java index b97594d5019..5fdfe844c19 100644 --- a/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java +++ b/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java @@ -90,7 +90,7 @@ private HttpRequest makeHttpRequest(BidRequest request, List im return BidderUtil.defaultRequest( request.toBuilder().imp(imps).build(), makeHeaders(request), - getEndpointUrlInternal(companyId), + makeEndpoint(companyId), mapper); } @@ -114,7 +114,7 @@ private static MultiMap makeHeaders(BidRequest request) { return headers; } - private String getEndpointUrlInternal(String companyId) { + private String makeEndpoint(String companyId) { return endpointUrl + "?company_id=" + HttpUtil.encodeUrl(companyId); } @@ -142,6 +142,7 @@ private static List bidsFromResponse(BidResponse bidResponse, List makeBidderBid(bid, bidResponse.getCur(), errors)) .filter(Objects::nonNull) .toList(); diff --git a/src/test/java/org/prebid/server/bidder/madsense/MadsenseBidderTest.java b/src/test/java/org/prebid/server/bidder/madsense/MadsenseBidderTest.java index 1fcd3106a43..ac4933dc439 100644 --- a/src/test/java/org/prebid/server/bidder/madsense/MadsenseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/madsense/MadsenseBidderTest.java @@ -36,7 +36,6 @@ import static java.util.Collections.singletonList; 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.prebid.server.bidder.model.BidderError.Type; import static org.prebid.server.bidder.model.BidderError.badInput; import static org.prebid.server.bidder.model.BidderError.badServerResponse; @@ -98,11 +97,9 @@ public void makeHttpRequestsShouldCreateSeparateRequestForEachBannerImp() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(2); assertThat(result.getValue()) - .extracting(HttpRequest::getImpIds, r -> r.getPayload().getImp().getFirst().getId()) - .containsExactlyInAnyOrder( - tuple(Set.of("bannerImp1"), "bannerImp1"), - tuple(Set.of("bannerImp2"), "bannerImp2") - ); + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Set.of("bannerImp1"), Set.of("bannerImp2")); + assertThat(result.getValue()) .extracting(HttpRequest::getUri) .containsOnly(ENDPOINT_URL + "?company_id=testCompanyId"); @@ -124,11 +121,6 @@ public void makeHttpRequestsShouldCreateOneRequestForAllVideoImps() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1); assertThat(result.getValue().getFirst().getImpIds()).containsExactlyInAnyOrder("videoImp1", "videoImp2"); - assertThat(result.getValue().getFirst().getPayload().getImp()) - .extracting(Imp::getId) - .containsExactlyInAnyOrder("videoImp1", "videoImp2"); - assertThat(result.getValue().getFirst().getUri()) - .isEqualTo(ENDPOINT_URL + "?company_id=testCompanyId"); } @Test @@ -149,32 +141,9 @@ public void makeHttpRequestsShouldHandleMixedBannerAndVideoImps() { // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(3); - - assertThat(result.getValue()) - .filteredOn(req -> req.getPayload().getImp().getFirst().getBanner() != null) - .hasSize(2) - .extracting( - HttpRequest::getImpIds, - request -> request.getPayload().getImp().getFirst().getId()) - .containsExactlyInAnyOrder( - tuple(Set.of("bannerImp1"), "bannerImp1"), - tuple(Set.of("bannerImp2"), "bannerImp2")); - - assertThat(result.getValue()) - .filteredOn(req -> req.getPayload().getImp().getFirst().getVideo() != null) - .hasSize(1) - .first() - .satisfies(videoReq -> { - assertThat(videoReq.getImpIds()).containsExactlyInAnyOrder("videoImp1", "videoImp2"); - assertThat(videoReq.getPayload().getImp()) - .extracting(Imp::getId) - .containsExactlyInAnyOrder("videoImp1", "videoImp2"); - }); - - assertThat(result.getValue()) - .allSatisfy(req -> assertThat(req.getUri()) - .isEqualTo(ENDPOINT_URL + "?company_id=testCompanyId")); + assertThat(result.getValue()).hasSize(3) + .extracting(HttpRequest::getImpIds) + .containsExactly(Set.of("bannerImp1"), Set.of("bannerImp2"), Set.of("videoImp1", "videoImp2")); } @Test