Skip to content

Commit

Permalink
Smrtconnect: Add new bidder (prebid#3060)
Browse files Browse the repository at this point in the history
  • Loading branch information
psmrt authored Mar 27, 2024
1 parent 2949607 commit 9418842
Show file tree
Hide file tree
Showing 12 changed files with 532 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.prebid.server.bidder.smrtconnect;

import com.fasterxml.jackson.core.type.TypeReference;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
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.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.smrtconnect.ExtImpSmrtconnect;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.BidderUtil;
import org.prebid.server.util.HttpUtil;

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

public class SmrtconnectBidder implements Bidder<BidRequest> {

private static final String SUPPLY_ID_MACRO = "{{SupplyId}}";
private static final TypeReference<ExtPrebid<?, ExtImpSmrtconnect>> SMRTCONNECT_EXT_TYPE_REFERENCE =
new TypeReference<>() {
};
private final String endpointUrl;
private final JacksonMapper mapper;

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

@Override
public final Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest bidRequest) {
final Imp firstImp = bidRequest.getImp().get(0);
final ExtImpSmrtconnect extImpSmrtconnect;

try {
extImpSmrtconnect = mapper.mapper().convertValue(firstImp.getExt(), SMRTCONNECT_EXT_TYPE_REFERENCE)
.getBidder();
} catch (IllegalArgumentException e) {
return Result.withError(BidderError.badInput("Ext.bidder not provided"));
}

return Result.withValue(
HttpRequest.<BidRequest>builder()
.method(HttpMethod.POST)
.uri(resolveEndpoint(extImpSmrtconnect.getSupplyId()))
.headers(HttpUtil.headers())
.body(mapper.encodeToBytes(bidRequest))
.impIds(BidderUtil.impIds(bidRequest))
.payload(bidRequest)
.build());
}

private String resolveEndpoint(String supplyId) {
return endpointUrl.replace(SUPPLY_ID_MACRO, HttpUtil.encodeUrl(supplyId));
}

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse));
} catch (DecodeException e) {
return Result.withError(BidderError.badServerResponse("Bad Server Response"));
} catch (PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

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

private static List<BidderBid> bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) {
return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.map(bid -> BidderBid.of(bid, getBidType(bid.getMtype()), bidResponse.getCur()))
.toList();
}

private static BidType getBidType(Integer mType) {
return switch (mType) {
case 1 -> BidType.banner;
case 2 -> BidType.video;
case 3 -> BidType.audio;
case 4 -> BidType.xNative;

default -> throw new PreBidException("Unsupported mType " + mType);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.prebid.server.proto.openrtb.ext.request.smrtconnect;

import lombok.Value;

@Value(staticConstructor = "of")
public class ExtImpSmrtconnect {

String supplyId;
}
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.smrtconnect.SmrtconnectBidder;
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 javax.validation.constraints.NotBlank;

@Configuration
@PropertySource(value = "classpath:/bidder-config/smrtconnect.yaml", factory = YamlPropertySourceFactory.class)
public class SmrtconnectConfiguration {

private static final String BIDDER_NAME = "smrtconnect";

@Bean("smrtconnectConfigurationProperties")
@ConfigurationProperties("adapters.smrtconnect")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

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

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(smrtconnectConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new SmrtconnectBidder(config.getEndpoint(), mapper))
.assemble();
}
}
21 changes: 21 additions & 0 deletions src/main/resources/bidder-config/smrtconnect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
adapters:
smrtconnect:
endpoint: https://amp.smrtconnect.com/openrtb2/auction?supply_id={{SupplyId}}
# This bidder does not operate globally. Please consider setting "disabled: true" in European datacenters.
geoscope:
- "!EEA"
endpoint-compression: gzip
meta-info:
maintainer-email: [email protected]
app-media-types:
- banner
- native
- video
- audio
site-media-types:
- banner
- native
- video
- audio
supported-vendors:
vendor-id: 0
14 changes: 14 additions & 0 deletions src/main/resources/static/bidder-params/smrtconnect.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Smrtconnect Params",
"description": "A schema which validates params accepted by the Smrtconnect",
"type": "object",
"properties": {
"supply_id": {
"type": "string",
"description": "Supply id",
"minLength": 1
}
},
"required": ["supply_id"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package org.prebid.server.bidder.smrtconnect;

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.Before;
import org.junit.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.smrtconnect.ExtImpSmrtconnect;

import java.util.Collections;
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.audio;
import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;

public class SmrtconnectBidderTest extends VertxTest {

private static final String ENDPOINT_URL = "https://test-url.com/?param={{SupplyId}}";

private SmrtconnectBidder smrtconnectBidder;

@Before
public void setUp() {
smrtconnectBidder = new SmrtconnectBidder(ENDPOINT_URL, jacksonMapper);
}

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

@Test
public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
// given
final BidRequest bidRequest = givenBidRequest(
impBuilder -> impBuilder
.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
// when
final Result<List<HttpRequest<BidRequest>>> result = smrtconnectBidder.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors())
.containsExactly(BidderError.badInput("Ext.bidder not provided"));
}

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

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue())
.extracting(HttpRequest::getUri)
.containsExactly("https://test-url.com/?param=1");
}

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

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

