Skip to content

Commit 9418842

Browse files
authored
Smrtconnect: Add new bidder (prebid#3060)
1 parent 2949607 commit 9418842

12 files changed

Lines changed: 532 additions & 0 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package org.prebid.server.bidder.smrtconnect;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.iab.openrtb.request.BidRequest;
5+
import com.iab.openrtb.request.Imp;
6+
import com.iab.openrtb.response.BidResponse;
7+
import com.iab.openrtb.response.SeatBid;
8+
import io.vertx.core.http.HttpMethod;
9+
import org.apache.commons.collections4.CollectionUtils;
10+
import org.prebid.server.bidder.Bidder;
11+
import org.prebid.server.bidder.model.BidderBid;
12+
import org.prebid.server.bidder.model.BidderCall;
13+
import org.prebid.server.bidder.model.BidderError;
14+
import org.prebid.server.bidder.model.HttpRequest;
15+
import org.prebid.server.bidder.model.Result;
16+
import org.prebid.server.exception.PreBidException;
17+
import org.prebid.server.json.DecodeException;
18+
import org.prebid.server.json.JacksonMapper;
19+
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
20+
import org.prebid.server.proto.openrtb.ext.request.smrtconnect.ExtImpSmrtconnect;
21+
import org.prebid.server.proto.openrtb.ext.response.BidType;
22+
import org.prebid.server.util.BidderUtil;
23+
import org.prebid.server.util.HttpUtil;
24+
25+
import java.util.Collection;
26+
import java.util.Collections;
27+
import java.util.List;
28+
import java.util.Objects;
29+
30+
public class SmrtconnectBidder implements Bidder<BidRequest> {
31+
32+
private static final String SUPPLY_ID_MACRO = "{{SupplyId}}";
33+
private static final TypeReference<ExtPrebid<?, ExtImpSmrtconnect>> SMRTCONNECT_EXT_TYPE_REFERENCE =
34+
new TypeReference<>() {
35+
};
36+
private final String endpointUrl;
37+
private final JacksonMapper mapper;
38+
39+
public SmrtconnectBidder(String endpointUrl, JacksonMapper mapper) {
40+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
41+
this.mapper = Objects.requireNonNull(mapper);
42+
}
43+
44+
@Override
45+
public final Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest bidRequest) {
46+
final Imp firstImp = bidRequest.getImp().get(0);
47+
final ExtImpSmrtconnect extImpSmrtconnect;
48+
49+
try {
50+
extImpSmrtconnect = mapper.mapper().convertValue(firstImp.getExt(), SMRTCONNECT_EXT_TYPE_REFERENCE)
51+
.getBidder();
52+
} catch (IllegalArgumentException e) {
53+
return Result.withError(BidderError.badInput("Ext.bidder not provided"));
54+
}
55+
56+
return Result.withValue(
57+
HttpRequest.<BidRequest>builder()
58+
.method(HttpMethod.POST)
59+
.uri(resolveEndpoint(extImpSmrtconnect.getSupplyId()))
60+
.headers(HttpUtil.headers())
61+
.body(mapper.encodeToBytes(bidRequest))
62+
.impIds(BidderUtil.impIds(bidRequest))
63+
.payload(bidRequest)
64+
.build());
65+
}
66+
67+
private String resolveEndpoint(String supplyId) {
68+
return endpointUrl.replace(SUPPLY_ID_MACRO, HttpUtil.encodeUrl(supplyId));
69+
}
70+
71+
@Override
72+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
73+
try {
74+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
75+
return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse));
76+
} catch (DecodeException e) {
77+
return Result.withError(BidderError.badServerResponse("Bad Server Response"));
78+
} catch (PreBidException e) {
79+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
80+
}
81+
}
82+
83+
private static List<BidderBid> extractBids(BidRequest bidRequest, BidResponse bidResponse) {
84+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
85+
return Collections.emptyList();
86+
}
87+
return bidsFromResponse(bidRequest, bidResponse);
88+
}
89+
90+
private static List<BidderBid> bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) {
91+
return bidResponse.getSeatbid().stream()
92+
.filter(Objects::nonNull)
93+
.map(SeatBid::getBid)
94+
.filter(Objects::nonNull)
95+
.flatMap(Collection::stream)
96+
.map(bid -> BidderBid.of(bid, getBidType(bid.getMtype()), bidResponse.getCur()))
97+
.toList();
98+
}
99+
100+
private static BidType getBidType(Integer mType) {
101+
return switch (mType) {
102+
case 1 -> BidType.banner;
103+
case 2 -> BidType.video;
104+
case 3 -> BidType.audio;
105+
case 4 -> BidType.xNative;
106+
107+
default -> throw new PreBidException("Unsupported mType " + mType);
108+
};
109+
}
110+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.prebid.server.proto.openrtb.ext.request.smrtconnect;
2+
3+
import lombok.Value;
4+
5+
@Value(staticConstructor = "of")
6+
public class ExtImpSmrtconnect {
7+
8+
String supplyId;
9+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.prebid.server.spring.config.bidder;
2+
3+
import org.prebid.server.bidder.BidderDeps;
4+
import org.prebid.server.bidder.smrtconnect.SmrtconnectBidder;
5+
import org.prebid.server.json.JacksonMapper;
6+
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
7+
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
8+
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
9+
import org.prebid.server.spring.env.YamlPropertySourceFactory;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.boot.context.properties.ConfigurationProperties;
12+
import org.springframework.context.annotation.Bean;
13+
import org.springframework.context.annotation.Configuration;
14+
import org.springframework.context.annotation.PropertySource;
15+
16+
import javax.validation.constraints.NotBlank;
17+
18+
@Configuration
19+
@PropertySource(value = "classpath:/bidder-config/smrtconnect.yaml", factory = YamlPropertySourceFactory.class)
20+
public class SmrtconnectConfiguration {
21+
22+
private static final String BIDDER_NAME = "smrtconnect";
23+
24+
@Bean("smrtconnectConfigurationProperties")
25+
@ConfigurationProperties("adapters.smrtconnect")
26+
BidderConfigurationProperties configurationProperties() {
27+
return new BidderConfigurationProperties();
28+
}
29+
30+
@Bean
31+
BidderDeps smrtconnectBidderDeps(BidderConfigurationProperties smrtconnectConfigurationProperties,
32+
@NotBlank @Value("${external-url}") String externalUrl,
33+
JacksonMapper mapper) {
34+
35+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
36+
.withConfig(smrtconnectConfigurationProperties)
37+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
38+
.bidderCreator(config -> new SmrtconnectBidder(config.getEndpoint(), mapper))
39+
.assemble();
40+
}
41+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
adapters:
2+
smrtconnect:
3+
endpoint: https://amp.smrtconnect.com/openrtb2/auction?supply_id={{SupplyId}}
4+
# This bidder does not operate globally. Please consider setting "disabled: true" in European datacenters.
5+
geoscope:
6+
- "!EEA"
7+
endpoint-compression: gzip
8+
meta-info:
9+
maintainer-email: prebid@smrtconnect.com
10+
app-media-types:
11+
- banner
12+
- native
13+
- video
14+
- audio
15+
site-media-types:
16+
- banner
17+
- native
18+
- video
19+
- audio
20+
supported-vendors:
21+
vendor-id: 0
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Smrtconnect Params",
4+
"description": "A schema which validates params accepted by the Smrtconnect",
5+
"type": "object",
6+
"properties": {
7+
"supply_id": {
8+
"type": "string",
9+
"description": "Supply id",
10+
"minLength": 1
11+
}
12+
},
13+
"required": ["supply_id"]
14+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package org.prebid.server.bidder.smrtconnect;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.iab.openrtb.request.BidRequest;
5+
import com.iab.openrtb.request.Imp;
6+
import com.iab.openrtb.response.Bid;
7+
import com.iab.openrtb.response.BidResponse;
8+
import com.iab.openrtb.response.SeatBid;
9+
import org.junit.Before;
10+
import org.junit.Test;
11+
import org.prebid.server.VertxTest;
12+
import org.prebid.server.bidder.model.BidderBid;
13+
import org.prebid.server.bidder.model.BidderCall;
14+
import org.prebid.server.bidder.model.BidderError;
15+
import org.prebid.server.bidder.model.HttpRequest;
16+
import org.prebid.server.bidder.model.HttpResponse;
17+
import org.prebid.server.bidder.model.Result;
18+
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
19+
import org.prebid.server.proto.openrtb.ext.request.smrtconnect.ExtImpSmrtconnect;
20+
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.function.UnaryOperator;
24+
25+
import static java.util.Collections.singletonList;
26+
import static java.util.function.UnaryOperator.identity;
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
29+
import static org.prebid.server.proto.openrtb.ext.response.BidType.audio;
30+
import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
31+
import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
32+
import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
33+
34+
public class SmrtconnectBidderTest extends VertxTest {
35+
36+
private static final String ENDPOINT_URL = "https://test-url.com/?param={{SupplyId}}";
37+
38+
private SmrtconnectBidder smrtconnectBidder;
39+
40+
@Before
41+
public void setUp() {
42+
smrtconnectBidder = new SmrtconnectBidder(ENDPOINT_URL, jacksonMapper);
43+
}
44+
45+
@Test
46+
public void creationShouldFailOnInvalidEndpointUrl() {
47+
assertThatIllegalArgumentException().isThrownBy(() -> new SmrtconnectBidder("invalid_url", jacksonMapper));
48+
}
49+
50+
@Test
51+
public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
52+
// given
53+
final BidRequest bidRequest = givenBidRequest(
54+
impBuilder -> impBuilder
55+
.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
56+
// when
57+
final Result<List<HttpRequest<BidRequest>>> result = smrtconnectBidder.makeHttpRequests(bidRequest);
58+
59+
// then
60+
assertThat(result.getErrors())
61+
.containsExactly(BidderError.badInput("Ext.bidder not provided"));
62+
}
63+
64+
@Test
65+
public void makeHttpRequestsShouldCreateCorrectURL() {
66+
// given
67+
final BidRequest bidRequest = givenBidRequest(identity());
68+
69+
// when
70+
final Result<List<HttpRequest<BidRequest>>> result = smrtconnectBidder.makeHttpRequests(bidRequest);
71+
72+
// then
73+
assertThat(result.getErrors()).isEmpty();
74+
assertThat(result.getValue())
75+
.extracting(HttpRequest::getUri)
76+
.containsExactly("https://test-url.com/?param=1");
77+
}
78+
79+
@Test
80+
public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
81+
// given
82+
final BidderCall<BidRequest> httpCall = givenHttpCall(null, "invalid");
83+
84+
// when
85+
final Result<List<BidderBid>> result = smrtconnectBidder.makeBids(httpCall, null);
86+
87+
// then
88+
assertThat(result.getValue()).isEmpty();
89+
assertThat(result.getErrors())
90+
.containsExactly(BidderError.badServerResponse("Bad Server Response"));
91+
}
92+
93+
@Test
94+
public void makeBidsShouldReturnAllFourBidTypesSuccessfully() throws JsonProcessingException {
95+
// given
96+
final Bid bannerBid = Bid.builder().impid("1").mtype(1).build();
97+
final Bid videoBid = Bid.builder().impid("2").mtype(2).build();
98+
final Bid audioBid = Bid.builder().impid("3").mtype(3).build();
99+
final Bid nativeBid = Bid.builder().impid("4").mtype(4).build();
100+
101+
final BidderCall<BidRequest> httpCall = givenHttpCall(
102+
givenBidRequest(
103+
impBuilder -> impBuilder
104+
.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))),
105+
givenBidResponse(bannerBid, videoBid, audioBid, nativeBid));
106+
107+
// when
108+
final Result<List<BidderBid>> result = smrtconnectBidder.makeBids(httpCall, null);
109+
110+
// then
111+
assertThat(result.getErrors()).isEmpty();
112+
assertThat(result.getValue()).containsExactly(
113+
BidderBid.of(bannerBid, banner, "USD"),
114+
BidderBid.of(videoBid, video, "USD"),
115+
BidderBid.of(audioBid, audio, "USD"),
116+
BidderBid.of(nativeBid, xNative, "USD"));
117+
}
118+
119+
private static BidRequest givenBidRequest(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
120+
return givenBidRequest(UnaryOperator.identity(), impCustomizer);
121+
}
122+
123+
private static BidRequest givenBidRequest(
124+
UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
125+
UnaryOperator<Imp.ImpBuilder> impCustomizer) {
126+
127+
return bidRequestCustomizer.apply(BidRequest.builder()
128+
.imp(singletonList(givenImp(impCustomizer))))
129+
.build();
130+
}
131+
132+
private static String givenBidResponse(Bid... bids) throws JsonProcessingException {
133+
return mapper.writeValueAsString(
134+
BidResponse.builder()
135+
.cur("USD")
136+
.seatbid(bids.length == 0
137+
? Collections.emptyList()
138+
: List.of(SeatBid.builder().bid(List.of(bids)).build()))
139+
.build());
140+
}
141+
142+
private static Imp givenImp(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
143+
return impCustomizer.apply(Imp.builder()
144+
.id("123")
145+
.ext(mapper.valueToTree(ExtPrebid.of(null,
146+
ExtImpSmrtconnect.of("1")))))
147+
.build();
148+
}
149+
150+
private static BidderCall<BidRequest> givenHttpCall(BidRequest bidRequest, String body) {
151+
return BidderCall.succeededHttp(
152+
HttpRequest.<BidRequest>builder().payload(bidRequest).build(),
153+
HttpResponse.of(200, null, body),
154+
null);
155+
}
156+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.prebid.server.it;
2+
3+
import io.restassured.response.Response;
4+
import org.json.JSONException;
5+
import org.junit.Test;
6+
import org.junit.runner.RunWith;
7+
import org.prebid.server.model.Endpoint;
8+
import org.springframework.test.context.junit4.SpringRunner;
9+
10+
import java.io.IOException;
11+
12+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
13+
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
14+
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
15+
import static com.github.tomakehurst.wiremock.client.WireMock.post;
16+
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
17+
import static java.util.Collections.singletonList;
18+
19+
@RunWith(SpringRunner.class)
20+
public class SmrtconnectTest extends IntegrationTest {
21+
22+
@Test
23+
public void openrtb2AuctionShouldRespondWithBidsFromTheSmrtconnect() throws IOException, JSONException {
24+
// given
25+
WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smrtconnect-exchange"))
26+
.withQueryParam("supply_id", equalTo("1"))
27+
.withRequestBody(equalToJson(jsonFrom("openrtb2/smrtconnect/test-smrtconnect-bid-request.json")))
28+
.willReturn(aResponse().withBody(jsonFrom("openrtb2/smrtconnect/test-smrtconnect-bid-response.json"))));
29+
30+
// when
31+
final Response response = responseFor("openrtb2/smrtconnect/test-auction-smrtconnect-request.json",
32+
Endpoint.openrtb2_auction);
33+
34+
// then
35+
assertJsonEquals("openrtb2/smrtconnect/test-auction-smrtconnect-response.json", response,
36+
singletonList("smrtconnect"));
37+
}
38+
}

0 commit comments

Comments
 (0)