diff --git a/extra/pom.xml b/extra/pom.xml
index ec38fb40a84..168d3203d12 100644
--- a/extra/pom.xml
+++ b/extra/pom.xml
@@ -51,7 +51,7 @@
1.2.3
0.16.0
2.0.10
- 3.2.4
+ 3.2.5
4.2.1
3.25.6
${protobuf.version}
diff --git a/src/main/java/org/prebid/server/bidder/alliancegravity/AllianceGravityBidder.java b/src/main/java/org/prebid/server/bidder/alliancegravity/AllianceGravityBidder.java
new file mode 100644
index 00000000000..baa043422f5
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidder/alliancegravity/AllianceGravityBidder.java
@@ -0,0 +1,175 @@
+package org.prebid.server.bidder.alliancegravity;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+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.User;
+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.apache.commons.lang3.StringUtils;
+import org.prebid.server.bidder.Bidder;
+import org.prebid.server.bidder.model.BidderBid;
+import org.prebid.server.bidder.model.BidderCall;
+import org.prebid.server.bidder.model.BidderError;
+import org.prebid.server.bidder.model.HttpRequest;
+import org.prebid.server.bidder.model.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.ExtImpPrebid;
+import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest;
+import org.prebid.server.proto.openrtb.ext.request.alliancegravity.ExtImpAllianceGravity;
+import org.prebid.server.proto.openrtb.ext.response.BidType;
+import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
+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;
+import java.util.Optional;
+
+public class AllianceGravityBidder implements Bidder {
+
+ private static final TypeReference> TYPE_REFERENCE =
+ new TypeReference<>() {
+ };
+
+ private final String endpointUrl;
+ private final JacksonMapper mapper;
+
+ public AllianceGravityBidder(String endpointUrl, JacksonMapper mapper) {
+ this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
+ this.mapper = Objects.requireNonNull(mapper);
+ }
+
+ @Override
+ public Result>> makeHttpRequests(BidRequest request) {
+ final List modifiedImps = new ArrayList<>();
+ final List errors = new ArrayList<>();
+
+ for (Imp imp : request.getImp()) {
+ try {
+ final ExtImpAllianceGravity extImp = parseImpExt(imp);
+ modifiedImps.add(modifyImp(imp, extImp));
+ } catch (PreBidException e) {
+ errors.add(BidderError.badInput(e.getMessage()));
+ }
+ }
+
+ if (modifiedImps.isEmpty()) {
+ return Result.withErrors(errors);
+ }
+
+ final BidRequest modifiedRequest = request.toBuilder().imp(modifiedImps).build();
+ return Result.of(
+ Collections.singletonList(
+ BidderUtil.defaultRequest(modifiedRequest, makeHeaders(modifiedRequest), endpointUrl, mapper)),
+ errors);
+ }
+
+ private ExtImpAllianceGravity parseImpExt(Imp imp) {
+ try {
+ return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder();
+ } catch (IllegalArgumentException e) {
+ throw new PreBidException(e.getMessage());
+ }
+ }
+
+ private Imp modifyImp(Imp imp, ExtImpAllianceGravity extImp) {
+ final ExtImpPrebid extImpPrebid = ExtImpPrebid.builder()
+ .storedrequest(ExtStoredRequest.of(extImp.getSrId()))
+ .build();
+
+ return imp.toBuilder()
+ .ext(mapper.mapper().valueToTree(ExtPrebid.of(extImpPrebid, null)))
+ .build();
+ }
+
+ private static MultiMap makeHeaders(BidRequest request) {
+ final MultiMap headers = HttpUtil.headers();
+
+ final Device device = request.getDevice();
+ if (device != null) {
+ HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa());
+ final String forwardedFor = StringUtils.isNotBlank(device.getIp()) ? device.getIp() : device.getIpv6();
+ HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, forwardedFor);
+ }
+
+ final Site site = request.getSite();
+ if (site != null) {
+ HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getPage());
+ }
+
+ final User user = request.getUser();
+ if (user != null && StringUtils.isNotBlank(user.getBuyeruid())) {
+ headers.add(HttpUtil.COOKIE_HEADER, "uids=" + user.getBuyeruid());
+ }
+
+ return headers;
+ }
+
+ @Override
+ public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) {
+ try {
+ final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
+ final List errors = new ArrayList<>();
+ return Result.of(extractBids(bidResponse, errors), errors);
+ } catch (DecodeException e) {
+ return Result.withError(BidderError.badServerResponse(e.getMessage()));
+ }
+ }
+
+ private List extractBids(BidResponse bidResponse, List errors) {
+ if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
+ return Collections.emptyList();
+ }
+
+ return bidResponse.getSeatbid().stream()
+ .filter(Objects::nonNull)
+ .map(SeatBid::getBid)
+ .filter(Objects::nonNull)
+ .flatMap(Collection::stream)
+ .filter(Objects::nonNull)
+ .map(bid -> resolveBidderBid(bid, bidResponse.getCur(), errors))
+ .filter(Objects::nonNull)
+ .toList();
+ }
+
+ private BidderBid resolveBidderBid(Bid bid, String currency, List errors) {
+ try {
+ return BidderBid.of(bid, getBidType(bid), currency);
+ } catch (PreBidException e) {
+ errors.add(BidderError.badServerResponse(e.getMessage()));
+ return null;
+ }
+ }
+
+ private BidType getBidType(Bid bid) {
+ return Optional.ofNullable(bid.getExt())
+ .map(ext -> ext.get("prebid"))
+ .filter(JsonNode::isObject)
+ .map(this::parseExtBidPrebid)
+ .map(ExtBidPrebid::getType)
+ .orElseThrow(() -> new PreBidException(
+ "Failed to parse impression \"%s\" mediatype".formatted(bid.getImpid())));
+ }
+
+ private ExtBidPrebid parseExtBidPrebid(JsonNode prebidNode) {
+ try {
+ return mapper.mapper().treeToValue(prebidNode, ExtBidPrebid.class);
+ } catch (JsonProcessingException e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/alliancegravity/ExtImpAllianceGravity.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/alliancegravity/ExtImpAllianceGravity.java
new file mode 100644
index 00000000000..a3953096077
--- /dev/null
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/alliancegravity/ExtImpAllianceGravity.java
@@ -0,0 +1,11 @@
+package org.prebid.server.proto.openrtb.ext.request.alliancegravity;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class ExtImpAllianceGravity {
+
+ @JsonProperty("srid")
+ String srId;
+}
diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AllianceGravityConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AllianceGravityConfiguration.java
new file mode 100644
index 00000000000..506c9101090
--- /dev/null
+++ b/src/main/java/org/prebid/server/spring/config/bidder/AllianceGravityConfiguration.java
@@ -0,0 +1,41 @@
+package org.prebid.server.spring.config.bidder;
+
+import org.prebid.server.bidder.BidderDeps;
+import org.prebid.server.bidder.alliancegravity.AllianceGravityBidder;
+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/alliancegravity.yaml", factory = YamlPropertySourceFactory.class)
+public class AllianceGravityConfiguration {
+
+ private static final String BIDDER_NAME = "alliance_gravity";
+
+ @Bean("alliancegravityConfigurationProperties")
+ @ConfigurationProperties("adapters.alliancegravity")
+ BidderConfigurationProperties configurationProperties() {
+ return new BidderConfigurationProperties();
+ }
+
+ @Bean
+ BidderDeps alliancegravityBidderDeps(BidderConfigurationProperties alliancegravityConfigurationProperties,
+ @NotBlank @Value("${external-url}") String externalUrl,
+ JacksonMapper mapper) {
+
+ return BidderDepsAssembler.forBidder(BIDDER_NAME)
+ .withConfig(alliancegravityConfigurationProperties)
+ .usersyncerCreator(UsersyncerCreator.create(externalUrl))
+ .bidderCreator(config -> new AllianceGravityBidder(config.getEndpoint(), mapper))
+ .assemble();
+ }
+}
diff --git a/src/main/resources/bidder-config/alliancegravity.yaml b/src/main/resources/bidder-config/alliancegravity.yaml
new file mode 100644
index 00000000000..9085e9ff8a5
--- /dev/null
+++ b/src/main/resources/bidder-config/alliancegravity.yaml
@@ -0,0 +1,25 @@
+adapters:
+ alliancegravity:
+ endpoint: https://pbs.production.agrvt.com/openrtb2/auction
+ ortb-version: "2.6"
+ modifying-vast-xml-allowed: true
+ meta-info:
+ maintainer-email: produit@alliancegravity.com
+ app-media-types:
+ - banner
+ - video
+ - audio
+ - native
+ site-media-types:
+ - banner
+ - video
+ - audio
+ - native
+ supported-vendors:
+ vendor-id: 501
+ usersync:
+ cookie-family-name: alliancegravity
+ iframe:
+ url: https://pbs.production.agrvt.com/static/cookie_sync.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}
+ support-cors: false
+ uid-macro: '$UID'
diff --git a/src/main/resources/static/bidder-params/alliance_gravity.json b/src/main/resources/static/bidder-params/alliance_gravity.json
new file mode 100644
index 00000000000..b2c122f710b
--- /dev/null
+++ b/src/main/resources/static/bidder-params/alliance_gravity.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Alliance Gravity Adapter Params",
+ "description": "A schema which validates params accepted by the Alliance Gravity adapter",
+ "type": "object",
+ "properties": {
+ "srid": {
+ "type": "string",
+ "description": "Stored request ID",
+ "minLength": 1
+ }
+ },
+ "required": ["srid"]
+}
diff --git a/src/test/java/org/prebid/server/bidder/alliancegravity/AllianceGravityBidderTest.java b/src/test/java/org/prebid/server/bidder/alliancegravity/AllianceGravityBidderTest.java
new file mode 100644
index 00000000000..ae4c9ea552d
--- /dev/null
+++ b/src/test/java/org/prebid/server/bidder/alliancegravity/AllianceGravityBidderTest.java
@@ -0,0 +1,407 @@
+package org.prebid.server.bidder.alliancegravity;
+
+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.User;
+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.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.proto.openrtb.ext.ExtPrebid;
+import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
+import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest;
+import org.prebid.server.proto.openrtb.ext.request.alliancegravity.ExtImpAllianceGravity;
+import org.prebid.server.proto.openrtb.ext.response.BidType;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.function.UnaryOperator;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+public class AllianceGravityBidderTest extends VertxTest {
+
+ private static final String ENDPOINT_URL = "https://test.endpoint.com/";
+
+ private AllianceGravityBidder target;
+
+ @BeforeEach
+ public void before() {
+ target = new AllianceGravityBidder(ENDPOINT_URL, jacksonMapper);
+ }
+
+ @Test
+ public void creationShouldFailOnInvalidEndpointUrl() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> new AllianceGravityBidder("invalid_url", jacksonMapper));
+ }
+
+ @Test
+ public void makeHttpRequestsShouldReturnErrorWhenImpExtCannotBeParsed() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(
+ imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getValue()).isEmpty();
+ assertThat(result.getErrors()).hasSize(1)
+ .allSatisfy(error -> assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input));
+ }
+
+ @Test
+ public void makeHttpRequestsShouldReturnErrorWhenAllImpFail() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .imp(List.of(
+ givenImp(imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))),
+ givenImp(imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))))))
+ .build();
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getValue()).isEmpty();
+ assertThat(result.getErrors()).hasSize(2);
+ }
+
+ @Test
+ public void makeHttpRequestsShouldSetStoredRequestInImpExt() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(UnaryOperator.identity());
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1)
+ .extracting(HttpRequest::getPayload)
+ .flatExtracting(BidRequest::getImp)
+ .extracting(Imp::getExt)
+ .containsExactly(mapper.valueToTree(
+ ExtPrebid.of(
+ ExtImpPrebid.builder()
+ .storedrequest(ExtStoredRequest.of("test-srid"))
+ .build(),
+ null)));
+ }
+
+ @Test
+ public void makeHttpRequestsShouldSendAllImpsInOneRequest() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .imp(List.of(givenImp(imp -> imp.id("imp1")), givenImp(imp -> imp.id("imp2"))))
+ .build();
+
+ // 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::getId)
+ .containsExactlyInAnyOrder("imp1", "imp2");
+ }
+
+ @Test
+ public void makeHttpRequestsShouldSkipInvalidImpsAndKeepValid() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .imp(List.of(
+ givenImp(imp -> imp.id("valid")),
+ givenImp(imp -> imp.id("invalid")
+ .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))))))
+ .build();
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getErrors()).hasSize(1);
+ assertThat(result.getValue()).hasSize(1)
+ .extracting(HttpRequest::getPayload)
+ .flatExtracting(BidRequest::getImp)
+ .extracting(Imp::getId)
+ .containsExactly("valid");
+ }
+
+ @Test
+ public void makeHttpRequestsShouldAddUserAgentAndForwardedForFromDeviceIp() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .device(Device.builder().ua("test-ua").ip("1.2.3.4").ipv6("::1").build())
+ .imp(singletonList(givenImp(UnaryOperator.identity())))
+ .build();
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1)
+ .extracting(HttpRequest::getHeaders)
+ .allSatisfy(headers -> {
+ assertThat(headers.get("User-Agent")).isEqualTo("test-ua");
+ assertThat(headers.get("X-Forwarded-For")).isEqualTo("1.2.3.4");
+ });
+ }
+
+ @Test
+ public void makeHttpRequestsShouldFallBackToDeviceIpv6WhenIpIsBlank() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .device(Device.builder().ipv6("2001:db8::1").build())
+ .imp(singletonList(givenImp(UnaryOperator.identity())))
+ .build();
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1)
+ .extracting(HttpRequest::getHeaders)
+ .extracting(headers -> headers.get("X-Forwarded-For"))
+ .containsExactly("2001:db8::1");
+ }
+
+ @Test
+ public void makeHttpRequestsShouldAddRefererFromSitePage() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .site(Site.builder().page("http://example.com").build())
+ .imp(singletonList(givenImp(UnaryOperator.identity())))
+ .build();
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1)
+ .extracting(HttpRequest::getHeaders)
+ .extracting(headers -> headers.get("Referer"))
+ .containsExactly("http://example.com");
+ }
+
+ @Test
+ public void makeHttpRequestsShouldAddCookieWithBuyerUid() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .user(User.builder().buyeruid("buyer-uid-1").build())
+ .imp(singletonList(givenImp(UnaryOperator.identity())))
+ .build();
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1)
+ .extracting(HttpRequest::getHeaders)
+ .extracting(headers -> headers.get("Cookie"))
+ .containsExactly("uids=buyer-uid-1");
+ }
+
+ @Test
+ public void makeHttpRequestsShouldNotAddCookieWhenBuyerUidIsBlank() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .user(User.builder().buyeruid("").build())
+ .imp(singletonList(givenImp(UnaryOperator.identity())))
+ .build();
+
+ // when
+ final Result>> result = target.makeHttpRequests(bidRequest);
+
+ // then
+ assertThat(result.getValue()).hasSize(1)
+ .extracting(HttpRequest::getHeaders)
+ .extracting(headers -> headers.get("Cookie"))
+ .containsOnlyNulls();
+ }
+
+ @Test
+ public void makeBidsShouldReturnErrorWhenResponseCannotBeParsed() {
+ // given
+ final BidderCall httpCall = givenHttpCall("invalid");
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getValue()).isEmpty();
+ assertThat(result.getErrors()).hasSize(1)
+ .allSatisfy(error -> assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response));
+ }
+
+ @Test
+ public void makeBidsShouldReturnEmptyListWhenResponseHasNoSeatbid() throws JsonProcessingException {
+ // given
+ final BidderCall httpCall = givenHttpCall(
+ mapper.writeValueAsString(BidResponse.builder().build()));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getValue()).isEmpty();
+ assertThat(result.getErrors()).isEmpty();
+ }
+
+ @Test
+ public void makeBidsShouldReturnBannerBidWhenBidExtPrebidTypeIsBanner() throws JsonProcessingException {
+ // given
+ final BidderCall httpCall = givenHttpCall(
+ givenBidResponse(bid -> bid.id("bid1").impid("imp1").ext(bidExtWithPrebidType("banner"))));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1)
+ .allSatisfy(bidderBid -> {
+ assertThat(bidderBid.getType()).isEqualTo(BidType.banner);
+ assertThat(bidderBid.getBid().getId()).isEqualTo("bid1");
+ assertThat(bidderBid.getBidCurrency()).isEqualTo("USD");
+ });
+ }
+
+ @Test
+ public void makeBidsShouldReturnVideoBidWhenBidExtPrebidTypeIsVideo() throws JsonProcessingException {
+ // given
+ final BidderCall httpCall = givenHttpCall(
+ givenBidResponse(bid -> bid.id("bid1").impid("imp1").ext(bidExtWithPrebidType("video"))));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1)
+ .allSatisfy(bidderBid -> assertThat(bidderBid.getType()).isEqualTo(BidType.video));
+ }
+
+ @Test
+ public void makeBidsShouldReturnAudioBidWhenBidExtPrebidTypeIsAudio() throws JsonProcessingException {
+ // given
+ final BidderCall httpCall = givenHttpCall(
+ givenBidResponse(bid -> bid.id("bid1").impid("imp1").ext(bidExtWithPrebidType("audio"))));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1)
+ .allSatisfy(bidderBid -> assertThat(bidderBid.getType()).isEqualTo(BidType.audio));
+ }
+
+ @Test
+ public void makeBidsShouldReturnNativeBidWhenBidExtPrebidTypeIsNative() throws JsonProcessingException {
+ // given
+ final BidderCall httpCall = givenHttpCall(
+ givenBidResponse(bid -> bid.id("bid1").impid("imp1").ext(bidExtWithPrebidType("native"))));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getErrors()).isEmpty();
+ assertThat(result.getValue()).hasSize(1)
+ .allSatisfy(bidderBid -> assertThat(bidderBid.getType()).isEqualTo(BidType.xNative));
+ }
+
+ @Test
+ public void makeBidsShouldReturnErrorWhenBidExtIsMissing() throws JsonProcessingException {
+ // given
+ final BidderCall httpCall = givenHttpCall(
+ givenBidResponse(bid -> bid.id("bid1").impid("imp1")));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getValue()).isEmpty();
+ assertThat(result.getErrors()).hasSize(1)
+ .allSatisfy(error -> {
+ assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
+ assertThat(error.getMessage()).contains("Failed to parse impression \"imp1\" mediatype");
+ });
+ }
+
+ @Test
+ public void makeBidsShouldReturnErrorWhenBidExtPrebidTypeIsUnknown() throws JsonProcessingException {
+ // given
+ final BidderCall httpCall = givenHttpCall(
+ givenBidResponse(bid -> bid.id("bid1").impid("imp1").ext(bidExtWithPrebidType("unknown"))));
+
+ // when
+ final Result> result = target.makeBids(httpCall, null);
+
+ // then
+ assertThat(result.getValue()).isEmpty();
+ assertThat(result.getErrors()).hasSize(1)
+ .allSatisfy(error -> assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response));
+ }
+
+ private static BidRequest givenBidRequest(UnaryOperator impCustomizer) {
+ return BidRequest.builder()
+ .imp(singletonList(givenImp(impCustomizer)))
+ .build();
+ }
+
+ private static Imp givenImp(UnaryOperator impCustomizer) {
+ return impCustomizer.apply(Imp.builder()
+ .id("imp_id")
+ .banner(Banner.builder().build())
+ .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAllianceGravity.of("test-srid")))))
+ .build();
+ }
+
+ private static BidderCall givenHttpCall(String body) {
+ return BidderCall.succeededHttp(
+ HttpRequest.builder().payload(null).build(),
+ HttpResponse.of(200, null, body),
+ null);
+ }
+
+ private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException {
+ return mapper.writeValueAsString(BidResponse.builder()
+ .cur("USD")
+ .seatbid(singletonList(SeatBid.builder()
+ .bid(singletonList(bidCustomizer
+ .apply(Bid.builder().price(BigDecimal.ONE))
+ .build()))
+ .build()))
+ .build());
+ }
+
+ private ObjectNode bidExtWithPrebidType(String type) {
+ final ObjectNode ext = mapper.createObjectNode();
+ ext.set("prebid", mapper.createObjectNode().put("type", type));
+ return ext;
+ }
+}
diff --git a/src/test/java/org/prebid/server/it/AllianceGravityTest.java b/src/test/java/org/prebid/server/it/AllianceGravityTest.java
new file mode 100644
index 00000000000..c945b70ff93
--- /dev/null
+++ b/src/test/java/org/prebid/server/it/AllianceGravityTest.java
@@ -0,0 +1,38 @@
+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 AllianceGravityTest extends IntegrationTest {
+
+ @Test
+ public void openrtb2AuctionShouldRespondWithBidsFromAllianceGravity() throws IOException, JSONException {
+ // given
+ WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/alliancegravity-exchange"))
+ .withRequestBody(equalToJson(
+ jsonFrom("openrtb2/alliancegravity/test-alliancegravity-bid-request.json"), true, true))
+ .willReturn(aResponse().withBody(
+ jsonFrom("openrtb2/alliancegravity/test-alliancegravity-bid-response.json"))));
+
+ // when
+ final Response response = responseFor(
+ "openrtb2/alliancegravity/test-auction-alliancegravity-request.json",
+ Endpoint.openrtb2_auction);
+
+ // then
+ assertJsonEquals(
+ "openrtb2/alliancegravity/test-auction-alliancegravity-response.json",
+ response,
+ singletonList("alliance_gravity"));
+ }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/alliancegravity/test-alliancegravity-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/alliancegravity/test-alliancegravity-bid-request.json
new file mode 100644
index 00000000000..f963675e212
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/alliancegravity/test-alliancegravity-bid-request.json
@@ -0,0 +1,55 @@
+{
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "w": 300,
+ "h": 250
+ },
+ "secure": 1,
+ "ext": {
+ "prebid": {
+ "storedrequest": {
+ "id": "test-stored-request-id"
+ }
+ }
+ }
+ }
+ ],
+ "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"
+ ],
+ "source": {
+ "tid": "${json-unit.any-string}"
+ },
+ "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/alliancegravity/test-alliancegravity-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/alliancegravity/test-alliancegravity-bid-response.json
new file mode 100644
index 00000000000..ce4dcc1bf1d
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/alliancegravity/test-alliancegravity-bid-response.json
@@ -0,0 +1,25 @@
+{
+ "id": "test-request-id",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "bid_id",
+ "impid": "test-imp-id",
+ "price": 1.50,
+ "adm": "test-ad-markup",
+ "crid": "test-creative-id",
+ "w": 300,
+ "h": 250,
+ "mtype": 1,
+ "ext": {
+ "prebid": {
+ "type": "banner"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/alliancegravity/test-auction-alliancegravity-request.json b/src/test/resources/org/prebid/server/it/openrtb2/alliancegravity/test-auction-alliancegravity-request.json
new file mode 100644
index 00000000000..8210bc61e0a
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/alliancegravity/test-auction-alliancegravity-request.json
@@ -0,0 +1,27 @@
+{
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "w": 300,
+ "h": 250
+ },
+ "ext": {
+ "prebid": {
+ "bidder": {
+ "alliance_gravity": {
+ "srid": "test-stored-request-id"
+ }
+ }
+ }
+ }
+ }
+ ],
+ "tmax": 5000,
+ "regs": {
+ "ext": {
+ "gdpr": 0
+ }
+ }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/alliancegravity/test-auction-alliancegravity-response.json b/src/test/resources/org/prebid/server/it/openrtb2/alliancegravity/test-auction-alliancegravity-response.json
new file mode 100644
index 00000000000..07e76653cd6
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/alliancegravity/test-auction-alliancegravity-response.json
@@ -0,0 +1,42 @@
+{
+ "id": "test-request-id",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "bid_id",
+ "impid": "test-imp-id",
+ "exp": 300,
+ "price": 1.50,
+ "adm": "test-ad-markup",
+ "crid": "test-creative-id",
+ "w": 300,
+ "h": 250,
+ "mtype": 1,
+ "ext": {
+ "prebid": {
+ "type": "banner",
+ "meta": {
+ "adaptercode": "alliance_gravity"
+ }
+ },
+ "origbidcpm": 1.50,
+ "origbidcur": "USD"
+ }
+ }
+ ],
+ "seat": "alliance_gravity",
+ "group": 0
+ }
+ ],
+ "cur": "USD",
+ "ext": {
+ "responsetimemillis": {
+ "alliance_gravity": "{{ alliance_gravity.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 5b0b91b257c..2ab2be3bb15 100644
--- a/src/test/resources/org/prebid/server/it/test-application.properties
+++ b/src/test/resources/org/prebid/server/it/test-application.properties
@@ -14,6 +14,8 @@ adapters.generic.aliases.infytv.enabled=true
adapters.generic.aliases.infytv.endpoint=http://localhost:8090/infytv-exchange
adapters.generic.aliases.zeta_global_ssp.enabled=true
adapters.generic.aliases.zeta_global_ssp.endpoint=http://localhost:8090/zeta_global_ssp-exchange
+adapters.alliancegravity.enabled=true
+adapters.alliancegravity.endpoint=http://localhost:8090/alliancegravity-exchange
adapters.aceex.enabled=true
adapters.aceex.endpoint=http://localhost:8090/aceex-exchange
adapters.acuityads.enabled=true