Skip to content

Commit 67ef251

Browse files
New Optidigital Adapter (#4054)
1 parent a71c040 commit 67ef251

File tree

11 files changed

+490
-0
lines changed

11 files changed

+490
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.prebid.server.bidder.optidigital;
2+
3+
import com.iab.openrtb.request.BidRequest;
4+
import com.iab.openrtb.response.BidResponse;
5+
import com.iab.openrtb.response.SeatBid;
6+
import org.apache.commons.collections4.CollectionUtils;
7+
import org.prebid.server.bidder.Bidder;
8+
import org.prebid.server.bidder.model.BidderBid;
9+
import org.prebid.server.bidder.model.BidderCall;
10+
import org.prebid.server.bidder.model.BidderError;
11+
import org.prebid.server.bidder.model.HttpRequest;
12+
import org.prebid.server.bidder.model.Result;
13+
import org.prebid.server.json.DecodeException;
14+
import org.prebid.server.json.JacksonMapper;
15+
import org.prebid.server.proto.openrtb.ext.response.BidType;
16+
import org.prebid.server.util.BidderUtil;
17+
import org.prebid.server.util.HttpUtil;
18+
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.Objects;
23+
24+
public class OptidigitalBidder implements Bidder<BidRequest> {
25+
26+
private final String endpointUrl;
27+
private final JacksonMapper mapper;
28+
29+
public OptidigitalBidder(String endpointUrl, JacksonMapper mapper) {
30+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
31+
this.mapper = Objects.requireNonNull(mapper);
32+
}
33+
34+
@Override
35+
public final Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest bidRequest) {
36+
return Result.withValue(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper));
37+
}
38+
39+
@Override
40+
public final Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
41+
try {
42+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
43+
return Result.withValues(extractBids(bidResponse));
44+
} catch (DecodeException e) {
45+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
46+
}
47+
}
48+
49+
private static List<BidderBid> extractBids(BidResponse bidResponse) {
50+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
51+
return Collections.emptyList();
52+
}
53+
return bidsFromResponse(bidResponse);
54+
}
55+
56+
private static List<BidderBid> bidsFromResponse(BidResponse bidResponse) {
57+
return bidResponse.getSeatbid().stream()
58+
.filter(Objects::nonNull)
59+
.map(SeatBid::getBid)
60+
.filter(Objects::nonNull)
61+
.flatMap(Collection::stream)
62+
.map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur()))
63+
.toList();
64+
}
65+
66+
}
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.optidigital.OptidigitalBidder;
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/optidigital.yaml", factory = YamlPropertySourceFactory.class)
20+
public class OptidigitalConfiguration {
21+
22+
private static final String BIDDER_NAME = "optidigital";
23+
24+
@Bean("optidigitalConfigurationProperties")
25+
@ConfigurationProperties("adapters.optidigital")
26+
BidderConfigurationProperties configurationProperties() {
27+
return new BidderConfigurationProperties();
28+
}
29+
30+
@Bean
31+
BidderDeps optidigitalBidderDeps(BidderConfigurationProperties optidigitalConfigurationProperties,
32+
@NotBlank @Value("${external-url}") String externalUrl,
33+
JacksonMapper mapper) {
34+
35+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
36+
.withConfig(optidigitalConfigurationProperties)
37+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
38+
.bidderCreator(config -> new OptidigitalBidder(config.getEndpoint(), mapper))
39+
.assemble();
40+
}
41+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
adapters:
2+
optidigital:
3+
enabled: false
4+
endpoint: https://pbs.optidigital.com/bidder/openrtb2
5+
endpoint-compression: gzip
6+
ortb-version: "2.6"
7+
meta-info:
8+
maintainer-email: [email protected]
9+
app-media-types:
10+
- banner
11+
site-media-types:
12+
- banner
13+
dooh-media-types:
14+
- banner
15+
supported-vendors:
16+
vendor-id: 915
17+
usersync:
18+
cookie-family-name: optidigital
19+
iframe:
20+
url: https://scripts.opti-digital.com/js/presyncs2s.html?endpoint=optidigital&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir={{redirect_url}}
21+
support-cors: false
22+
uid-macro: '$UID'
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Optidigital Adapter Params",
4+
"description": "A schema which validates params accepted by the Optidigital adapter",
5+
"type": "object",
6+
"properties": {
7+
"publisherId": {
8+
"type": "string",
9+
"minLength": 2,
10+
"description": "Publisher ID"
11+
},
12+
"placementId": {
13+
"type": "string",
14+
"minLength": 1,
15+
"description": "Placement ID"
16+
},
17+
"divId": {
18+
"type": "string",
19+
"description": "Div ID"
20+
},
21+
"pageTemplate": {
22+
"type": "string",
23+
"description": "Page Template"
24+
}
25+
},
26+
"required": [
27+
"publisherId",
28+
"placementId"
29+
]
30+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package org.prebid.server.bidder.optidigital;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.iab.openrtb.request.Banner;
5+
import com.iab.openrtb.request.BidRequest;
6+
import com.iab.openrtb.request.Imp;
7+
import com.iab.openrtb.response.Bid;
8+
import com.iab.openrtb.response.BidResponse;
9+
import com.iab.openrtb.response.SeatBid;
10+
import org.junit.jupiter.api.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.response.BidType;
19+
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.function.UnaryOperator;
23+
24+
import static java.util.function.UnaryOperator.identity;
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
27+
28+
public class OptidigitalBidderTest extends VertxTest {
29+
30+
private static final String ENDPOINT_URL = "https://test.com";
31+
32+
private final OptidigitalBidder target = new OptidigitalBidder(ENDPOINT_URL, jacksonMapper);
33+
34+
@Test
35+
public void creationShouldFailOnInvalidEndpointUrl() {
36+
assertThatIllegalArgumentException()
37+
.isThrownBy(() -> new OptidigitalBidder("invalid_url", jacksonMapper));
38+
}
39+
40+
@Test
41+
public void makeHttpRequestsShouldNotModifyIncomingRequest() {
42+
// given
43+
final BidRequest bidRequest = givenBidRequest(identity(), identity());
44+
45+
// when
46+
final Result<List<HttpRequest<BidRequest>>> result;
47+
result = target.makeHttpRequests(bidRequest);
48+
49+
// then
50+
assertThat(result.getErrors()).isEmpty();
51+
assertThat(result.getValue()).hasSize(1)
52+
.extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
53+
.containsExactly(bidRequest);
54+
}
55+
56+
@Test
57+
public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
58+
// given
59+
final BidderCall<BidRequest> httpCall = givenHttpCall(null, "invalid");
60+
61+
// when
62+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
63+
64+
// then
65+
assertThat(result.getValue()).isEmpty();
66+
assertThat(result.getErrors()).hasSize(1)
67+
.allSatisfy(error -> {
68+
assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
69+
assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
70+
});
71+
}
72+
73+
@Test
74+
public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
75+
// given
76+
final BidderCall<BidRequest> httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
77+
78+
// when
79+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
80+
81+
// then
82+
assertThat(result.getErrors()).isEmpty();
83+
assertThat(result.getValue()).isEmpty();
84+
}
85+
86+
@Test
87+
public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
88+
// given
89+
final BidderCall<BidRequest> httpCall = givenHttpCall(null,
90+
mapper.writeValueAsString(BidResponse.builder().build()));
91+
92+
// when
93+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
94+
95+
// then
96+
assertThat(result.getErrors()).isEmpty();
97+
assertThat(result.getValue()).isEmpty();
98+
}
99+
100+
@Test
101+
public void makeBidsShouldReturnBidderBidWithNoErrors() throws JsonProcessingException {
102+
// given
103+
final BidRequest bidRequest = givenBidRequest(identity());
104+
final BidderCall<BidRequest> httpCall = givenHttpCall(bidRequest,
105+
mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("impId"))));
106+
107+
// when
108+
final Result<List<BidderBid>> result = target.makeBids(httpCall, bidRequest);
109+
110+
// then
111+
assertThat(result.getErrors()).isEmpty();
112+
assertThat(result.getValue())
113+
.containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.banner, "USD"));
114+
}
115+
116+
private static BidRequest givenBidRequest(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
117+
return givenBidRequest(identity(), impCustomizer);
118+
}
119+
120+
private static BidRequest givenBidRequest(
121+
UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
122+
UnaryOperator<Imp.ImpBuilder> impCustomizer) {
123+
124+
return bidRequestCustomizer.apply(BidRequest.builder()
125+
.id("requestId")
126+
.imp(Collections.singletonList(givenImp(impCustomizer))))
127+
.build();
128+
}
129+
130+
private static Imp givenImp(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
131+
return impCustomizer.apply(Imp.builder()
132+
.id("impId")
133+
.banner(Banner.builder().build())
134+
.ext(null))
135+
.build();
136+
}
137+
138+
private static BidResponse givenBidResponse(UnaryOperator<Bid.BidBuilder> bidCustomizer) {
139+
return BidResponse.builder()
140+
.seatbid(Collections.singletonList(SeatBid.builder()
141+
.bid(Collections.singletonList(bidCustomizer.apply(Bid.builder()).build()))
142+
.build()))
143+
.cur("USD")
144+
.build();
145+
}
146+
147+
private static BidderCall<BidRequest> givenHttpCall(BidRequest bidRequest, String body) {
148+
return BidderCall.succeededHttp(
149+
HttpRequest.<BidRequest>builder().payload(bidRequest).build(),
150+
HttpResponse.of(200, null, body),
151+
null);
152+
}
153+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.prebid.server.it;
2+
3+
import io.restassured.response.Response;
4+
import org.json.JSONException;
5+
import org.junit.jupiter.api.Test;
6+
import org.prebid.server.model.Endpoint;
7+
8+
import java.io.IOException;
9+
10+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
11+
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
12+
import static com.github.tomakehurst.wiremock.client.WireMock.post;
13+
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
14+
import static java.util.Collections.singletonList;
15+
16+
public class OptidigitalTest extends IntegrationTest {
17+
18+
@Test
19+
public void openrtb2AuctionShouldRespondWithBidsFromOptidigital() throws IOException, JSONException {
20+
// given
21+
WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/optidigital-exchange"))
22+
.withRequestBody(equalToJson(
23+
jsonFrom("openrtb2/optidigital/test-optidigital-bid-request.json")))
24+
.willReturn(aResponse().withBody(
25+
jsonFrom("openrtb2/optidigital/test-optidigital-bid-response.json"))));
26+
27+
// when
28+
final Response response =
29+
responseFor("openrtb2/optidigital/test-auction-optidigital-request.json", Endpoint.openrtb2_auction);
30+
31+
// then
32+
assertJsonEquals(
33+
"openrtb2/optidigital/test-auction-optidigital-response.json",
34+
response,
35+
singletonList("optidigital"));
36+
}
37+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"id": "request_id",
3+
"imp": [
4+
{
5+
"id": "imp_id",
6+
"banner": {
7+
"w": 300,
8+
"h": 250
9+
},
10+
"ext": {
11+
"optidigital": {
12+
"publisherId": "testPublisherId",
13+
"placementId": "testPlacementId"
14+
}
15+
}
16+
}
17+
],
18+
"tmax": 5000,
19+
"regs": {
20+
"gdpr": 0
21+
}
22+
}

0 commit comments

Comments
 (0)