diff --git a/src/main/java/org/prebid/server/bidder/alvads/AlvadsBidder.java b/src/main/java/org/prebid/server/bidder/alvads/AlvadsBidder.java new file mode 100644 index 00000000000..dc111550a9f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/alvads/AlvadsBidder.java @@ -0,0 +1,186 @@ +package org.prebid.server.bidder.alvads; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +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 io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.alvads.model.AlvaAdsImp; +import org.prebid.server.bidder.alvads.model.AlvaAdsSite; +import org.prebid.server.bidder.alvads.model.AlvadsRequestOrtb; +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.alvads.AlvadsImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +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.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class AlvadsBidder implements Bidder { + + private static final TypeReference> ALVADS_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AlvadsBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + + bidRequest.getImp().forEach(imp -> { + try { + final AlvadsImpExt impExt = parseImpExt(imp); + final HttpRequest request = makeHttpRequest(bidRequest, imp, impExt); + httpRequests.add(request); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + }); + + return httpRequests.isEmpty() ? Result.withErrors(errors) : Result.of(httpRequests, errors); + } + + private AlvadsImpExt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ALVADS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Missing or invalid bidder ext in impression with id: " + imp.getId()); + } + } + + private HttpRequest makeHttpRequest(BidRequest request, Imp imp, AlvadsImpExt impExt) { + final AlvaAdsImp impObj = makeImp(imp); + final AlvaAdsSite siteObj = makeSite(request.getSite(), impExt.getPublisherUniqueId()); + final AlvadsRequestOrtb alvadsRequest = AlvadsRequestOrtb.builder() + .id(request.getId()) + .imp(List.of(impObj)) + .device(request.getDevice()) + .user(request.getUser()) + .regs(request.getRegs()) + .site(siteObj) + .build(); + + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .payload(alvadsRequest) + .body(mapper.encodeToBytes(alvadsRequest)) + .impIds(alvadsRequest.getImp().stream().map(AlvaAdsImp::getId).collect(Collectors.toSet())) + .build(); + } + + private static AlvaAdsImp makeImp(Imp imp) { + final Banner banner = imp.getBanner(); + final Video video = imp.getVideo(); + + return AlvaAdsImp.builder() + .id(imp.getId()) + .tagid(imp.getTagid()) + .bidfloor(imp.getBidfloor()) + .banner(banner != null ? sizes(banner.getW(), banner.getH()) : null) + .video(video != null ? sizes(video.getW(), video.getH()) : null) + .build(); + } + + private static Map sizes(Integer w, Integer h) { + return (w == null || h == null) ? null : Map.of("w", w, "h", h); + } + + private static AlvaAdsSite makeSite(Site site, String publisherUniqueId) { + final String page = site != null ? site.getPage() : null; + return AlvaAdsSite.builder() + .page(page) + .ref(page) + .publisher(Map.of("id", publisherUniqueId)) + .build(); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse, httpCall.getRequest().getPayload())); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse("Failed to decode BidResponse: " + e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, AlvadsRequestOrtb request) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, request); + } + + private List bidsFromResponse(BidResponse bidResponse, AlvadsRequestOrtb request) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, request, bidResponse.getCur())) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBid(Bid bid, AlvadsRequestOrtb request, String currency) { + return request.getImp().stream() + .filter(i -> i.getId().equals(bid.getImpid())) + .findFirst() + .map(imp -> BidderBid.of(bid, getBidType(bid, imp), currency)) + .orElse(null); + } + + private BidType getBidType(Bid bid, AlvaAdsImp imp) { + if (imp == null) { + return BidType.banner; + } + + if (imp.getVideo() != null) { + return BidType.video; + } + + return Optional.ofNullable(getBidExt(bid)) + .map(ExtBidAlvads::getCrtype) + .orElse(BidType.banner); + } + + private ExtBidAlvads getBidExt(Bid bid) { + try { + return mapper.mapper().convertValue(bid.getExt(), ExtBidAlvads.class); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/alvads/ExtBidAlvads.java b/src/main/java/org/prebid/server/bidder/alvads/ExtBidAlvads.java new file mode 100644 index 00000000000..2792260095e --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/alvads/ExtBidAlvads.java @@ -0,0 +1,10 @@ +package org.prebid.server.bidder.alvads; + +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +@Value +public class ExtBidAlvads { + + BidType crtype; +} diff --git a/src/main/java/org/prebid/server/bidder/alvads/model/AlvaAdsImp.java b/src/main/java/org/prebid/server/bidder/alvads/model/AlvaAdsImp.java new file mode 100644 index 00000000000..c339f5882df --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/alvads/model/AlvaAdsImp.java @@ -0,0 +1,22 @@ +package org.prebid.server.bidder.alvads.model; + +import lombok.Builder; +import lombok.Value; + +import java.math.BigDecimal; +import java.util.Map; + +@Value +@Builder +public class AlvaAdsImp { + + String id; + + Map banner; + + Map video; + + String tagid; + + BigDecimal bidfloor; +} diff --git a/src/main/java/org/prebid/server/bidder/alvads/model/AlvaAdsSite.java b/src/main/java/org/prebid/server/bidder/alvads/model/AlvaAdsSite.java new file mode 100644 index 00000000000..cc6626c4190 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/alvads/model/AlvaAdsSite.java @@ -0,0 +1,17 @@ +package org.prebid.server.bidder.alvads.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Value +@Builder +public class AlvaAdsSite { + + String page; + + String ref; + + Map publisher; +} diff --git a/src/main/java/org/prebid/server/bidder/alvads/model/AlvadsRequestOrtb.java b/src/main/java/org/prebid/server/bidder/alvads/model/AlvadsRequestOrtb.java new file mode 100644 index 00000000000..4683a8500b6 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/alvads/model/AlvadsRequestOrtb.java @@ -0,0 +1,26 @@ +package org.prebid.server.bidder.alvads.model; + +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.User; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Value +@Builder(toBuilder = true) +public class AlvadsRequestOrtb { + + String id; + + List imp; + + Device device; + + User user; + + Regs regs; + + AlvaAdsSite site; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/alvads/AlvadsImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/alvads/AlvadsImpExt.java new file mode 100644 index 00000000000..86e4afe0a78 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/alvads/AlvadsImpExt.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.alvads; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AlvadsImpExt { + + @JsonProperty("publisherUniqueId") + String publisherUniqueId; + +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AlvadsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AlvadsConfiguration.java new file mode 100644 index 00000000000..b345661957c --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AlvadsConfiguration.java @@ -0,0 +1,42 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.alvads.AlvadsBidder; +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/alvads.yaml", factory = YamlPropertySourceFactory.class) +public class AlvadsConfiguration { + + private static final String BIDDER_NAME = "alvads"; + + @Bean("alvadsConfigurationProperties") + @ConfigurationProperties("adapters.alvads") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps alvadsBidderDeps( + BidderConfigurationProperties alvadsConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(alvadsConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AlvadsBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/alvads.yaml b/src/main/resources/bidder-config/alvads.yaml new file mode 100644 index 00000000000..2fddd965a67 --- /dev/null +++ b/src/main/resources/bidder-config/alvads.yaml @@ -0,0 +1,14 @@ +adapters: + alvads: + endpoint: https://helios-ads-qa-core.ssidevops.com/decision/openrtb + meta-info: + maintainer-email: alvads@oyealva.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 0 + diff --git a/src/main/resources/static/bidder-params/alvads.json b/src/main/resources/static/bidder-params/alvads.json new file mode 100644 index 00000000000..e7b4a6096b3 --- /dev/null +++ b/src/main/resources/static/bidder-params/alvads.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Alvads Adapter Params", + "description": "A schema which validates params accepted by the Alvads adapter", + "type": "object", + + "properties": { + "publisherUniqueId": { + "type": "string", + "description": "Publisher Unique Id" + } + }, + + "required": ["publisherUniqueId"] +} diff --git a/src/test/java/org/prebid/server/bidder/alvads/AlvadsBidderTest.java b/src/test/java/org/prebid/server/bidder/alvads/AlvadsBidderTest.java new file mode 100644 index 00000000000..0cd7b3600ce --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/alvads/AlvadsBidderTest.java @@ -0,0 +1,374 @@ +package org.prebid.server.bidder.alvads; + +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.Regs; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +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 io.vertx.core.MultiMap; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.alvads.model.AlvaAdsImp; +import org.prebid.server.bidder.alvads.model.AlvadsRequestOrtb; +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.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +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.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; + +public class AlvadsBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://helios-ads-qa-core.ssidevops.com/decision/openrtb"; + + private final AlvadsBidder target = new AlvadsBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AlvadsBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorForInvalidImpExt() { + // given + final ObjectNode extNode = jacksonMapper.mapper().createObjectNode(); + extNode.put("bidder", "invalid"); + + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of(Imp.builder().id("1").ext(extNode).build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isNotEmpty(); + assertThat(result.getErrors().get(0).getMessage()) + .contains("Missing or invalid bidder ext"); + } + + @Test + public void makeHttpRequestsShouldBuildValidHttpRequestsUrl() { + // given + final BidRequest bidRequest = createBidRequestWithBannerAndVideo(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .allMatch(uri -> uri.equals(ENDPOINT_URL)); + } + + @Test + public void makeHttpRequestsShouldBuildValidHttpRequestsHeaders() { + // given + final Imp bannerImp = createImp("imp-banner", "pub-1", 300, 250); + final BidRequest bidRequest = createBidRequest(List.of(bannerImp)); + + // 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)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + } + + @Test + public void makeHttpRequestsShouldBuildValidHttpRequestsImpIds() { + // given + final BidRequest bidRequest = createBidRequestWithBannerAndVideo(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue().get(0).getImpIds()).contains("imp-1"); + assertThat(result.getValue().get(1).getImpIds()).contains("imp-2"); + } + + @Test + public void makeHttpRequestsShouldBuildValidHttpRequestsImpContent() { + // given + final BidRequest bidRequest = createBidRequestWithBannerAndVideo(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final HttpRequest req1 = result.getValue().get(0); + final HttpRequest req2 = result.getValue().get(1); + + assertThat(req1.getPayload().getImp()) + .hasSize(1) + .extracting("id", "banner", "video") + .containsExactly(tuple("imp-1", Map.of("w", 300, "h", 250), null)); + + assertThat(req2.getPayload().getImp()) + .hasSize(1) + .extracting("id", "banner", "video") + .containsExactly(tuple("imp-2", null, Map.of("w", 640, "h", 480))); + } + + @Test + public void makeHttpRequestsShouldBuildValidHttpRequestsFromInput() { + // given + final BidRequest bidRequest = createBidRequestWithBannerAndVideo(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final HttpRequest req1 = result.getValue().get(0); + final HttpRequest req2 = result.getValue().get(1); + + assertThat(req1.getPayload().getId()).isEqualTo("req-123"); + assertThat(req2.getPayload().getId()).isEqualTo("req-123"); + + assertThat(req1.getPayload().getSite().getPage()) + .isEqualTo("https://example.com"); + assertThat(req2.getPayload().getSite().getPage()) + .isEqualTo("https://example.com"); + + Device expectedDevice = bidRequest.getDevice(); + assertThat(req1.getPayload().getDevice()).isEqualTo(expectedDevice); + assertThat(req2.getPayload().getDevice()).isEqualTo(expectedDevice); + + User expectedUser = bidRequest.getUser(); + assertThat(req1.getPayload().getUser()).isEqualTo(expectedUser); + assertThat(req2.getPayload().getUser()).isEqualTo(expectedUser); + + Regs expectedRegs = bidRequest.getRegs(); + assertThat(req1.getPayload().getRegs()).isEqualTo(expectedRegs); + assertThat(req2.getPayload().getRegs()).isEqualTo(expectedRegs); + + assertThat(req1.getPayload().getImp()) + .hasSize(1) + .extracting("id", "banner", "video") + .containsExactly( + tuple("imp-1", + Map.of("w", 300, "h", 250), + null)); + + assertThat(req2.getPayload().getImp()) + .hasSize(1) + .extracting("id", "banner", "video") + .containsExactly( + tuple("imp-2", + null, + Map.of("w", 640, "h", 480))); + + } + + @Test + public void makeBidsShouldReturnEmptyListForEmptyResponse() { + // given + final BidderCall call = buildBidderCall(List.of(), List.of(), "USD"); + + // when + final Result> result = target.makeBids(call, createBidRequest(List.of())); + + // then + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidderBid() { + // given + final Imp bannerImp = createImp("imp-banner", "pub-1", 300, 250); + final BidRequest bidRequest = createBidRequest(List.of(bannerImp)); + + final Bid bannerBid = createBid("bid-banner", "imp-banner", 1.5); + final SeatBid seatBid = createSeatBid(bannerBid); + + final BidderCall call = buildBidderCall( + List.of(createAlvadsRequestImp("imp-banner", 300, 250)), + List.of(seatBid), + "USD"); + + // when + final Result> result = target.makeBids(call, bidRequest); + + // then + assertThat(result.getValue()).containsExactly(BidderBid.of(bannerBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidderBid() { + // given + final Imp videoImp = createImp("imp-video", "pub-2", 640, 480); + final BidRequest bidRequest = createBidRequest(List.of(videoImp)); + + final Bid videoBid = createBid("bid-video", "imp-video", 2.5); + final SeatBid seatBid = createSeatBid(videoBid); + + final BidderCall call = buildBidderCall( + List.of(createAlvadsRequestImp("imp-video", 640, 480)), + List.of(seatBid), + "USD"); + + // when + final Result> result = target.makeBids(call, bidRequest); + + // then + assertThat(result.getValue()).containsExactly(BidderBid.of(videoBid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldIgnoreUnsupportedBidType() { + // given + final Imp unknownImp = createImp("imp-unknown", "pub-3", 100, 100); + final BidRequest bidRequest = createBidRequest(List.of(unknownImp)); + + final Bid unknownBid = createBid("bid-unknown", "imp-unknown", 1.0); + final SeatBid seatBid = createSeatBid(unknownBid); + + final BidderCall call = buildBidderCall(List.of(), List.of(seatBid), "USD"); + + // when + final Result> result = target.makeBids(call, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest createBidRequestWithBannerAndVideo() { + final ObjectNode bidderNode1 = jacksonMapper.mapper().createObjectNode(); + bidderNode1.put("publisherUniqueId", "pub-1"); + bidderNode1.put("endpointUrl", ENDPOINT_URL); + + final ObjectNode impExtNode1 = jacksonMapper.mapper().createObjectNode(); + impExtNode1.set("bidder", bidderNode1); + + final ObjectNode bidderNode2 = jacksonMapper.mapper().createObjectNode(); + bidderNode2.put("publisherUniqueId", "pub-2"); + bidderNode2.put("endpointUrl", ENDPOINT_URL); + + final ObjectNode impExtNode2 = jacksonMapper.mapper().createObjectNode(); + impExtNode2.set("bidder", bidderNode2); + + final Imp imp1 = Imp.builder() + .id("imp-1") + .banner(Banner.builder().w(300).h(250).build()) + .ext(impExtNode1) + .build(); + + final Imp imp2 = Imp.builder() + .id("imp-2") + .video(Video.builder().w(640).h(480).build()) + .ext(impExtNode2) + .build(); + + final Site site = Site.builder().page("https://example.com").build(); + + return BidRequest.builder() + .id("req-123") + .imp(List.of(imp1, imp2)) + .site(site) + .device(Device.builder().build()) + .build(); + } + + private static Bid createBid(String id, String impId, double price) { + return Bid.builder() + .id(id) + .impid(impId) + .price(BigDecimal.valueOf(price)) + .build(); + } + + private static SeatBid createSeatBid(Bid... bids) { + return SeatBid.builder() + .bid(Arrays.asList(bids)) + .build(); + } + + private static BidResponse createBidResponse(List seatBids, String currency) { + return BidResponse.builder() + .seatbid(seatBids) + .cur(currency) + .build(); + } + + private static Imp createImp(String id, String publisherId, int width, int height) { + final ObjectNode bidderNode = jacksonMapper.mapper().createObjectNode() + .put("publisherUniqueId", publisherId); + + final ObjectNode extNode = jacksonMapper.mapper().createObjectNode(); + extNode.set("bidder", bidderNode); + + return Imp.builder() + .id(id) + .banner(Banner.builder().w(width).h(height).build()) + .video(height > 250 ? Video.builder().w(width).h(height).build() : null) + .ext(extNode) + .build(); + } + + private static BidRequest createBidRequest(List imps) { + return BidRequest.builder() + .id("req-123") + .imp(imps) + .build(); + } + + private static AlvaAdsImp createAlvadsRequestImp(String impId, int width, int height) { + return AlvaAdsImp.builder() + .id(impId) + .banner(height <= 250 ? Map.of("w", width, "h", height) : null) + .video(height > 250 ? Map.of("w", width, "h", height) : null) + .build(); + } + + private static BidderCall buildBidderCall( + List imps, + List seatBids, + String currency) { + + final BidResponse bidResponse = createBidResponse(seatBids, currency); + + final HttpResponse httpResponse = HttpResponse.of( + 200, + MultiMap.caseInsensitiveMultiMap(), + jacksonMapper.encodeToString(bidResponse) + ); + + final HttpRequest request = HttpRequest.builder() + .payload(AlvadsRequestOrtb.builder() + .imp(imps) + .build()) + .build(); + + return BidderCall.succeededHttp(request, httpResponse, null); + } + +} diff --git a/src/test/java/org/prebid/server/it/AlvadsTest.java b/src/test/java/org/prebid/server/it/AlvadsTest.java new file mode 100644 index 00000000000..f5d6a6a3590 --- /dev/null +++ b/src/test/java/org/prebid/server/it/AlvadsTest.java @@ -0,0 +1,35 @@ +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 AlvadsTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAlvads() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/alvads-exchange")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/alvads/test-alvads-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/alvads/test-alvads-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/alvads/test-auction-alvads-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/alvads/test-auction-alvads-response.json", response, + singletonList("alvads")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-alvads-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-alvads-bid-request.json new file mode 100644 index 00000000000..81791f47055 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-alvads-bid-request.json @@ -0,0 +1,34 @@ +{ + "id": "tid", + "imp": [ + { + "id": "imp1", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "123", + "bidfloor": 0 + } + ], + "device": { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", + "ip": "193.168.244.1" + }, + "user": { + "id": "+59172893207", + "buyeruid": "79e917b5-8bb3-4e46-94dc-1053497311f8" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "site": { + "page": "https://facebooktest.com", + "ref": "https://facebooktest.com", + "publisher": { + "id": "D7DACCE3-C23D-4AB9-8FE6-9FF41BF32F8F" + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-alvads-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-alvads-bid-response.json new file mode 100644 index 00000000000..82ed15f1664 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-alvads-bid-response.json @@ -0,0 +1,27 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bid001", + "impid": "imp1", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ], + "seat": "alvads" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "alvads": 250 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-auction-alvads-request.json b/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-auction-alvads-request.json new file mode 100644 index 00000000000..a13ff93cf3e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-auction-alvads-request.json @@ -0,0 +1,50 @@ +{ + "id": "tid", + "imp": [ + { + "id": "imp1", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "123", + "bidfloor": 0, + "bidfloorcur": "USD", + "ext": { + "alvads": { + "sid": "testSid", + "placementId": "testPlacementId", + "publisherUniqueId": "D7DACCE3-C23D-4AB9-8FE6-9FF41BF32F8F" + }, + "prebid": { + "is_rewarded_inventory": 1 + }, + "userId": "+59172893207" + } + } + ], + "device": { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", + "ip": "193.168.244.1" + }, + "user": { + "id": "+59172893207", + "buyeruid": "79e917b5-8bb3-4e46-94dc-1053497311f8" + }, + "site": { + "page": "https://facebooktest.com", + "ref": "https://facebooktest.com", + "publisher": { + "id": "D7DACCE3-C23D-4AB9-8FE6-9FF41BF32F8F" + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "user_fingerprint": "e061375f-bba1-4a57-98f9-cc072d5a5ad8" + }, + "tmax": 5000 +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-auction-alvads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-auction-alvads-response.json new file mode 100644 index 00000000000..08f4720b56b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/alvads/test-auction-alvads-response.json @@ -0,0 +1,43 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bid001", + "impid": "imp1", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "exp": 300, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "alvads" + } + }, + "origbidcpm": 3.33, + "origbidcur": "USD" + } + } + ], + "seat": "alvads", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "alvads": "{{ alvads.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 1000 + }, + "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 51958ab075b..d9b6ba06924 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -72,6 +72,8 @@ adapters.adverxo.aliases.bidsmind.enabled=true adapters.adverxo.aliases.bidsmind.endpoint=http://localhost:8090/bidsmind-exchange adapters.adverxo.aliases.mobupps.enabled=true adapters.adverxo.aliases.mobupps.endpoint=http://localhost:8090/mobupps-exchange +adapters.alvads.enabled=true +adapters.alvads.endpoint=http://localhost:8090/alvads-exchange adapters.adview.enabled=true adapters.adview.endpoint=http://localhost:8090/adview-exchange?accountId={{AccountId}} adapters.adprime.enabled=true