Skip to content

Commit

Permalink
Cointraffic: Add Bidder (#3306)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoxaAntoxic authored Jul 17, 2024
1 parent d745965 commit 013c155
Show file tree
Hide file tree
Showing 11 changed files with 478 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.prebid.server.bidder.cointraffic;

import com.iab.openrtb.request.BidRequest;
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.json.DecodeException;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.BidderUtil;
import org.prebid.server.util.HttpUtil;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public class CointrafficBidder implements Bidder<BidRequest> {

private final String endpointUrl;
private final JacksonMapper mapper;

public CointrafficBidder(String endpointUrl, JacksonMapper mapper) {
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
this.mapper = Objects.requireNonNull(mapper);
}

@Override
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest bidRequest) {
final MultiMap headers = HttpUtil.headers().add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5");
return Result.withValue(BidderUtil.defaultRequest(bidRequest, headers, endpointUrl, mapper));
}

@Override
public final Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
final List<BidderError> errors = new ArrayList<>();
return Result.of(extractBids(bidResponse), errors);
} catch (DecodeException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

private static List<BidderBid> extractBids(BidResponse bidResponse) {
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
return Collections.emptyList();
}
return bidsFromResponse(bidResponse);
}

private static List<BidderBid> bidsFromResponse(BidResponse bidResponse) {
return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur()))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.prebid.server.spring.config.bidder;

import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.cointraffic.CointrafficBidder;
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/cointraffic.yaml", factory = YamlPropertySourceFactory.class)
public class CointrafficConfiguration {

private static final String BIDDER_NAME = "cointraffic";

@Bean("cointrafficConfigurationProperties")
@ConfigurationProperties("adapters.cointraffic")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

@Bean
BidderDeps cointrafficBidderDeps(BidderConfigurationProperties cointrafficConfigurationProperties,
@NotBlank @Value("${external-url}") String externalUrl,
JacksonMapper mapper) {

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(cointrafficConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new CointrafficBidder(config.getEndpoint(), mapper))
.assemble();
}
}
11 changes: 11 additions & 0 deletions src/main/resources/bidder-config/cointraffic.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
adapters:
cointraffic:
endpoint: https://apps.adsgravity.io/pbs/v1/request
meta-info:
maintainer-email: tech@cointraffic.io
app-media-types:
- banner
site-media-types:
- banner
supported-vendors:
vendor-id: 0
16 changes: 16 additions & 0 deletions src/main/resources/static/bidder-params/cointraffic.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Cointraffic Adapter Params",
"description": "A schema which validates params accepted by the Cointraffic adapter",
"type": "object",
"properties": {
"placementId": {
"type": "string",
"minLength": 1,
"description": "Ad placement identifier"
}
},
"required": [
"placementId"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package org.prebid.server.bidder.cointraffic;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import org.junit.jupiter.api.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 java.util.Arrays;
import java.util.List;
import java.util.function.UnaryOperator;

import static java.util.Collections.singletonList;
import static java.util.function.UnaryOperator.identity;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
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.X_OPENRTB_VERSION_HEADER;
import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;

public class CointrafficBidderTest extends VertxTest {

private static final String ENDPOINT_URL = "https://test.host.com/prebid/bid/";

private final CointrafficBidder target = new CointrafficBidder(ENDPOINT_URL, jacksonMapper);

@Test
public void creationShouldFailOnInvalidEndpointUrl() {
assertThatIllegalArgumentException().isThrownBy(() -> new CointrafficBidder("invalid_url", jacksonMapper));
}

@Test
public void makeHttpRequestsShouldReturnExpectedRequestWithAllImps() {
final BidRequest bidRequest = givenBidRequest(
imp -> imp.id("impId1"),
imp -> imp.id("impId2"));

// when
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1).first()
.satisfies(request -> assertThat(request.getBody())
.isEqualTo(jacksonMapper.encodeToBytes(bidRequest)))
.satisfies(request -> assertThat(request.getPayload())
.isEqualTo(bidRequest))
.satisfies(request -> assertThat(request.getImpIds())
.containsExactlyInAnyOrder("impId1", "impId2"));
}

@Test
public void makeHttpRequestsShouldReturnExpectedHeaders() {
final BidRequest bidRequest = givenBidRequest(
imp -> imp.id("impId1"),
imp -> imp.id("impId2"));

// when
final Result<List<HttpRequest<BidRequest>>> 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))
.satisfies(headers -> assertThat(headers.get(X_OPENRTB_VERSION_HEADER))
.isEqualTo("2.5"));
}

@Test
public void makeHttpRequestsShouldUseCorrectUri() {
// given
final BidRequest bidRequest = givenBidRequest(identity());

// when
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1).first()
.extracting(HttpRequest::getUri)
.isEqualTo(ENDPOINT_URL);
}

@Test
public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
// given
final BidderCall<BidRequest> httpCall = givenHttpCall("invalid");

// when
final Result<List<BidderBid>> 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()).startsWith("Failed to decode: Unrecognized token");
});
}

@Test
public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
// given
final BidderCall<BidRequest> httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build()));

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).isEmpty();
}

@Test
public void makeBidsShouldRAlwaysReturnBannerBid() throws JsonProcessingException {
// given
final Bid bannerBid = Bid.builder().impid("1").mtype(2).build();

final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidResponse(bannerBid));

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).containsOnly(BidderBid.of(bannerBid, banner, "USD"));

}

private static BidRequest givenBidRequest(UnaryOperator<Imp.ImpBuilder>... impCustomizers) {
return BidRequest.builder()
.imp(Arrays.stream(impCustomizers).map(CointrafficBidderTest::givenImp).toList())
.build();
}

private static Imp givenImp(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
return impCustomizer.apply(Imp.builder().id("impId")).build();
}

private static String givenBidResponse(Bid... bids) throws JsonProcessingException {
return mapper.writeValueAsString(BidResponse.builder()
.cur("USD")
.seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build()))
.build());
}

private static BidderCall<BidRequest> givenHttpCall(String body) {
return BidderCall.succeededHttp(
HttpRequest.<BidRequest>builder().build(),
HttpResponse.of(200, null, body),
null);
}
}
38 changes: 38 additions & 0 deletions src/test/java/org/prebid/server/it/CointrafficTest.java
Original file line number Diff line number Diff line change
@@ -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 java.util.List;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;

public class CointrafficTest extends IntegrationTest {

@Test
public void openrtb2AuctionShouldRespondWithBidsFromCointraffic() throws IOException, JSONException {
// given
WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cointraffic-exchange"))
.withRequestBody(equalToJson(jsonFrom("openrtb2/cointraffic/test-cointraffic-bid-request.json")))
.willReturn(aResponse().withBody(jsonFrom("openrtb2/cointraffic/test-cointraffic-bid-response.json"))));

// when
final Response response = responseFor(
"openrtb2/cointraffic/test-auction-cointraffic-request.json",
Endpoint.openrtb2_auction
);

// then
assertJsonEquals(
"openrtb2/cointraffic/test-auction-cointraffic-response.json",
response,
List.of("cointraffic"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"id": "request_id",
"imp": [
{
"id": "imp_id",
"banner": {
"w": 320,
"h": 250
},
"ext": {
"cointraffic": {
"placementId": "placementId"
}
}
}
],
"tmax": 5000,
"regs": {
"ext": {
"gdpr": 0
}
}
}
Loading

0 comments on commit 013c155

Please sign in to comment.