Skip to content

Commit bfe8deb

Browse files
AntoxaAntoxicRitesh Ghodrao
authored andcommitted
New Zentotem Adapter (prebid#4078)
1 parent feb5d96 commit bfe8deb

11 files changed

Lines changed: 546 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package org.prebid.server.bidder.zentotem;
2+
3+
import com.iab.openrtb.request.BidRequest;
4+
import com.iab.openrtb.request.Imp;
5+
import com.iab.openrtb.response.Bid;
6+
import com.iab.openrtb.response.BidResponse;
7+
import com.iab.openrtb.response.SeatBid;
8+
import org.apache.commons.collections4.CollectionUtils;
9+
import org.prebid.server.bidder.Bidder;
10+
import org.prebid.server.bidder.model.BidderBid;
11+
import org.prebid.server.bidder.model.BidderCall;
12+
import org.prebid.server.bidder.model.BidderError;
13+
import org.prebid.server.bidder.model.HttpRequest;
14+
import org.prebid.server.bidder.model.Result;
15+
import org.prebid.server.exception.PreBidException;
16+
import org.prebid.server.json.DecodeException;
17+
import org.prebid.server.json.JacksonMapper;
18+
import org.prebid.server.proto.openrtb.ext.response.BidType;
19+
import org.prebid.server.util.BidderUtil;
20+
import org.prebid.server.util.HttpUtil;
21+
22+
import java.util.ArrayList;
23+
import java.util.Collection;
24+
import java.util.Collections;
25+
import java.util.List;
26+
import java.util.Objects;
27+
import java.util.stream.Collectors;
28+
29+
public class ZentotemBidder implements Bidder<BidRequest> {
30+
31+
private final String endpointUrl;
32+
private final JacksonMapper mapper;
33+
34+
public ZentotemBidder(String endpointUrl, JacksonMapper mapper) {
35+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
36+
this.mapper = Objects.requireNonNull(mapper);
37+
}
38+
39+
@Override
40+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
41+
final List<HttpRequest<BidRequest>> httpRequests = new ArrayList<>();
42+
final List<BidderError> errors = new ArrayList<>();
43+
44+
for (Imp imp : request.getImp()) {
45+
try {
46+
final BidRequest outgoingRequest = request.toBuilder()
47+
.imp(Collections.singletonList(imp))
48+
.build();
49+
httpRequests.add(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper));
50+
} catch (PreBidException e) {
51+
errors.add(BidderError.badInput(e.getMessage()));
52+
}
53+
}
54+
55+
return Result.of(httpRequests, errors);
56+
}
57+
58+
@Override
59+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
60+
try {
61+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
62+
final List<BidderError> errors = new ArrayList<>();
63+
return Result.of(extractBids(bidResponse, errors), errors);
64+
} catch (DecodeException | PreBidException e) {
65+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
66+
}
67+
}
68+
69+
private static List<BidderBid> extractBids(BidResponse bidResponse, List<BidderError> errors) {
70+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
71+
return Collections.emptyList();
72+
}
73+
return bidsFromResponse(bidResponse, errors);
74+
}
75+
76+
private static List<BidderBid> bidsFromResponse(BidResponse bidResponse, List<BidderError> errors) {
77+
return bidResponse.getSeatbid().stream()
78+
.filter(Objects::nonNull)
79+
.map(SeatBid::getBid)
80+
.filter(Objects::nonNull)
81+
.flatMap(Collection::stream)
82+
.filter(Objects::nonNull)
83+
.map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors))
84+
.filter(Objects::nonNull)
85+
.collect(Collectors.toList());
86+
}
87+
88+
private static BidderBid makeBidderBid(Bid bid, String currency, List<BidderError> errors) {
89+
final BidType bidType = getBidType(bid, errors);
90+
return bidType != null
91+
? BidderBid.of(bid, bidType, currency)
92+
: null;
93+
}
94+
95+
private static BidType getBidType(Bid bid, List<BidderError> errors) {
96+
return switch (bid.getMtype()) {
97+
case 1 -> BidType.banner;
98+
case 2 -> BidType.video;
99+
case 4 -> BidType.xNative;
100+
case null, default -> {
101+
errors.add(BidderError.badServerResponse(
102+
"could not define media type for impression: " + bid.getImpid()));
103+
yield null;
104+
}
105+
};
106+
}
107+
}
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.zentotem.ZentotemBidder;
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 jakarta.validation.constraints.NotBlank;
17+
18+
@Configuration
19+
@PropertySource(value = "classpath:/bidder-config/zentotem.yaml", factory = YamlPropertySourceFactory.class)
20+
public class ZentotemConfiguration {
21+
22+
private static final String BIDDER_NAME = "zentotem";
23+
24+
@Bean("zentotemConfigurationProperties")
25+
@ConfigurationProperties("adapters.zentotem")
26+
BidderConfigurationProperties configurationProperties() {
27+
return new BidderConfigurationProperties();
28+
}
29+
30+
@Bean
31+
BidderDeps zentotemBidderDeps(BidderConfigurationProperties zentotemConfigurationProperties,
32+
@NotBlank @Value("${external-url}") String externalUrl,
33+
JacksonMapper mapper) {
34+
35+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
36+
.withConfig(zentotemConfigurationProperties)
37+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
38+
.bidderCreator(config -> new ZentotemBidder(config.getEndpoint(), mapper))
39+
.assemble();
40+
}
41+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
adapters:
2+
zentotem:
3+
endpoint: https://rtb.zentotem.net/bid?sspuid=cqlnvfk00bhs0b6rci6g
4+
endpoint-compression: gzip
5+
modifying-vast-xml-allowed: true
6+
meta-info:
7+
maintainer-email: support@zentotem.net
8+
app-media-types:
9+
- banner
10+
- video
11+
- native
12+
site-media-types:
13+
- banner
14+
- video
15+
- native
16+
supported-vendors:
17+
vendor-id: 0
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Zentotem Adapter Params",
4+
"description": "A schema which validates params accepted by the Zentotem adapter",
5+
"type": "object",
6+
"properties": {}
7+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package org.prebid.server.bidder.zentotem;
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.jupiter.api.Test;
10+
import org.prebid.server.VertxTest;
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.HttpResponse;
16+
import org.prebid.server.bidder.model.Result;
17+
import org.prebid.server.proto.openrtb.ext.response.BidType;
18+
19+
import java.math.BigDecimal;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
import java.util.Set;
23+
import java.util.function.UnaryOperator;
24+
25+
import static java.util.Collections.singletonList;
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
28+
import static org.prebid.server.bidder.model.BidderError.badServerResponse;
29+
30+
public class ZentotemBidderTest extends VertxTest {
31+
32+
private static final String ENDPOINT_URL = "https://test.endpoint.com";
33+
34+
private final ZentotemBidder target = new ZentotemBidder(ENDPOINT_URL, jacksonMapper);
35+
36+
@Test
37+
public void creationShouldFailOnInvalidEndpointUrl() {
38+
assertThatIllegalArgumentException().isThrownBy(() -> new ZentotemBidder("invalid_url", jacksonMapper));
39+
}
40+
41+
@Test
42+
public void makeHttpRequestsShouldCreateSeparateRequestForEachImp() {
43+
// given
44+
final BidRequest bidRequest = givenBidRequest(
45+
imp -> imp.id("imp1"),
46+
imp -> imp.id("imp2"));
47+
48+
// when
49+
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
50+
51+
// then
52+
assertThat(result.getErrors()).isEmpty();
53+
assertThat(result.getValue()).hasSize(2)
54+
.extracting(HttpRequest::getImpIds)
55+
.containsExactly(Set.of("imp1"), Set.of("imp2"));
56+
}
57+
58+
@Test
59+
public void makeHttpRequestsShouldSetCorrectUriAndBody() {
60+
// given
61+
final BidRequest bidRequest = givenBidRequest(imp -> imp.id("imp1"));
62+
63+
// when
64+
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
65+
66+
// then
67+
final BidRequest expectedRequest = bidRequest.toBuilder()
68+
.imp(singletonList(bidRequest.getImp().getFirst()))
69+
.build();
70+
71+
assertThat(result.getErrors()).isEmpty();
72+
assertThat(result.getValue()).hasSize(1).first()
73+
.satisfies(httpRequest -> {
74+
assertThat(httpRequest.getUri()).isEqualTo(ENDPOINT_URL);
75+
assertThat(httpRequest.getPayload()).isEqualTo(expectedRequest);
76+
assertThat(httpRequest.getBody()).isEqualTo(jacksonMapper.encodeToBytes(expectedRequest));
77+
});
78+
}
79+
80+
@Test
81+
public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() {
82+
// given
83+
final BidderCall<BidRequest> httpCall = givenHttpCall("invalid_json");
84+
85+
// when
86+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
87+
88+
// then
89+
assertThat(result.getErrors()).hasSize(1)
90+
.allSatisfy(error -> {
91+
assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
92+
assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid_json'");
93+
});
94+
assertThat(result.getValue()).isEmpty();
95+
}
96+
97+
@Test
98+
public void makeBidsShouldReturnEmptyListWhenBidResponseOrSeatBidAreNull() throws JsonProcessingException {
99+
// given
100+
final BidResponse bidResponseWithNullSeatBid = BidResponse.builder().seatbid(null).build();
101+
final BidderCall<BidRequest> httpCallWithNullSeatBid =
102+
givenHttpCall(mapper.writeValueAsString(bidResponseWithNullSeatBid));
103+
104+
// when
105+
final Result<List<BidderBid>> nullSeatBidResult = target.makeBids(httpCallWithNullSeatBid, null);
106+
107+
// then
108+
assertThat(nullSeatBidResult.getErrors()).isEmpty();
109+
assertThat(nullSeatBidResult.getValue()).isEmpty();
110+
}
111+
112+
@Test
113+
public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
114+
// given
115+
final Bid bannerBid = givenBid(1);
116+
final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidResponse(bannerBid));
117+
118+
// when
119+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
120+
121+
// then
122+
assertThat(result.getErrors()).isEmpty();
123+
assertThat(result.getValue()).containsOnly(BidderBid.of(bannerBid, BidType.banner, "USD"));
124+
}
125+
126+
@Test
127+
public void makeBidsShouldReturnVideoBid() throws JsonProcessingException {
128+
// given
129+
final Bid videoBid = givenBid(2);
130+
final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidResponse(videoBid));
131+
132+
// when
133+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
134+
135+
// then
136+
assertThat(result.getErrors()).isEmpty();
137+
assertThat(result.getValue()).containsOnly(BidderBid.of(videoBid, BidType.video, "USD"));
138+
}
139+
140+
@Test
141+
public void makeBidsShouldReturnNativeBid() throws JsonProcessingException {
142+
// given
143+
final Bid nativeBid = givenBid(4);
144+
final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidResponse(nativeBid));
145+
146+
// when
147+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
148+
149+
// then
150+
assertThat(result.getErrors()).isEmpty();
151+
assertThat(result.getValue()).containsOnly(BidderBid.of(nativeBid, BidType.xNative, "USD"));
152+
}
153+
154+
@Test
155+
public void makeBidsShouldReturnErrorIfMtypeIsUnsupported() throws JsonProcessingException {
156+
// given
157+
final Bid bidWithUnsupportedMtype = givenBid(3);
158+
final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidResponse(bidWithUnsupportedMtype));
159+
160+
// when
161+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
162+
163+
// then
164+
assertThat(result.getValue()).isEmpty();
165+
assertThat(result.getErrors()).hasSize(1)
166+
.containsExactly(badServerResponse("could not define media type for impression: impId"));
167+
}
168+
169+
private static BidRequest givenBidRequest(UnaryOperator<Imp.ImpBuilder>... impCustomizers) {
170+
final List<Imp> imps = Arrays.stream(impCustomizers)
171+
.map(ZentotemBidderTest::givenImp)
172+
.toList();
173+
return BidRequest.builder().imp(imps).build();
174+
}
175+
176+
private static Imp givenImp(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
177+
return impCustomizer.apply(Imp.builder().id("impId")).build();
178+
}
179+
180+
private static Bid givenBid(Integer mtype) {
181+
return Bid.builder().id("bidId").impid("impId").price(BigDecimal.ONE).mtype(mtype).build();
182+
}
183+
184+
private static String givenBidResponse(Bid... bids) throws JsonProcessingException {
185+
return mapper.writeValueAsString(BidResponse.builder()
186+
.cur("USD")
187+
.seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build()))
188+
.build());
189+
}
190+
191+
private static BidderCall<BidRequest> givenHttpCall(String body) {
192+
return BidderCall.succeededHttp(
193+
HttpRequest.<BidRequest>builder().build(),
194+
HttpResponse.of(200, null, body),
195+
null);
196+
}
197+
}

0 commit comments

Comments
 (0)