Skip to content

Commit ae9b2f2

Browse files
AdverxoTest does not work
1 parent 6e46054 commit ae9b2f2

File tree

12 files changed

+503
-0
lines changed

12 files changed

+503
-0
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package org.prebid.server.bidder.adverxo;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.core.type.TypeReference;
5+
import com.fasterxml.jackson.databind.JsonNode;
6+
import com.iab.openrtb.request.BidRequest;
7+
import com.iab.openrtb.request.Imp;
8+
import com.iab.openrtb.response.Bid;
9+
import com.iab.openrtb.response.BidResponse;
10+
import com.iab.openrtb.response.SeatBid;
11+
import io.vertx.core.http.HttpMethod;
12+
import org.apache.commons.collections4.CollectionUtils;
13+
import org.apache.commons.lang3.ObjectUtils;
14+
import org.apache.commons.lang3.StringUtils;
15+
import org.prebid.server.bidder.Bidder;
16+
import org.prebid.server.bidder.model.BidderBid;
17+
import org.prebid.server.bidder.model.BidderCall;
18+
import org.prebid.server.bidder.model.BidderError;
19+
import org.prebid.server.bidder.model.HttpRequest;
20+
import org.prebid.server.bidder.model.Result;
21+
import org.prebid.server.currency.CurrencyConversionService;
22+
import org.prebid.server.exception.PreBidException;
23+
import org.prebid.server.json.DecodeException;
24+
import org.prebid.server.json.JacksonMapper;
25+
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
26+
import org.prebid.server.proto.openrtb.ext.request.adverxo.ExtImpAdverxo;
27+
import org.prebid.server.proto.openrtb.ext.response.BidType;
28+
import org.prebid.server.util.HttpUtil;
29+
30+
import java.math.BigDecimal;
31+
import java.util.ArrayList;
32+
import java.util.Collections;
33+
import java.util.List;
34+
import java.util.Objects;
35+
import java.util.Optional;
36+
37+
public class AdverxoBidder implements Bidder<BidRequest> {
38+
39+
private static final TypeReference<ExtPrebid<?, ExtImpAdverxo>> ADVERXO_EXT_TYPE_REFERENCE =
40+
new TypeReference<>() {
41+
};
42+
private static final String DEFAULT_BID_CURRENCY = "USD";
43+
private static final String ADUNIT_MACROS_ENDPOINT = "{{adUnitId}}";
44+
private static final String AUTH_MACROS_ENDPOINT = "{{auth}}";
45+
private static final String PRICE_MACRO = "${AUCTION_PRICE}";
46+
47+
private final String endpointUrl;
48+
private final JacksonMapper mapper;
49+
private final CurrencyConversionService currencyConversionService;
50+
51+
public AdverxoBidder(String endpointUrl, JacksonMapper mapper,
52+
CurrencyConversionService currencyConversionService) {
53+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
54+
this.mapper = mapper;
55+
this.currencyConversionService = currencyConversionService;
56+
}
57+
58+
@Override
59+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
60+
final List<BidderError> errors = new ArrayList<>();
61+
final List<HttpRequest<BidRequest>> requests = new ArrayList<>();
62+
63+
for (Imp imp : request.getImp()) {
64+
try {
65+
final ExtImpAdverxo extImp = parseImpExt(imp);
66+
final String endpoint = resolveEndpoint(extImp);
67+
final Imp modifiedImp = modifyImp(imp, request);
68+
final BidRequest outgoingRequest = createRequest(request, modifiedImp);
69+
70+
requests.add(createHttpRequest(outgoingRequest, endpoint, imp.getId()));
71+
} catch (PreBidException e) {
72+
errors.add(BidderError.badInput(e.getMessage()));
73+
}
74+
}
75+
76+
return Result.of(requests, errors);
77+
}
78+
79+
private ExtImpAdverxo parseImpExt(Imp imp) {
80+
try {
81+
return mapper.mapper().convertValue(imp.getExt(), ADVERXO_EXT_TYPE_REFERENCE).getBidder();
82+
} catch (IllegalArgumentException e) {
83+
throw new PreBidException("Error parsing ext.imp.bidder: " + e.getMessage());
84+
}
85+
}
86+
87+
private String resolveEndpoint(ExtImpAdverxo extImpAdverxo) {
88+
final String adUnitAsString = Optional.of(extImpAdverxo.getAdUnitId())
89+
.map(Object::toString)
90+
.orElse(StringUtils.EMPTY);
91+
final String authAsString = Optional.ofNullable(extImpAdverxo.getAuth())
92+
.map(Object::toString)
93+
.orElse(StringUtils.EMPTY);
94+
95+
return endpointUrl
96+
.replace(ADUNIT_MACROS_ENDPOINT, adUnitAsString)
97+
.replace(AUTH_MACROS_ENDPOINT, authAsString);
98+
}
99+
100+
private Imp modifyImp(Imp imp, BidRequest request) {
101+
final BigDecimal bidFloor = imp.getBidfloor();
102+
final String bidFloorCur = imp.getBidfloorcur();
103+
104+
if (bidFloor != null && bidFloor.compareTo(BigDecimal.ZERO) > 0
105+
&& StringUtils.isNotBlank(bidFloorCur)
106+
&& !StringUtils.equalsIgnoreCase(bidFloorCur, DEFAULT_BID_CURRENCY)) {
107+
108+
final BigDecimal convertedPrice = currencyConversionService.convertCurrency(
109+
bidFloor,
110+
request,
111+
bidFloorCur,
112+
DEFAULT_BID_CURRENCY
113+
);
114+
115+
return imp.toBuilder()
116+
.bidfloor(convertedPrice)
117+
.bidfloorcur(DEFAULT_BID_CURRENCY)
118+
.build();
119+
}
120+
return imp;
121+
}
122+
123+
private BidRequest createRequest(BidRequest originalRequest, Imp modifiedImp) {
124+
return originalRequest.toBuilder()
125+
.imp(Collections.singletonList(modifiedImp))
126+
.build();
127+
}
128+
129+
private HttpRequest<BidRequest> createHttpRequest(BidRequest outgoingRequest,
130+
String endpoint,
131+
String impId) {
132+
return HttpRequest.<BidRequest>builder()
133+
.method(HttpMethod.POST)
134+
.uri(endpoint)
135+
.headers(HttpUtil.headers())
136+
.body(mapper.encodeToBytes(outgoingRequest))
137+
.impIds(Collections.singleton(impId))
138+
.payload(outgoingRequest)
139+
.build();
140+
}
141+
142+
@Override
143+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
144+
try {
145+
final List<BidderError> bidderErrors = new ArrayList<>();
146+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
147+
return Result.withValues(extractBids(bidResponse, bidderErrors));
148+
} catch (DecodeException | PreBidException e) {
149+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
150+
}
151+
}
152+
153+
private List<BidderBid> extractBids(BidResponse bidResponse,
154+
List<BidderError> bidderErrors) {
155+
156+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
157+
return Collections.emptyList();
158+
}
159+
160+
final String currency = bidResponse.getCur();
161+
final List<BidderBid> bidderBids = new ArrayList<>();
162+
163+
for (SeatBid seatBid : bidResponse.getSeatbid()) {
164+
if (CollectionUtils.isEmpty(seatBid.getBid())) {
165+
continue;
166+
}
167+
168+
for (Bid bid : seatBid.getBid()) {
169+
final BidType bidType = getBidType(bid);
170+
final String resolvedAdm = resolveAdmForBidType(bid, bidType);
171+
final Bid processedBid = processBidMacros(bid, resolvedAdm);
172+
173+
bidderBids.add(BidderBid.of(processedBid, bidType, currency));
174+
}
175+
}
176+
177+
return bidderBids;
178+
}
179+
180+
private BidType getBidType(Bid bid) {
181+
final Integer markupType = ObjectUtils.defaultIfNull(bid.getMtype(), 0);
182+
183+
return switch (markupType) {
184+
case 1 -> BidType.banner;
185+
case 2 -> BidType.video;
186+
case 4 -> BidType.xNative;
187+
default -> throw new PreBidException(
188+
"could not define media type for impression: " + bid.getImpid());
189+
};
190+
}
191+
192+
private String resolveAdmForBidType(Bid bid, BidType bidType) {
193+
if (bidType != BidType.xNative) {
194+
return bid.getAdm();
195+
}
196+
197+
try {
198+
final JsonNode admNode = mapper.mapper().readTree(bid.getAdm());
199+
final JsonNode nativeNode = admNode.get("native");
200+
return nativeNode != null ? nativeNode.toString() : bid.getAdm();
201+
} catch (JsonProcessingException e) {
202+
throw new PreBidException("Error parsing native ADM: " + e.getMessage());
203+
}
204+
}
205+
206+
private Bid processBidMacros(Bid bid, String adm) {
207+
final String price = bid.getPrice() != null ? bid.getPrice().toPlainString() : "0";
208+
209+
return bid.toBuilder()
210+
.adm(replaceMacro(adm, price))
211+
.nurl(replaceMacro(bid.getNurl(), price))
212+
.build();
213+
}
214+
215+
private static String replaceMacro(String input, String value) {
216+
return input != null ? input.replace(PRICE_MACRO, value) : null;
217+
}
218+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.prebid.server.proto.openrtb.ext.request.adverxo;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Value;
6+
7+
@Value
8+
@AllArgsConstructor(staticName = "of")
9+
public class ExtImpAdverxo {
10+
11+
@JsonProperty("adUnitId")
12+
int adUnitId;
13+
@JsonProperty("auth")
14+
String auth;
15+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.prebid.server.spring.config.bidder;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
5+
import org.prebid.server.bidder.BidderDeps;
6+
import org.prebid.server.bidder.adverxo.AdverxoBidder;
7+
import org.prebid.server.currency.CurrencyConversionService;
8+
import org.prebid.server.json.JacksonMapper;
9+
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
10+
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
11+
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
12+
import org.prebid.server.spring.env.YamlPropertySourceFactory;
13+
import org.springframework.beans.factory.annotation.Value;
14+
import org.springframework.boot.context.properties.ConfigurationProperties;
15+
import org.springframework.context.annotation.Bean;
16+
import org.springframework.context.annotation.Configuration;
17+
import org.springframework.context.annotation.PropertySource;
18+
19+
@Configuration
20+
@PropertySource(value = "classpath:/bidder-config/adverxo.yaml", factory = YamlPropertySourceFactory.class)
21+
public class AdverxoBidderConfiguration {
22+
23+
private static final String BIDDER_NAME = "adverxo";
24+
25+
@Bean("adverxoConfigurationProperties")
26+
@ConfigurationProperties("adapters.adverxo")
27+
BidderConfigurationProperties configurationProperties() {
28+
return new BidderConfigurationProperties();
29+
}
30+
31+
@Bean
32+
BidderDeps adverxoBidderDeps(BidderConfigurationProperties adverxoConfigurationProperties,
33+
@NotBlank @Value("${external-url}") String externalUrl,
34+
JacksonMapper mapper, CurrencyConversionService currencyConversionService) {
35+
36+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
37+
.withConfig(adverxoConfigurationProperties)
38+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
39+
.bidderCreator(config -> new AdverxoBidder(config.getEndpoint(), mapper, currencyConversionService))
40+
.assemble();
41+
}
42+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
adapters:
2+
adverxo:
3+
endpoint: "https://pbsadverxo.com/auction?adUnitId={{adUnitId}}&auth={{auth}}"
4+
endpoint-compression: gzip
5+
meta-info:
6+
maintainer-email: [email protected]
7+
app-media-types:
8+
- banner
9+
- native
10+
- video
11+
site-media-types:
12+
- banner
13+
- native
14+
- video
15+
supported-vendors:
16+
vendor-id: 0
17+
userSync:
18+
cookie-family-name: adverxo
19+
iframe:
20+
url: https://pbsadverxo.com/usync?type=iframe&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}
21+
support-cors: false
22+
userMacro: $UID
23+
redirect:
24+
url: https://pbsadverxo.com/usync?type=image&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}
25+
support-cors: false
26+
userMacro: '$UID'
27+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Adverxo Adapter Params",
4+
"description": "A schema which validates params accepted by the Adverxo adapter",
5+
"type": "object",
6+
"properties": {
7+
"adUnitId": {
8+
"type": "integer",
9+
"minimum": 1,
10+
"description": "Unique identifier for the ad unit in Adverxo platform."
11+
},
12+
"auth": {
13+
"type": "string",
14+
"minLength": 6,
15+
"description": "Authentication token provided by Adverxo platform for the AdUnit."
16+
}
17+
},
18+
"required": ["adUnitId", "auth"]
19+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.prebid.server.bidder.adverxo;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.extension.ExtendWith;
6+
import org.mockito.Mock;
7+
import org.mockito.junit.jupiter.MockitoExtension;
8+
import org.prebid.server.VertxTest;
9+
import org.prebid.server.currency.CurrencyConversionService;
10+
11+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
12+
13+
@ExtendWith(MockitoExtension.class)
14+
15+
public class AdverxoBidderTest extends VertxTest {
16+
17+
private static final String ENDPOINT_URL = "https://test.endpoint.com";
18+
19+
@Mock
20+
private CurrencyConversionService currencyConversionService;
21+
22+
private AdverxoBidder target;
23+
24+
@BeforeEach
25+
public void setUp() {
26+
target = new AdverxoBidder(ENDPOINT_URL, jacksonMapper, currencyConversionService);
27+
}
28+
29+
@Test
30+
public void creationShouldFailOnInvalidEndpointUrl() {
31+
assertThatIllegalArgumentException().isThrownBy(() -> new AdverxoBidder(
32+
"invalid_url",
33+
jacksonMapper,
34+
currencyConversionService));
35+
}
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.equalTo;
12+
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
13+
import static com.github.tomakehurst.wiremock.client.WireMock.post;
14+
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
15+
import static java.util.Collections.singletonList;
16+
17+
public class AdverxoTest extends IntegrationTest{
18+
19+
@Test
20+
public void openrtb2AuctionShouldRespondWithBidsFromTheAdverxo() throws IOException, JSONException {
21+
// given
22+
WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adverxo-exchange"))
23+
.withQueryParam("adUnitId", equalTo("1"))
24+
.withQueryParam("auth", equalTo("123456"))
25+
.withRequestBody(equalToJson(jsonFrom("openrtb2/adverxo/test-adverxo-bid-request.json")))
26+
.willReturn(aResponse().withBody(jsonFrom("openrtb2/adverxo/test-adverxo-bid-response.json"))));
27+
28+
// when
29+
final Response response = responseFor("openrtb2/adverxo/test-auction-adverxo-request.json",
30+
Endpoint.openrtb2_auction);
31+
// then
32+
System.out.println("Actual response: " + response.asString());
33+
assertJsonEquals("openrtb2/adverxo/test-auction-adverxo-response.json", response,
34+
singletonList("adverxo"));
35+
}
36+
}

0 commit comments

Comments
 (0)