// then
assertThat(result.getValue()).isEmpty();
assertThat(result.getErrors())
.containsExactly(BidderError.badServerResponse("Bad Server Response"));
}

@Test
public void makeBidsShouldReturnAllFourBidTypesSuccessfully() throws JsonProcessingException {
// given
final Bid bannerBid = Bid.builder().impid("1").mtype(1).build();
final Bid videoBid = Bid.builder().impid("2").mtype(2).build();
final Bid audioBid = Bid.builder().impid("3").mtype(3).build();
final Bid nativeBid = Bid.builder().impid("4").mtype(4).build();

final BidderCall<BidRequest> httpCall = givenHttpCall(
givenBidRequest(
impBuilder -> impBuilder
.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))),
givenBidResponse(bannerBid, videoBid, audioBid, nativeBid));

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).containsExactly(
BidderBid.of(bannerBid, banner, "USD"),
BidderBid.of(videoBid, video, "USD"),
BidderBid.of(audioBid, audio, "USD"),
BidderBid.of(nativeBid, xNative, "USD"));
}

private static BidRequest givenBidRequest(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
return givenBidRequest(UnaryOperator.identity(), impCustomizer);
}

private static BidRequest givenBidRequest(
UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
UnaryOperator<Imp.ImpBuilder> impCustomizer) {

return bidRequestCustomizer.apply(BidRequest.builder()
.imp(singletonList(givenImp(impCustomizer))))
.build();
}

private static String givenBidResponse(Bid... bids) throws JsonProcessingException {
return mapper.writeValueAsString(
BidResponse.builder()
.cur("USD")
.seatbid(bids.length == 0
? Collections.emptyList()
: List.of(SeatBid.builder().bid(List.of(bids)).build()))
.build());
}

private static Imp givenImp(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
return impCustomizer.apply(Imp.builder()
.id("123")
.ext(mapper.valueToTree(ExtPrebid.of(null,
ExtImpSmrtconnect.of("1")))))
.build();
}

private static BidderCall<BidRequest> givenHttpCall(BidRequest bidRequest, String body) {
return BidderCall.succeededHttp(
HttpRequest.<BidRequest>builder().payload(bidRequest).build(),
HttpResponse.of(200, null, body),
null);
}
}
38 changes: 38 additions & 0 deletions src/test/java/org/prebid/server/it/SmrtconnectTest.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.Test;
import org.junit.runner.RunWith;
import org.prebid.server.model.Endpoint;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
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;

@RunWith(SpringRunner.class)
public class SmrtconnectTest extends IntegrationTest {

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

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

// then
assertJsonEquals("openrtb2/smrtconnect/test-auction-smrtconnect-response.json", response,
singletonList("smrtconnect"));
}
}
Loading

0 comments on commit 9418842

Please sign in to comment.