From 322c0ab841ac6324ad7f7dd6e174e45a2003b691 Mon Sep 17 00:00:00 2001 From: vmalitskyi Date: Wed, 26 Feb 2025 12:12:41 +0100 Subject: [PATCH 1/7] New adapter: Port Ogury adapter from PBS-Go --- .../server/bidder/ogury/OguryBidder.java | 285 ++++++++++++++ .../config/bidder/OguryConfiguration.java | 43 +++ src/main/resources/bidder-config/ogury.yaml | 17 + .../resources/static/bidder-params/ogury.json | 20 + .../server/bidder/ogury/OguryBidderTest.java | 356 ++++++++++++++++++ .../java/org/prebid/server/it/OguryTest.java | 32 ++ .../ogury/test-auction-ogury-request.json | 28 ++ .../ogury/test-auction-ogury-response.json | 38 ++ .../ogury/test-ogury-bid-request.json | 57 +++ .../ogury/test-ogury-bid-response.json | 20 + .../server/it/test-application.properties | 2 + 11 files changed, 898 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/OguryConfiguration.java create mode 100644 src/main/resources/bidder-config/ogury.yaml create mode 100644 src/main/resources/static/bidder-params/ogury.json create mode 100644 src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/OguryTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/ogury/test-ogury-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/ogury/test-ogury-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java new file mode 100644 index 00000000000..be85c68e71b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java @@ -0,0 +1,285 @@ +package org.prebid.server.bidder.ogury; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +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.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; + +public class OguryBidder implements Bidder { + + private static final String EXT_FIELD_BIDDER = "bidder"; + private static final String BIDDER_CURRENCY = "USD"; + private static final String PREBID_FIELD_ASSET_KEY = "assetKey"; + private static final String PREBID_FIELD_ADUNIT_ID = "adUnitId"; + + private final String endpointUrl; + private final CurrencyConversionService currencyConversionService; + private final JacksonMapper mapper; + + public OguryBidder(String endpointUrl, CurrencyConversionService currencyConversionService, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> httpRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + if (request == null || CollectionUtils.isEmpty(request.getImp())) { + errors.add(BidderError.badInput("There are no valid impressions to create bid request to ogury bidder")); + return Result.withErrors(errors); + } + + final List impsWithOguryParams = new ArrayList<>(); + List imps = modifyImps(request, impsWithOguryParams::add); + + final BidderError error = validateRequestKeys(request, impsWithOguryParams); + if (error != null) { + errors.add(error); + return Result.withErrors(errors); + } + + if (CollectionUtils.isNotEmpty(impsWithOguryParams)) { + imps = impsWithOguryParams; + } + + final BidRequest modifiedBidRequest = request.toBuilder().imp(imps).build(); + final MultiMap headers = buildHeaders(modifiedBidRequest); + httpRequests.add(BidderUtil.defaultRequest(modifiedBidRequest, headers, endpointUrl, mapper)); + + return Result.of(httpRequests, errors); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final HttpResponse response = getResponse(httpCall); + if (response == null || isNotHasContent(response)) { + return Result.empty(); + } + + final BidderError error = checkResponseStatusCodeForErrors(response); + if (error != null) { + return Result.withError(error); + } + + try { + final String body = response.getBody(); + if (StringUtils.isEmpty(body)) { + return Result.empty(); + } + + final BidResponse bidResponse = mapper.decodeValue(body, BidResponse.class); + + final List errors = new ArrayList<>(); + final List bidderBids = extractBids(bidResponse, errors::add); + + return Result.of(bidderBids, errors); + } catch (Exception e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List modifyImps(BidRequest request, Consumer impWithOguryParamsHandler) { + return Optional.of(request) + .map(BidRequest::getImp) + .map(sourceImps -> sourceImps.stream().map(imp -> { + final ObjectNode impExt = resolveImpExt(imp); + final ObjectNode impExtBidderHoist = resolveImpExtBidderHoist(impExt); + + final ObjectNode modifiedImpExt = modifyImpExt(impExt, impExtBidderHoist); + final BigDecimal bidFloor = resolveBidFloor(request, imp); + final Imp modifiedImp = modifyImp(imp, bidFloor, modifiedImpExt); + + if (hasOguryParams(impExtBidderHoist)) { + impWithOguryParamsHandler.accept(modifiedImp); + } + + return modifiedImp; + })) + .map(Stream::toList).orElse(null); + } + + private Imp modifyImp(Imp imp, BigDecimal bidFloor, ObjectNode modifiedImpExt) { + return imp.toBuilder() + .tagid(imp.getId()) + .bidfloor(bidFloor) + .bidfloorcur(BIDDER_CURRENCY) + .ext(modifiedImpExt) + .build(); + } + + private ObjectNode modifyImpExt(ObjectNode impExt, ObjectNode impExtBidderHoist) { + if (impExt == null || impExtBidderHoist == null) { + return impExt; + } + + final ObjectNode modifiedImpExt = impExt.deepCopy(); + Optional.ofNullable(impExtBidderHoist.fieldNames()) + .ifPresent(fields -> { + fields.forEachRemaining(field -> modifiedImpExt.set(field, impExtBidderHoist.get(field))); + modifiedImpExt.remove(EXT_FIELD_BIDDER); + }); + + return modifiedImpExt; + } + + private List extractBids(BidResponse bidResponse, Consumer bidErrorHandler) { + return Optional.ofNullable(bidResponse.getSeatbid()).stream() + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> { + try { + return BidderBid.of(bid, getBidType(bid), bidResponse.getCur()); + } catch (PreBidException e) { + bidErrorHandler.accept(BidderError.badServerResponse(e.getMessage())); + return null; + } + }) + .filter(Objects::nonNull) + .toList(); + } + + private HttpResponse getResponse(BidderCall httpCall) { + return Optional.ofNullable(httpCall) + .map(BidderCall::getResponse) + .orElse(null); + } + + private boolean hasOguryParams(ObjectNode impExtBidderHoist) { + return Optional.ofNullable(impExtBidderHoist).map(it -> it.get(PREBID_FIELD_ASSET_KEY) != null + && it.get(PREBID_FIELD_ADUNIT_ID) != null) + .orElse(false); + } + + private ObjectNode resolveImpExtBidderHoist(ObjectNode impExt) { + return (ObjectNode) Optional.ofNullable(impExt) + .map(ext -> ext.get(EXT_FIELD_BIDDER)) + .orElse(null); + } + + private ObjectNode resolveImpExt(Imp imp) { + return Optional.of(imp).map(Imp::getExt).orElse(null); + } + + private BigDecimal resolveBidFloor(BidRequest bidRequest, Imp imp) { + final BigDecimal bidFloor = imp.getBidfloor(); + final String bidFloorCurrency = imp.getBidfloorcur(); + + if (BidderUtil.isValidPrice(bidFloor) + && StringUtils.isNotBlank(bidFloorCurrency) + && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) { + return currencyConversionService.convertCurrency(bidFloor, bidRequest, bidFloorCurrency, BIDDER_CURRENCY); + } + + return bidFloor; + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for impression: `%s`".formatted(bid.getImpid())); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "Unsupported MType '%d', for impression '%s'".formatted(bid.getMtype(), bid.getImpid())); + }; + } + + private BidderError validateRequestKeys(BidRequest request, List impsWithOguryParams) { + final Optional siteOpt = Optional.of(request).map(BidRequest::getSite); + final Optional publisherId = siteOpt.map(Site::getPublisher).map(Publisher::getId); + + if (CollectionUtils.isEmpty(impsWithOguryParams) && (siteOpt.isEmpty() || publisherId.isEmpty())) { + return BidderError.badInput("Invalid request. assetKey/adUnitId or request.site.publisher.id required"); + } + + return null; + } + + private BidderError checkResponseStatusCodeForErrors(HttpResponse response) { + final int statusCode = response.getStatusCode(); + + if (statusCode == HttpResponseStatus.BAD_REQUEST.code()) { + return BidderError.badInput("Unexpected status code: %d. Run with request.debug = 1 for more info" + .formatted(statusCode)); + } + + if (statusCode != HttpResponseStatus.OK.code()) { + return BidderError.generic("Unexpected status code: %d. Run with request.debug = 1 for more info" + .formatted(statusCode)); + } + + return null; + } + + private boolean isNotHasContent(HttpResponse response) { + return Optional.of(response) + .map(HttpResponse::getStatusCode) + .map(code -> code == HttpResponseStatus.NO_CONTENT.code() || StringUtils.isEmpty(response.getBody())) + .orElse(false); + } + + private MultiMap buildHeaders(BidRequest request) { + final MultiMap headers = HttpUtil.headers(); + + Optional.ofNullable(request) + .map(BidRequest::getDevice) + .ifPresentOrElse(device -> { + final String lang = device.getLanguage(); + headers.add(HttpUtil.USER_AGENT_HEADER, device.getUa()) + .add(HttpUtil.ACCEPT_LANGUAGE_HEADER, lang != null ? lang : "en-US"); + + Optional.of(device) + .map(Device::getIp) + .ifPresent(ip -> headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip)); + + Optional.of(device) + .map(Device::getIpv6) + .ifPresent(ip -> headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip)); + }, () -> headers.add(HttpUtil.ACCEPT_LANGUAGE_HEADER, "en-US")); + + return headers; + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OguryConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OguryConfiguration.java new file mode 100644 index 00000000000..5e6174a3e35 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/OguryConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.ogury.OguryBidder; +import org.prebid.server.currency.CurrencyConversionService; +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 jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/ogury.yaml", factory = YamlPropertySourceFactory.class) +public class OguryConfiguration { + + private static final String BIDDER_NAME = "ogury"; + + @Bean("oguryConfigurationProperties") + @ConfigurationProperties("adapters.ogury") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps oguryBidderDeps(BidderConfigurationProperties oguryConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(oguryConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new OguryBidder(config.getEndpoint(), currencyConversionService, mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/ogury.yaml b/src/main/resources/bidder-config/ogury.yaml new file mode 100644 index 00000000000..7423f78c534 --- /dev/null +++ b/src/main/resources/bidder-config/ogury.yaml @@ -0,0 +1,17 @@ +adapters: + ogury: + endpoint: "https://prebids2s.presage.io/api/header-bidding-request" + endpointCompression: gzip + geoscope: + - global + meta-info: + maintainer-email: deliveryservices@ogury.co + site-media-types: + - banner + vendor-id: 31 + usersync: + cookie-family-name: ogury + iframe: + url: "https://ms-cookie-sync.presage.io/user-sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}&source=prebids2s" + uid-macro: "{{OGURY_UID}}" + support-cors: false diff --git a/src/main/resources/static/bidder-params/ogury.json b/src/main/resources/static/bidder-params/ogury.json new file mode 100644 index 00000000000..b05902ee0a4 --- /dev/null +++ b/src/main/resources/static/bidder-params/ogury.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Ogury Adapter Params", + "description": "A schema which validates params accepted by the Ogury adapter", + "type": "object", + "properties": { + "assetKey": { + "type": [ + "string" + ], + "description": "The asset key provided by Ogury" + }, + "adUnitId": { + "type": [ + "string" + ], + "description": "Ad unit id configured with Ogury" + } + } +} diff --git a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java new file mode 100644 index 00000000000..a66b1ad5fda --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java @@ -0,0 +1,356 @@ +package org.prebid.server.bidder.ogury; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +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 io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +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.CompositeBidderResponse; +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.currency.CurrencyConversionService; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class OguryBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + + @Mock + private CurrencyConversionService currencyConversionService; + + private OguryBidder target; + + @BeforeEach + public void setUp() { + target = new OguryBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); + } + + @Test + public void shouldFailOnInvalidEndpointUrl() { + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> new OguryBidder("invalid_url", currencyConversionService, jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldEncodePassedBidRequest() { + // given + when(currencyConversionService.convertCurrency(any(), any(), any(), any())).thenReturn(BigDecimal.ONE); + final BidRequest bidRequest = givenBidRequest(); + final BidRequest modifiedBidRequest = givenModifiedBidRequest(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final MultiMap expectedHeaders = HttpUtil.headers() + .add(HttpUtil.USER_AGENT_HEADER, "ua") + .add(HttpUtil.ACCEPT_LANGUAGE_HEADER, "en-US") + .add(HttpUtil.X_FORWARDED_FOR_HEADER, "0.0.0.0") + .add(HttpUtil.X_FORWARDED_FOR_HEADER, "ip6"); + final Result>> expectedResult = Result.withValue(HttpRequest.builder() + .method(HttpMethod.POST) + .uri(ENDPOINT_URL) + .headers(expectedHeaders) + .impIds(Set.of("imp_id")) + .body(jacksonMapper.encodeToBytes(modifiedBidRequest)) + .payload(modifiedBidRequest) + .build()); + assertThat(result.getValue()).usingRecursiveComparison().isEqualTo(expectedResult.getValue()); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenRequestDoesNotHaveImpression() { + // given + final BidRequest bidrequest = givenBidRequest(bidRequest -> bidRequest.imp(null)); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getErrors()).isNotEmpty() + .contains(BidderError.badInput("There are no valid impressions to create bid request to ogury bidder")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenRequestDoesNotHaveOguryKeys() { + // given + final BidRequest bidrequest = givenBidRequest(bidRequest -> bidRequest.imp(List.of(Imp.builder().build()))); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getErrors()).isNotEmpty() + .contains(BidderError.badInput( + "Invalid request. assetKey/adUnitId or request.site.publisher.id required")); + } + + @Test + public void makeBidderResponseShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + assertThat(result.getBids()).isEmpty(); + } + + @Test + public void makeBidderResponseShouldReturnErrorIfResponseCodeIsBadRequest() { + // given + final BidderCall httpCall = givenHttpCall(HttpResponseStatus.BAD_REQUEST.code(), "invalid"); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()) + .startsWith("Unexpected status code: 400. Run with request.debug = 1 for more info"); + }); + assertThat(result.getBids()).isEmpty(); + } + + @Test + public void makeBidderResponseShouldReturnErrorIfResponseCodeIsNotOK() { + // given + final BidderCall httpCall = givenHttpCall(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), + "invalid"); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.generic); + assertThat(error.getMessage()) + .startsWith("Unexpected status code: 500. Run with request.debug = 1 for more info"); + }); + assertThat(result.getBids()).isEmpty(); + } + + @Test + public void makeBidderResponseShouldReturnEmptyListIfBidResponseIsNull() { + // given + final BidderCall httpCall = givenHttpCall(null); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()).isEmpty(); + } + + @Test + public void makeBidderResponseShouldReturnErrorWhenResponseBodyIsWrong() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isNotEmpty(); + assertThat(result.getBids()).isEmpty(); + } + + @Test + public void makeBidderResponseShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()).isEmpty(); + } + + @Test + public void makeBidderResponseShouldReturnEmptyListIfBidResponseSeatBidIsEmpty() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + mapper.writeValueAsString(BidResponse.builder().seatbid(emptyList()).build())); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()).isEmpty(); + } + + @Test + public void makeBidderResponseShouldReturnErrorWhenBidMTypeIsNotPresent() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bid -> bid.impid("123"))); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("Missing MType for impression: `123`")); + assertThat(result.getBids()).isEmpty(); + } + + @Test + public void makeBidderResponseShouldReturnErrorWhenBidMTypeIsNotSupported() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bid -> bid.impid("123").mtype(10))); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("Unsupported MType '10', for impression '123'")); + assertThat(result.getBids()).isEmpty(); + } + + @Test + public void makeBidderResponseShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bid -> bid.mtype(1))); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()) + .extracting(BidderBid::getType) + .containsExactly(BidType.banner); + } + + @Test + public void makeBidderResponseShouldReturnBidWithCurFromResponse() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bid -> bid.mtype(1))); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()) + .extracting(BidderBid::getBidCurrency) + .containsExactly("CUR"); + } + + private static String givenBidResponse(UnaryOperator bidCustomizer) + throws JsonProcessingException { + + return mapper.writeValueAsString(BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .cur("CUR") + .build()); + } + + private BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { + return bidRequestCustomizer.apply(BidRequest.builder() + .id("id")) + .build(); + } + + private BidRequest givenBidRequest() { + final ObjectNode ogury = mapper.createObjectNode(); + ogury.putIfAbsent("adUnitId", TextNode.valueOf("1")); + ogury.putIfAbsent("assetKey", TextNode.valueOf("key")); + final ObjectNode bidder = mapper.createObjectNode(); + bidder.putIfAbsent("bidder", ogury); + + return givenBidRequest(bidRequest -> bidRequest.device(Device.builder() + .ua("ua") + .ip("0.0.0.0") + .ipv6("ip6") + .build()) + .imp(List.of(Imp.builder() + .id("imp_id") + .bidfloor(BigDecimal.TWO) + .bidfloorcur("CAD") + .ext(bidder) + .build()))); + } + + private BidRequest givenModifiedBidRequest() { + final ObjectNode oguryKeys = mapper.createObjectNode(); + oguryKeys.putIfAbsent("adUnitId", TextNode.valueOf("1")); + oguryKeys.putIfAbsent("assetKey", TextNode.valueOf("key")); + + return givenBidRequest(bidRequest -> bidRequest.device(Device.builder() + .ua("ua") + .ip("0.0.0.0") + .ipv6("ip6") + .build()) + .imp(List.of(Imp.builder() + .id("imp_id") + .bidfloor(BigDecimal.ONE) + .bidfloorcur("USD") + .tagid("imp_id") + .ext(oguryKeys) + .bidfloorcur("USD") + .build()))); + } + + private static BidderCall givenHttpCall(String body) { + return givenHttpCall(HttpResponseStatus.OK.code(), body); + } + + private static BidderCall givenHttpCall(int statusCode, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(statusCode, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/OguryTest.java b/src/test/java/org/prebid/server/it/OguryTest.java new file mode 100644 index 00000000000..60be4430217 --- /dev/null +++ b/src/test/java/org/prebid/server/it/OguryTest.java @@ -0,0 +1,32 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; + +public class OguryTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromOgury() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/ogury-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/ogury/test-ogury-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/ogury/test-ogury-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/ogury/test-auction-ogury-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/ogury/test-auction-ogury-response.json", response, singletonList("ogury")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-request.json new file mode 100644 index 00000000000..409c6e9133a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-request.json @@ -0,0 +1,28 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "prebid": { + "bidder": { + "ogury": { + "adUnitId": "1", + "assetKey": "key" + } + } + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-response.json new file mode 100644 index 00000000000..299240bce6f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 300, + "h": 250, + "exp": 300, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 1.25 + } + } + ], + "seat": "ogury", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "ogury": "{{ ogury.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-ogury-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-ogury-bid-request.json new file mode 100644 index 00000000000..33ab5c5bafe --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-ogury-bid-request.json @@ -0,0 +1,57 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "imp_id", + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "adUnitId": "1", + "assetKey": "key" + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-ogury-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-ogury-bid-response.json new file mode 100644 index 00000000000..4f4ed625a01 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-ogury-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 250, + "w": 300, + "mtype": 1 + } + ] + } + ], + "bidid": "bid_id" +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 851962c5192..e75ef3710c3 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -332,6 +332,8 @@ adapters.nextmillennium.endpoint=http://localhost:8090/nextmillennium-exchange adapters.nextmillennium.extra-info.nmmFlags=1 adapters.nobid.enabled=true adapters.nobid.endpoint=http://localhost:8090/nobid-exchange?pubid= +adapters.ogury.enabled=true +adapters.ogury.endpoint=http://localhost:8090/ogury-exchange adapters.oms.enabled=true adapters.oms.endpoint=http://localhost:8090/oms-exchange adapters.onetag.enabled=true From d52e31101a1253a18c4b8e726fe323ebaf0e2b55 Mon Sep 17 00:00:00 2001 From: vmalitskyi Date: Wed, 26 Feb 2025 13:44:51 +0100 Subject: [PATCH 2/7] Fix redirect url params --- src/main/resources/bidder-config/ogury.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/bidder-config/ogury.yaml b/src/main/resources/bidder-config/ogury.yaml index 7423f78c534..32ccccac07e 100644 --- a/src/main/resources/bidder-config/ogury.yaml +++ b/src/main/resources/bidder-config/ogury.yaml @@ -12,6 +12,6 @@ adapters: usersync: cookie-family-name: ogury iframe: - url: "https://ms-cookie-sync.presage.io/user-sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}&source=prebids2s" + url: "https://ms-cookie-sync.presage.io/user-sync.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&source=prebids2s" uid-macro: "{{OGURY_UID}}" support-cors: false From 6918245ba0f853d693e865a0ee03ae53e848941d Mon Sep 17 00:00:00 2001 From: vmalitskyi Date: Fri, 7 Mar 2025 14:22:40 +0100 Subject: [PATCH 3/7] Ogury adapter: Code cleanup --- .../server/bidder/ogury/OguryBidder.java | 54 ++++++++----------- .../server/bidder/ogury/OguryBidderTest.java | 13 ----- 2 files changed, 23 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java index be85c68e71b..f879e83b9f2 100644 --- a/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java +++ b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java @@ -34,7 +34,6 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; -import java.util.stream.Stream; public class OguryBidder implements Bidder { @@ -54,29 +53,42 @@ public OguryBidder(String endpointUrl, CurrencyConversionService currencyConvers } @Override - public Result>> makeHttpRequests(BidRequest request) { + public Result>> makeHttpRequests(BidRequest bidRequest) { final List> httpRequests = new ArrayList<>(); final List errors = new ArrayList<>(); - if (request == null || CollectionUtils.isEmpty(request.getImp())) { - errors.add(BidderError.badInput("There are no valid impressions to create bid request to ogury bidder")); - return Result.withErrors(errors); - } - + List modifiedImps = new ArrayList<>(); final List impsWithOguryParams = new ArrayList<>(); - List imps = modifyImps(request, impsWithOguryParams::add); - final BidderError error = validateRequestKeys(request, impsWithOguryParams); + for (Imp imp : bidRequest.getImp()) { + try { + final ObjectNode impExt = resolveImpExt(imp); + final ObjectNode impExtBidderHoist = resolveImpExtBidderHoist(impExt); + + final ObjectNode modifiedImpExt = modifyImpExt(impExt, impExtBidderHoist); + final BigDecimal bidFloor = resolveBidFloor(bidRequest, imp); + final Imp modifiedImp = modifyImp(imp, bidFloor, modifiedImpExt); + modifiedImps.add(modifiedImp); + + if (hasOguryParams(impExtBidderHoist)) { + impsWithOguryParams.add(modifiedImp); + } + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + final BidderError error = validateRequestKeys(bidRequest, impsWithOguryParams); if (error != null) { errors.add(error); return Result.withErrors(errors); } if (CollectionUtils.isNotEmpty(impsWithOguryParams)) { - imps = impsWithOguryParams; + modifiedImps = impsWithOguryParams; } - final BidRequest modifiedBidRequest = request.toBuilder().imp(imps).build(); + final BidRequest modifiedBidRequest = bidRequest.toBuilder().imp(modifiedImps).build(); final MultiMap headers = buildHeaders(modifiedBidRequest); httpRequests.add(BidderUtil.defaultRequest(modifiedBidRequest, headers, endpointUrl, mapper)); @@ -112,26 +124,6 @@ public Result> makeBids(BidderCall httpCall, BidRequ } } - private List modifyImps(BidRequest request, Consumer impWithOguryParamsHandler) { - return Optional.of(request) - .map(BidRequest::getImp) - .map(sourceImps -> sourceImps.stream().map(imp -> { - final ObjectNode impExt = resolveImpExt(imp); - final ObjectNode impExtBidderHoist = resolveImpExtBidderHoist(impExt); - - final ObjectNode modifiedImpExt = modifyImpExt(impExt, impExtBidderHoist); - final BigDecimal bidFloor = resolveBidFloor(request, imp); - final Imp modifiedImp = modifyImp(imp, bidFloor, modifiedImpExt); - - if (hasOguryParams(impExtBidderHoist)) { - impWithOguryParamsHandler.accept(modifiedImp); - } - - return modifiedImp; - })) - .map(Stream::toList).orElse(null); - } - private Imp modifyImp(Imp imp, BigDecimal bidFloor, ObjectNode modifiedImpExt) { return imp.toBuilder() .tagid(imp.getId()) diff --git a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java index a66b1ad5fda..c4cab6a3936 100644 --- a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java @@ -90,19 +90,6 @@ public void makeHttpRequestsShouldEncodePassedBidRequest() { assertThat(result.getErrors()).isEmpty(); } - @Test - public void makeHttpRequestsShouldReturnErrorWhenRequestDoesNotHaveImpression() { - // given - final BidRequest bidrequest = givenBidRequest(bidRequest -> bidRequest.imp(null)); - - // when - final Result>> result = target.makeHttpRequests(bidrequest); - - // then - assertThat(result.getErrors()).isNotEmpty() - .contains(BidderError.badInput("There are no valid impressions to create bid request to ogury bidder")); - } - @Test public void makeHttpRequestsShouldReturnErrorWhenRequestDoesNotHaveOguryKeys() { // given From ad893a9436ce6a1aea9e2fe1fc83eedbd4167599 Mon Sep 17 00:00:00 2001 From: vmalitskyi Date: Wed, 12 Mar 2025 10:24:48 +0100 Subject: [PATCH 4/7] Ogury adapter: Code cleanup --- .../server/bidder/ogury/OguryBidder.java | 247 +++++++----------- .../server/bidder/ogury/OguryBidderTest.java | 12 +- .../ogury/test-auction-ogury-request.json | 1 + 3 files changed, 107 insertions(+), 153 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java index f879e83b9f2..dae8d7f4a02 100644 --- a/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java +++ b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java @@ -9,7 +9,6 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.MultiMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -18,7 +17,7 @@ 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.Price; import org.prebid.server.bidder.model.Result; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; @@ -33,7 +32,6 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.function.Consumer; public class OguryBidder implements Bidder { @@ -57,7 +55,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ final List> httpRequests = new ArrayList<>(); final List errors = new ArrayList<>(); - List modifiedImps = new ArrayList<>(); + final List modifiedImps = new ArrayList<>(); final List impsWithOguryParams = new ArrayList<>(); for (Imp imp : bidRequest.getImp()) { @@ -65,9 +63,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ final ObjectNode impExt = resolveImpExt(imp); final ObjectNode impExtBidderHoist = resolveImpExtBidderHoist(impExt); - final ObjectNode modifiedImpExt = modifyImpExt(impExt, impExtBidderHoist); - final BigDecimal bidFloor = resolveBidFloor(bidRequest, imp); - final Imp modifiedImp = modifyImp(imp, bidFloor, modifiedImpExt); + final Imp modifiedImp = modifyImp(imp, bidRequest, impExtBidderHoist); modifiedImps.add(modifiedImp); if (hasOguryParams(impExtBidderHoist)) { @@ -78,37 +74,105 @@ public Result>> makeHttpRequests(BidRequest bidRequ } } - final BidderError error = validateRequestKeys(bidRequest, impsWithOguryParams); - if (error != null) { - errors.add(error); + final boolean isKeysValid = validateRequestKeys(bidRequest, impsWithOguryParams); + if (!isKeysValid) { + errors.add(BidderError.badInput( + "Invalid request. assetKey/adUnitId or request.site.publisher.id required")); return Result.withErrors(errors); } - if (CollectionUtils.isNotEmpty(impsWithOguryParams)) { - modifiedImps = impsWithOguryParams; - } + final BidRequest modifiedBidRequest = bidRequest.toBuilder() + .imp(CollectionUtils.isNotEmpty(impsWithOguryParams) ? impsWithOguryParams : modifiedImps) + .build(); - final BidRequest modifiedBidRequest = bidRequest.toBuilder().imp(modifiedImps).build(); - final MultiMap headers = buildHeaders(modifiedBidRequest); + final MultiMap headers = resolveHeaders(modifiedBidRequest.getDevice()); httpRequests.add(BidderUtil.defaultRequest(modifiedBidRequest, headers, endpointUrl, mapper)); return Result.of(httpRequests, errors); } - @Override - public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - final HttpResponse response = getResponse(httpCall); - if (response == null || isNotHasContent(response)) { - return Result.empty(); + private ObjectNode resolveImpExt(Imp imp) { + return Optional.of(imp).map(Imp::getExt).orElse(null); + } + + private ObjectNode resolveImpExtBidderHoist(ObjectNode impExt) { + return (ObjectNode) Optional.ofNullable(impExt) + .map(ext -> ext.get(EXT_FIELD_BIDDER)) + .orElse(null); + } + + private Imp modifyImp(Imp imp, BidRequest bidRequest, ObjectNode impExtBidderHoist) { + final Price price = resolvePrice(imp, bidRequest); + return imp.toBuilder() + .tagid(imp.getId()) + .bidfloor(price.getValue()) + .bidfloorcur(price.getCurrency()) + .ext(modifyExt(imp.getExt(), impExtBidderHoist)) + .build(); + } + + private Price resolvePrice(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, BIDDER_CURRENCY) + ? convertBidFloor(initialBidFloorPrice, bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency( + bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + BIDDER_CURRENCY); + + return Price.of(BIDDER_CURRENCY, convertedPrice); + } + + private ObjectNode modifyExt(ObjectNode impExt, ObjectNode impExtBidderHoist) { + if (impExt == null || impExtBidderHoist == null) { + return impExt; } - final BidderError error = checkResponseStatusCodeForErrors(response); - if (error != null) { - return Result.withError(error); + final ObjectNode modifiedImpExt = impExt.deepCopy(); + impExtBidderHoist.fieldNames().forEachRemaining(field -> + modifiedImpExt.set(field, impExtBidderHoist.get(field))); + modifiedImpExt.remove(EXT_FIELD_BIDDER); + + return modifiedImpExt; + } + + private boolean hasOguryParams(ObjectNode impExtBidderHoist) { + return impExtBidderHoist != null + && impExtBidderHoist.has(PREBID_FIELD_ASSET_KEY) + && impExtBidderHoist.has(PREBID_FIELD_ADUNIT_ID); + } + + private boolean validateRequestKeys(BidRequest request, List impsWithOguryParams) { + return !CollectionUtils.isEmpty(impsWithOguryParams) && Optional.ofNullable(request.getSite()) + .map(Site::getPublisher) + .map(Publisher::getId) + .isEmpty(); + } + + private MultiMap resolveHeaders(Device device) { + final MultiMap headers = HttpUtil.headers().add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, device.getLanguage()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + } else { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, "en-US"); } + return headers; + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { - final String body = response.getBody(); + final String body = httpCall.getResponse().getBody(); if (StringUtils.isEmpty(body)) { return Result.empty(); } @@ -116,7 +180,7 @@ public Result> makeBids(BidderCall httpCall, BidRequ final BidResponse bidResponse = mapper.decodeValue(body, BidResponse.class); final List errors = new ArrayList<>(); - final List bidderBids = extractBids(bidResponse, errors::add); + final List bidderBids = extractBids(bidResponse, errors); return Result.of(bidderBids, errors); } catch (Exception e) { @@ -124,83 +188,28 @@ public Result> makeBids(BidderCall httpCall, BidRequ } } - private Imp modifyImp(Imp imp, BigDecimal bidFloor, ObjectNode modifiedImpExt) { - return imp.toBuilder() - .tagid(imp.getId()) - .bidfloor(bidFloor) - .bidfloorcur(BIDDER_CURRENCY) - .ext(modifiedImpExt) - .build(); - } - - private ObjectNode modifyImpExt(ObjectNode impExt, ObjectNode impExtBidderHoist) { - if (impExt == null || impExtBidderHoist == null) { - return impExt; - } - - final ObjectNode modifiedImpExt = impExt.deepCopy(); - Optional.ofNullable(impExtBidderHoist.fieldNames()) - .ifPresent(fields -> { - fields.forEachRemaining(field -> modifiedImpExt.set(field, impExtBidderHoist.get(field))); - modifiedImpExt.remove(EXT_FIELD_BIDDER); - }); - - return modifiedImpExt; - } - - private List extractBids(BidResponse bidResponse, Consumer bidErrorHandler) { - return Optional.ofNullable(bidResponse.getSeatbid()).stream() + private List extractBids(BidResponse bidResponse, List errors) { + return Optional.ofNullable(bidResponse) + .map(BidResponse::getSeatbid) + .stream() .flatMap(Collection::stream) .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(Objects::nonNull) - .map(bid -> { - try { - return BidderBid.of(bid, getBidType(bid), bidResponse.getCur()); - } catch (PreBidException e) { - bidErrorHandler.accept(BidderError.badServerResponse(e.getMessage())); - return null; - } - }) + .map(bid -> createBidderBid(bid, bidResponse, errors)) .filter(Objects::nonNull) .toList(); } - private HttpResponse getResponse(BidderCall httpCall) { - return Optional.ofNullable(httpCall) - .map(BidderCall::getResponse) - .orElse(null); - } - - private boolean hasOguryParams(ObjectNode impExtBidderHoist) { - return Optional.ofNullable(impExtBidderHoist).map(it -> it.get(PREBID_FIELD_ASSET_KEY) != null - && it.get(PREBID_FIELD_ADUNIT_ID) != null) - .orElse(false); - } - - private ObjectNode resolveImpExtBidderHoist(ObjectNode impExt) { - return (ObjectNode) Optional.ofNullable(impExt) - .map(ext -> ext.get(EXT_FIELD_BIDDER)) - .orElse(null); - } - - private ObjectNode resolveImpExt(Imp imp) { - return Optional.of(imp).map(Imp::getExt).orElse(null); - } - - private BigDecimal resolveBidFloor(BidRequest bidRequest, Imp imp) { - final BigDecimal bidFloor = imp.getBidfloor(); - final String bidFloorCurrency = imp.getBidfloorcur(); - - if (BidderUtil.isValidPrice(bidFloor) - && StringUtils.isNotBlank(bidFloorCurrency) - && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) { - return currencyConversionService.convertCurrency(bidFloor, bidRequest, bidFloorCurrency, BIDDER_CURRENCY); + private BidderBid createBidderBid(Bid bid, BidResponse bidResponse, List errors) { + try { + return BidderBid.of(bid, getBidType(bid), bidResponse.getCur()); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; } - - return bidFloor; } private static BidType getBidType(Bid bid) { @@ -218,60 +227,4 @@ private static BidType getBidType(Bid bid) { "Unsupported MType '%d', for impression '%s'".formatted(bid.getMtype(), bid.getImpid())); }; } - - private BidderError validateRequestKeys(BidRequest request, List impsWithOguryParams) { - final Optional siteOpt = Optional.of(request).map(BidRequest::getSite); - final Optional publisherId = siteOpt.map(Site::getPublisher).map(Publisher::getId); - - if (CollectionUtils.isEmpty(impsWithOguryParams) && (siteOpt.isEmpty() || publisherId.isEmpty())) { - return BidderError.badInput("Invalid request. assetKey/adUnitId or request.site.publisher.id required"); - } - - return null; - } - - private BidderError checkResponseStatusCodeForErrors(HttpResponse response) { - final int statusCode = response.getStatusCode(); - - if (statusCode == HttpResponseStatus.BAD_REQUEST.code()) { - return BidderError.badInput("Unexpected status code: %d. Run with request.debug = 1 for more info" - .formatted(statusCode)); - } - - if (statusCode != HttpResponseStatus.OK.code()) { - return BidderError.generic("Unexpected status code: %d. Run with request.debug = 1 for more info" - .formatted(statusCode)); - } - - return null; - } - - private boolean isNotHasContent(HttpResponse response) { - return Optional.of(response) - .map(HttpResponse::getStatusCode) - .map(code -> code == HttpResponseStatus.NO_CONTENT.code() || StringUtils.isEmpty(response.getBody())) - .orElse(false); - } - - private MultiMap buildHeaders(BidRequest request) { - final MultiMap headers = HttpUtil.headers(); - - Optional.ofNullable(request) - .map(BidRequest::getDevice) - .ifPresentOrElse(device -> { - final String lang = device.getLanguage(); - headers.add(HttpUtil.USER_AGENT_HEADER, device.getUa()) - .add(HttpUtil.ACCEPT_LANGUAGE_HEADER, lang != null ? lang : "en-US"); - - Optional.of(device) - .map(Device::getIp) - .ifPresent(ip -> headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip)); - - Optional.of(device) - .map(Device::getIpv6) - .ifPresent(ip -> headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip)); - }, () -> headers.add(HttpUtil.ACCEPT_LANGUAGE_HEADER, "en-US")); - - return headers; - } } diff --git a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java index c4cab6a3936..077c1842fa7 100644 --- a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java @@ -132,9 +132,9 @@ public void makeBidderResponseShouldReturnErrorIfResponseCodeIsBadRequest() { // then assertThat(result.getErrors()).hasSize(1) .allSatisfy(error -> { - assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); assertThat(error.getMessage()) - .startsWith("Unexpected status code: 400. Run with request.debug = 1 for more info"); + .startsWith("Failed to decode: Unrecognized token 'invalid'"); }); assertThat(result.getBids()).isEmpty(); } @@ -151,9 +151,9 @@ public void makeBidderResponseShouldReturnErrorIfResponseCodeIsNotOK() { // then assertThat(result.getErrors()).hasSize(1) .allSatisfy(error -> { - assertThat(error.getType()).isEqualTo(BidderError.Type.generic); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); assertThat(error.getMessage()) - .startsWith("Unexpected status code: 500. Run with request.debug = 1 for more info"); + .startsWith("Failed to decode: Unrecognized token 'invalid'"); }); assertThat(result.getBids()).isEmpty(); } @@ -172,7 +172,7 @@ public void makeBidderResponseShouldReturnEmptyListIfBidResponseIsNull() { } @Test - public void makeBidderResponseShouldReturnErrorWhenResponseBodyIsWrong() throws JsonProcessingException { + public void makeBidderResponseShouldNotReturnErrorWhenResponseBodyIsEmpty() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); @@ -180,7 +180,7 @@ public void makeBidderResponseShouldReturnErrorWhenResponseBodyIsWrong() throws final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then - assertThat(result.getErrors()).isNotEmpty(); + assertThat(result.getErrors()).isEmpty(); assertThat(result.getBids()).isEmpty(); } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-request.json index 409c6e9133a..6fbe8ffd455 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-request.json @@ -3,6 +3,7 @@ "imp": [ { "id": "imp_id", + "bidfloorcur" : "USD", "banner": { "w": 300, "h": 250 From 46713fd66546e43c18c26ce58af6bbf8ae8d5889 Mon Sep 17 00:00:00 2001 From: vmalitskyi Date: Wed, 12 Mar 2025 21:01:00 +0100 Subject: [PATCH 5/7] Ogury adapter: Code cleanup --- .../server/bidder/ogury/OguryBidder.java | 59 +++++++------------ .../server/bidder/ogury/OguryBidderTest.java | 20 ++----- 2 files changed, 28 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java index dae8d7f4a02..91906d7643c 100644 --- a/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java +++ b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java @@ -11,7 +11,6 @@ import com.iab.openrtb.response.SeatBid; import io.vertx.core.MultiMap; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -29,6 +28,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -52,7 +52,6 @@ public OguryBidder(String endpointUrl, CurrencyConversionService currencyConvers @Override public Result>> makeHttpRequests(BidRequest bidRequest) { - final List> httpRequests = new ArrayList<>(); final List errors = new ArrayList<>(); final List modifiedImps = new ArrayList<>(); @@ -60,13 +59,10 @@ public Result>> makeHttpRequests(BidRequest bidRequ for (Imp imp : bidRequest.getImp()) { try { - final ObjectNode impExt = resolveImpExt(imp); - final ObjectNode impExtBidderHoist = resolveImpExtBidderHoist(impExt); + final Imp modifiedImp = modifyImp(imp, bidRequest); - final Imp modifiedImp = modifyImp(imp, bidRequest, impExtBidderHoist); modifiedImps.add(modifiedImp); - - if (hasOguryParams(impExtBidderHoist)) { + if (hasOguryParams(imp)) { impsWithOguryParams.add(modifiedImp); } } catch (PreBidException e) { @@ -74,8 +70,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ } } - final boolean isKeysValid = validateRequestKeys(bidRequest, impsWithOguryParams); - if (!isKeysValid) { + if (!isValidRequestKeys(bidRequest, impsWithOguryParams)) { errors.add(BidderError.badInput( "Invalid request. assetKey/adUnitId or request.site.publisher.id required")); return Result.withErrors(errors); @@ -86,28 +81,23 @@ public Result>> makeHttpRequests(BidRequest bidRequ .build(); final MultiMap headers = resolveHeaders(modifiedBidRequest.getDevice()); - httpRequests.add(BidderUtil.defaultRequest(modifiedBidRequest, headers, endpointUrl, mapper)); + final List> httpRequests = Collections.singletonList( + BidderUtil.defaultRequest(modifiedBidRequest, headers, endpointUrl, mapper)); return Result.of(httpRequests, errors); } - private ObjectNode resolveImpExt(Imp imp) { - return Optional.of(imp).map(Imp::getExt).orElse(null); - } - - private ObjectNode resolveImpExtBidderHoist(ObjectNode impExt) { - return (ObjectNode) Optional.ofNullable(impExt) - .map(ext -> ext.get(EXT_FIELD_BIDDER)) - .orElse(null); + private ObjectNode resolveImpExtBidderHoist(Imp imp) { + return (ObjectNode) imp.getExt().get(EXT_FIELD_BIDDER); } - private Imp modifyImp(Imp imp, BidRequest bidRequest, ObjectNode impExtBidderHoist) { + private Imp modifyImp(Imp imp, BidRequest bidRequest) { final Price price = resolvePrice(imp, bidRequest); return imp.toBuilder() .tagid(imp.getId()) .bidfloor(price.getValue()) .bidfloorcur(price.getCurrency()) - .ext(modifyExt(imp.getExt(), impExtBidderHoist)) + .ext(modifyExt(imp)) .build(); } @@ -128,42 +118,40 @@ private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) { return Price.of(BIDDER_CURRENCY, convertedPrice); } - private ObjectNode modifyExt(ObjectNode impExt, ObjectNode impExtBidderHoist) { - if (impExt == null || impExtBidderHoist == null) { - return impExt; - } + private ObjectNode modifyExt(Imp imp) { + final ObjectNode impExt = imp.getExt(); + final ObjectNode impExtBidderHoist = resolveImpExtBidderHoist(imp); final ObjectNode modifiedImpExt = impExt.deepCopy(); - impExtBidderHoist.fieldNames().forEachRemaining(field -> - modifiedImpExt.set(field, impExtBidderHoist.get(field))); + modifiedImpExt.setAll(impExtBidderHoist); modifiedImpExt.remove(EXT_FIELD_BIDDER); return modifiedImpExt; } - private boolean hasOguryParams(ObjectNode impExtBidderHoist) { + private boolean hasOguryParams(Imp imp) { + final ObjectNode impExtBidderHoist = resolveImpExtBidderHoist(imp); + return impExtBidderHoist != null && impExtBidderHoist.has(PREBID_FIELD_ASSET_KEY) && impExtBidderHoist.has(PREBID_FIELD_ADUNIT_ID); } - private boolean validateRequestKeys(BidRequest request, List impsWithOguryParams) { - return !CollectionUtils.isEmpty(impsWithOguryParams) && Optional.ofNullable(request.getSite()) + private boolean isValidRequestKeys(BidRequest request, List impsWithOguryParams) { + return !CollectionUtils.isEmpty(impsWithOguryParams) || Optional.ofNullable(request.getSite()) .map(Site::getPublisher) .map(Publisher::getId) - .isEmpty(); + .isPresent(); } private MultiMap resolveHeaders(Device device) { - final MultiMap headers = HttpUtil.headers().add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + final MultiMap headers = HttpUtil.headers(); if (device != null) { HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, device.getLanguage()); HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); - } else { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, "en-US"); } return headers; @@ -173,9 +161,6 @@ private MultiMap resolveHeaders(Device device) { public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final String body = httpCall.getResponse().getBody(); - if (StringUtils.isEmpty(body)) { - return Result.empty(); - } final BidResponse bidResponse = mapper.decodeValue(body, BidResponse.class); @@ -224,7 +209,7 @@ private static BidType getBidType(Bid bid) { case 3 -> BidType.audio; case 4 -> BidType.xNative; default -> throw new PreBidException( - "Unsupported MType '%d', for impression '%s'".formatted(bid.getMtype(), bid.getImpid())); + "Unsupported MType '%d', for impression '%s'".formatted(markupType, bid.getImpid())); }; } } diff --git a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java index 077c1842fa7..fbbee8abff3 100644 --- a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java @@ -93,7 +93,10 @@ public void makeHttpRequestsShouldEncodePassedBidRequest() { @Test public void makeHttpRequestsShouldReturnErrorWhenRequestDoesNotHaveOguryKeys() { // given - final BidRequest bidrequest = givenBidRequest(bidRequest -> bidRequest.imp(List.of(Imp.builder().build()))); + final ObjectNode bidder = mapper.createObjectNode(); + bidder.putIfAbsent("bidder", mapper.createObjectNode()); + final BidRequest bidrequest = givenBidRequest(bidRequest -> + bidRequest.imp(List.of(Imp.builder().ext(bidder).build()))); // when final Result>> result = target.makeHttpRequests(bidrequest); @@ -158,19 +161,6 @@ public void makeBidderResponseShouldReturnErrorIfResponseCodeIsNotOK() { assertThat(result.getBids()).isEmpty(); } - @Test - public void makeBidderResponseShouldReturnEmptyListIfBidResponseIsNull() { - // given - final BidderCall httpCall = givenHttpCall(null); - - // when - final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getBids()).isEmpty(); - } - @Test public void makeBidderResponseShouldNotReturnErrorWhenResponseBodyIsEmpty() throws JsonProcessingException { // given @@ -301,6 +291,7 @@ private BidRequest givenBidRequest() { .ua("ua") .ip("0.0.0.0") .ipv6("ip6") + .language("en-US") .build()) .imp(List.of(Imp.builder() .id("imp_id") @@ -319,6 +310,7 @@ private BidRequest givenModifiedBidRequest() { .ua("ua") .ip("0.0.0.0") .ipv6("ip6") + .language("en-US") .build()) .imp(List.of(Imp.builder() .id("imp_id") From 840a05bb49356e978e28c8797a3934bc47621eae Mon Sep 17 00:00:00 2001 From: vmalitskyi Date: Mon, 17 Mar 2025 16:34:46 +0100 Subject: [PATCH 6/7] Ogury adapter: update tests --- .../server/bidder/ogury/OguryBidderTest.java | 450 ++++++++++++++++-- 1 file changed, 398 insertions(+), 52 deletions(-) diff --git a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java index fbbee8abff3..83d8cf7c244 100644 --- a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java @@ -6,13 +6,12 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpMethod; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,13 +31,15 @@ import java.math.BigDecimal; import java.util.List; -import java.util.Set; import java.util.function.UnaryOperator; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -46,26 +47,74 @@ public class OguryBidderTest extends VertxTest { private static final String ENDPOINT_URL = "https://test.endpoint.com"; - @Mock + @Mock(strictness = LENIENT) private CurrencyConversionService currencyConversionService; private OguryBidder target; @BeforeEach public void setUp() { + when(currencyConversionService.convertCurrency(any(), any(), any(), any())).thenReturn(BigDecimal.ONE); target = new OguryBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); } @Test public void shouldFailOnInvalidEndpointUrl() { - Assertions.assertThatIllegalArgumentException() + assertThatIllegalArgumentException() .isThrownBy(() -> new OguryBidder("invalid_url", currencyConversionService, jacksonMapper)); } + @Test + public void makeHttpRequestsShouldPassUserAgentHeader() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(HttpUtil.USER_AGENT_HEADER)) + .isEqualTo("ua")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldPassAcceptLanguageHeader() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(HttpUtil.ACCEPT_LANGUAGE_HEADER)) + .isEqualTo("en-US")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldPassXForwardedForHeader() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.getAll(HttpUtil.X_FORWARDED_FOR_HEADER)) + .contains("0.0.0.0", "ip6")); + assertThat(result.getErrors()).isEmpty(); + } + @Test public void makeHttpRequestsShouldEncodePassedBidRequest() { // given - when(currencyConversionService.convertCurrency(any(), any(), any(), any())).thenReturn(BigDecimal.ONE); final BidRequest bidRequest = givenBidRequest(); final BidRequest modifiedBidRequest = givenModifiedBidRequest(); @@ -73,30 +122,17 @@ public void makeHttpRequestsShouldEncodePassedBidRequest() { final Result>> result = target.makeHttpRequests(bidRequest); // then - final MultiMap expectedHeaders = HttpUtil.headers() - .add(HttpUtil.USER_AGENT_HEADER, "ua") - .add(HttpUtil.ACCEPT_LANGUAGE_HEADER, "en-US") - .add(HttpUtil.X_FORWARDED_FOR_HEADER, "0.0.0.0") - .add(HttpUtil.X_FORWARDED_FOR_HEADER, "ip6"); - final Result>> expectedResult = Result.withValue(HttpRequest.builder() - .method(HttpMethod.POST) - .uri(ENDPOINT_URL) - .headers(expectedHeaders) - .impIds(Set.of("imp_id")) - .body(jacksonMapper.encodeToBytes(modifiedBidRequest)) - .payload(modifiedBidRequest) - .build()); - assertThat(result.getValue()).usingRecursiveComparison().isEqualTo(expectedResult.getValue()); + assertThat(result.getValue().getFirst()).extracting(HttpRequest::getBody) + .isEqualTo(jacksonMapper.encodeToBytes(modifiedBidRequest)); assertThat(result.getErrors()).isEmpty(); } @Test public void makeHttpRequestsShouldReturnErrorWhenRequestDoesNotHaveOguryKeys() { // given - final ObjectNode bidder = mapper.createObjectNode(); - bidder.putIfAbsent("bidder", mapper.createObjectNode()); + final ObjectNode ext = givenExtWithEmptyBidder(); final BidRequest bidrequest = givenBidRequest(bidRequest -> - bidRequest.imp(List.of(Imp.builder().ext(bidder).build()))); + bidRequest.imp(List.of(Imp.builder().ext(ext).build()))); // when final Result>> result = target.makeHttpRequests(bidrequest); @@ -108,45 +144,288 @@ public void makeHttpRequestsShouldReturnErrorWhenRequestDoesNotHaveOguryKeys() { } @Test - public void makeBidderResponseShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + public void makeHttpRequestsShouldSendOnlyImpsWithOguryParamsIfPresent() { // given - final BidderCall httpCall = givenHttpCall("invalid"); + final ObjectNode ext = givenExtWithEmptyBidder(); + final ObjectNode extWithOguryKeyas = givenExtWithBidderWithOguryKeys(); + final Site site = givenSite(); + + final BidRequest bidrequest = givenBidRequest(bidRequest -> + bidRequest.site(site) + .imp(List.of( + Imp.builder() + .id("without_ogury_keys") + .ext(ext) + .build(), + Imp.builder() + .id("with_ogury_keys") + .ext(extWithOguryKeyas) + .build()))); // when - final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + final Result>> result = target.makeHttpRequests(bidrequest); // then - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> { - assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); - assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .hasSize(1) + .allSatisfy(imp -> assertThat(imp.getId()).isEqualTo("with_ogury_keys")); + + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldSendAllImpsWhenHasPublisherIdAndImpsWithOguryIsEmpty() { + // given + final ObjectNode ext = givenExtWithEmptyBidder(); + final Site site = givenSite(); + + final BidRequest bidrequest = givenBidRequest(bidRequest -> + bidRequest.site(site) + .imp(List.of( + Imp.builder() + .id("id1") + .ext(ext) + .build(), + Imp.builder() + .id("id2") + .ext(ext) + .build()))); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .hasSize(2); + + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldNotSendImpsWhenHasNotPublisherIdAndImpsWithOguryIsEmpty() { + // given + final ObjectNode ext = givenExtWithEmptyBidder(); + + final BidRequest bidrequest = givenBidRequest(bidRequest -> + bidRequest.site(Site.builder().build()) + .imp(List.of( + Imp.builder() + .id("id1") + .ext(ext) + .build(), + Imp.builder() + .id("id2") + .ext(ext) + .build()))); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getValue()).isEmpty(); + + assertThat(result.getErrors()).isNotEmpty() + .contains(BidderError.badInput( + "Invalid request. assetKey/adUnitId or request.site.publisher.id required")); + } + + @Test + public void makeHttpRequestsShouldCopyImpIdToTagId() { + // given + final ObjectNode ext = givenExtWithEmptyBidder(); + final Site site = givenSite(); + + final BidRequest bidrequest = givenBidRequest(bidRequest -> + bidRequest.site(site) + .imp(List.of( + Imp.builder() + .id("id1") + .ext(ext) + .build()))); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .flatExtracting(Imp::getTagid) + .hasSize(1) + .first() + .isEqualTo("id1"); + + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldCleanImpExtWithoutLostExtraFields() { + // given + final ObjectNode extWithOguryKeys = givenExtWithBidderWithOguryKeys(); + extWithOguryKeys.putIfAbsent("extra_field", TextNode.valueOf("extra_value")); + + final BidRequest bidrequest = givenBidRequest(bidRequest -> + bidRequest.imp(List.of( + Imp.builder() + .id("id1") + .ext(extWithOguryKeys) + .build()))); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .hasSize(1) + .first() + .satisfies(ext -> { + assertThat(ext.get("extra_field").asText()).isEqualTo("extra_value"); + assertThat(ext.get("prebid")).isNull(); }); - assertThat(result.getBids()).isEmpty(); + + assertThat(result.getErrors()).isEmpty(); } @Test - public void makeBidderResponseShouldReturnErrorIfResponseCodeIsBadRequest() { + public void makeHttpRequestsShouldConvertPriceIfCurrencyIsDifferentFromUSD() { // given - final BidderCall httpCall = givenHttpCall(HttpResponseStatus.BAD_REQUEST.code(), "invalid"); + final ObjectNode ext = givenExtWithEmptyBidder(); + final Site site = givenSite(); + + final BidRequest bidrequest = givenBidRequest(bidRequest -> + bidRequest.site(site) + .imp(List.of( + Imp.builder() + .id("id1") + .bidfloorcur("CA") + .ext(ext) + .bidfloor(BigDecimal.valueOf(1.5)) + .build()))); // when - final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + final Result>> result = target.makeHttpRequests(bidrequest); // then - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> { - assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); - assertThat(error.getMessage()) - .startsWith("Failed to decode: Unrecognized token 'invalid'"); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .hasSize(1) + .first() + .satisfies(imp -> { + assertThat(imp.getBidfloorcur()).isEqualTo("USD"); + assertThat(imp.getBidfloor()).isEqualTo(BigDecimal.valueOf(1)); }); - assertThat(result.getBids()).isEmpty(); + + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldNotConvertPriceIfCurrencyIsUSD() { + // given + final ObjectNode ext = givenExtWithEmptyBidder(); + final Site site = givenSite(); + + final BidRequest bidrequest = givenBidRequest(bidRequest -> + bidRequest.site(site) + .imp(List.of( + Imp.builder() + .id("id1") + .bidfloorcur("USD") + .ext(ext) + .bidfloor(BigDecimal.valueOf(1.5)) + .build()))); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .hasSize(1) + .first() + .satisfies(imp -> { + assertThat(imp.getBidfloorcur()).isEqualTo("USD"); + assertThat(imp.getBidfloor()).isEqualTo(BigDecimal.valueOf(1.5)); + }); + + assertThat(result.getErrors()).isEmpty(); } @Test - public void makeBidderResponseShouldReturnErrorIfResponseCodeIsNotOK() { + public void makeHttpRequestsShouldNotConvertPriceIfCurrencyIsAbsent() { // given - final BidderCall httpCall = givenHttpCall(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), - "invalid"); + final ObjectNode ext = givenExtWithEmptyBidder(); + final Site site = givenSite(); + + final BidRequest bidrequest = givenBidRequest(bidRequest -> + bidRequest.site(site) + .imp(List.of( + Imp.builder() + .id("id1") + .ext(ext) + .bidfloor(BigDecimal.valueOf(1.5)) + .build()))); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .hasSize(1) + .first() + .satisfies(imp -> { + assertThat(imp.getBidfloorcur()).isNull(); + assertThat(imp.getBidfloor()).isEqualTo(BigDecimal.valueOf(1.5)); + }); + + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldNotConvertPriceIfFloorIsAbsent() { + // given + final ObjectNode ext = givenExtWithEmptyBidder(); + final Site site = givenSite(); + + final BidRequest bidrequest = givenBidRequest(bidRequest -> + bidRequest.site(site) + .imp(List.of( + Imp.builder() + .id("id1") + .ext(ext) + .build()))); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .hasSize(1) + .first() + .satisfies(imp -> { + assertThat(imp.getBidfloorcur()).isNull(); + assertThat(imp.getBidfloor()).isNull(); + }); + verifyNoInteractions(currencyConversionService); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidderResponseShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); // when final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); @@ -155,8 +434,7 @@ public void makeBidderResponseShouldReturnErrorIfResponseCodeIsNotOK() { assertThat(result.getErrors()).hasSize(1) .allSatisfy(error -> { assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); - assertThat(error.getMessage()) - .startsWith("Failed to decode: Unrecognized token 'invalid'"); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); }); assertThat(result.getBids()).isEmpty(); } @@ -247,6 +525,54 @@ public void makeBidderResponseShouldReturnBannerBid() throws JsonProcessingExcep .containsExactly(BidType.banner); } + @Test + public void makeBidderResponseShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bid -> bid.mtype(2))); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()) + .extracting(BidderBid::getType) + .containsExactly(BidType.video); + } + + @Test + public void makeBidderResponseShouldReturnAudioBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bid -> bid.mtype(3))); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()) + .extracting(BidderBid::getType) + .containsExactly(BidType.audio); + } + + @Test + public void makeBidderResponseShouldReturnNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bid -> bid.mtype(4))); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()) + .extracting(BidderBid::getType) + .containsExactly(BidType.xNative); + } + @Test public void makeBidderResponseShouldReturnBidWithCurFromResponse() throws JsonProcessingException { // given @@ -281,12 +607,7 @@ private BidRequest givenBidRequest(UnaryOperator b } private BidRequest givenBidRequest() { - final ObjectNode ogury = mapper.createObjectNode(); - ogury.putIfAbsent("adUnitId", TextNode.valueOf("1")); - ogury.putIfAbsent("assetKey", TextNode.valueOf("key")); - final ObjectNode bidder = mapper.createObjectNode(); - bidder.putIfAbsent("bidder", ogury); - + final ObjectNode bidder = givenExtWithBidderWithOguryKeys(); return givenBidRequest(bidRequest -> bidRequest.device(Device.builder() .ua("ua") .ip("0.0.0.0") @@ -322,6 +643,31 @@ private BidRequest givenModifiedBidRequest() { .build()))); } + private ObjectNode givenExtWithEmptyBidder() { + final ObjectNode bidder = mapper.createObjectNode(); + bidder.putIfAbsent("bidder", mapper.createObjectNode()); + + return bidder; + } + + private Site givenSite() { + return Site.builder() + .publisher(Publisher.builder() + .id("publiser_id") + .build()) + .build(); + } + + private ObjectNode givenExtWithBidderWithOguryKeys() { + final ObjectNode ogury = mapper.createObjectNode(); + ogury.putIfAbsent("adUnitId", TextNode.valueOf("1")); + ogury.putIfAbsent("assetKey", TextNode.valueOf("key")); + final ObjectNode bidder = mapper.createObjectNode(); + bidder.putIfAbsent("bidder", ogury); + + return bidder; + } + private static BidderCall givenHttpCall(String body) { return givenHttpCall(HttpResponseStatus.OK.code(), body); } From a0aca3744bc3a266175d7afac65e16f3bcf58b27 Mon Sep 17 00:00:00 2001 From: vmalitskyi Date: Wed, 19 Mar 2025 12:42:49 +0100 Subject: [PATCH 7/7] Ogury adapter: tests cleanup --- .../server/bidder/ogury/OguryBidderTest.java | 299 ++++++------------ 1 file changed, 104 insertions(+), 195 deletions(-) diff --git a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java index 83d8cf7c244..8222df6ab01 100644 --- a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; @@ -30,11 +29,14 @@ import org.prebid.server.util.HttpUtil; import java.math.BigDecimal; +import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.function.UnaryOperator; import static java.util.Collections.emptyList; 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.mockito.ArgumentMatchers.any; @@ -108,69 +110,42 @@ public void makeHttpRequestsShouldPassXForwardedForHeader() { assertThat(result.getValue()).hasSize(1).first() .extracting(HttpRequest::getHeaders) .satisfies(headers -> assertThat(headers.getAll(HttpUtil.X_FORWARDED_FOR_HEADER)) - .contains("0.0.0.0", "ip6")); - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void makeHttpRequestsShouldEncodePassedBidRequest() { - // given - final BidRequest bidRequest = givenBidRequest(); - final BidRequest modifiedBidRequest = givenModifiedBidRequest(); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue().getFirst()).extracting(HttpRequest::getBody) - .isEqualTo(jacksonMapper.encodeToBytes(modifiedBidRequest)); + .containsExactlyInAnyOrder("0.0.0.0", "ip6")); assertThat(result.getErrors()).isEmpty(); } @Test public void makeHttpRequestsShouldReturnErrorWhenRequestDoesNotHaveOguryKeys() { // given - final ObjectNode ext = givenExtWithEmptyBidder(); - final BidRequest bidrequest = givenBidRequest(bidRequest -> - bidRequest.imp(List.of(Imp.builder().ext(ext).build()))); + final BidRequest bidrequest = givenBidRequest( + identity(), + givenImp(imp -> imp.ext(givenEmptyImpExt()))); // when final Result>> result = target.makeHttpRequests(bidrequest); // then - assertThat(result.getErrors()).isNotEmpty() - .contains(BidderError.badInput( - "Invalid request. assetKey/adUnitId or request.site.publisher.id required")); + assertThat(result.getErrors()).containsExactly( + BidderError.badInput("Invalid request. assetKey/adUnitId or request.site.publisher.id required")); } @Test public void makeHttpRequestsShouldSendOnlyImpsWithOguryParamsIfPresent() { // given - final ObjectNode ext = givenExtWithEmptyBidder(); - final ObjectNode extWithOguryKeyas = givenExtWithBidderWithOguryKeys(); - final Site site = givenSite(); - - final BidRequest bidrequest = givenBidRequest(bidRequest -> - bidRequest.site(site) - .imp(List.of( - Imp.builder() - .id("without_ogury_keys") - .ext(ext) - .build(), - Imp.builder() - .id("with_ogury_keys") - .ext(extWithOguryKeyas) - .build()))); + final BidRequest bidrequest = givenBidRequest( + bidRequest -> bidRequest.site(givenSite()), + givenImp(imp -> imp.id("without_ogury_keys").ext(givenEmptyImpExt())), + givenImp(imp -> imp.id("with_ogury_keys").ext(givenImpExtWithOguryKeys()))); // when final Result>> result = target.makeHttpRequests(bidrequest); // then assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) - .hasSize(1) - .allSatisfy(imp -> assertThat(imp.getId()).isEqualTo("with_ogury_keys")); + .extracting(Imp::getId) + .containsExactly("with_ogury_keys"); assertThat(result.getErrors()).isEmpty(); } @@ -178,27 +153,18 @@ public void makeHttpRequestsShouldSendOnlyImpsWithOguryParamsIfPresent() { @Test public void makeHttpRequestsShouldSendAllImpsWhenHasPublisherIdAndImpsWithOguryIsEmpty() { // given - final ObjectNode ext = givenExtWithEmptyBidder(); - final Site site = givenSite(); - - final BidRequest bidrequest = givenBidRequest(bidRequest -> - bidRequest.site(site) - .imp(List.of( - Imp.builder() - .id("id1") - .ext(ext) - .build(), - Imp.builder() - .id("id2") - .ext(ext) - .build()))); + final ObjectNode emptyImpExt = givenEmptyImpExt(); + final BidRequest bidrequest = givenBidRequest( + bidRequest -> bidRequest.site(givenSite()), + givenImp(imp -> imp.id("id1").ext(emptyImpExt)), + givenImp(imp -> imp.id("id2").ext(emptyImpExt))); // when final Result>> result = target.makeHttpRequests(bidrequest); // then assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .hasSize(2); @@ -208,19 +174,12 @@ public void makeHttpRequestsShouldSendAllImpsWhenHasPublisherIdAndImpsWithOguryI @Test public void makeHttpRequestsShouldNotSendImpsWhenHasNotPublisherIdAndImpsWithOguryIsEmpty() { // given - final ObjectNode ext = givenExtWithEmptyBidder(); - - final BidRequest bidrequest = givenBidRequest(bidRequest -> - bidRequest.site(Site.builder().build()) - .imp(List.of( - Imp.builder() - .id("id1") - .ext(ext) - .build(), - Imp.builder() - .id("id2") - .ext(ext) - .build()))); + final ObjectNode emptyImpExt = givenEmptyImpExt(); + + final BidRequest bidrequest = givenBidRequest( + bidRequest -> bidRequest.site(Site.builder().build()), + givenImp(imp -> imp.id("id1").ext(emptyImpExt)), + givenImp(imp -> imp.id("id2").ext(emptyImpExt))); // when final Result>> result = target.makeHttpRequests(bidrequest); @@ -228,36 +187,26 @@ public void makeHttpRequestsShouldNotSendImpsWhenHasNotPublisherIdAndImpsWithOgu // then assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).isNotEmpty() - .contains(BidderError.badInput( - "Invalid request. assetKey/adUnitId or request.site.publisher.id required")); + assertThat(result.getErrors()).containsExactly( + BidderError.badInput("Invalid request. assetKey/adUnitId or request.site.publisher.id required")); } @Test public void makeHttpRequestsShouldCopyImpIdToTagId() { // given - final ObjectNode ext = givenExtWithEmptyBidder(); - final Site site = givenSite(); - - final BidRequest bidrequest = givenBidRequest(bidRequest -> - bidRequest.site(site) - .imp(List.of( - Imp.builder() - .id("id1") - .ext(ext) - .build()))); + final BidRequest bidrequest = givenBidRequest( + bidRequest -> bidRequest.site(givenSite()), + givenImp(imp -> imp.id("id1").ext(givenEmptyImpExt()))); // when final Result>> result = target.makeHttpRequests(bidrequest); // then assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .flatExtracting(Imp::getTagid) - .hasSize(1) - .first() - .isEqualTo("id1"); + .containsExactly("id1"); assertThat(result.getErrors()).isEmpty(); } @@ -265,56 +214,46 @@ public void makeHttpRequestsShouldCopyImpIdToTagId() { @Test public void makeHttpRequestsShouldCleanImpExtWithoutLostExtraFields() { // given - final ObjectNode extWithOguryKeys = givenExtWithBidderWithOguryKeys(); - extWithOguryKeys.putIfAbsent("extra_field", TextNode.valueOf("extra_value")); + final ObjectNode extWithOguryKeys = givenImpExtWithOguryKeys(); + extWithOguryKeys.put("extra_field", "extra_value"); - final BidRequest bidrequest = givenBidRequest(bidRequest -> - bidRequest.imp(List.of( - Imp.builder() - .id("id1") - .ext(extWithOguryKeys) - .build()))); + final BidRequest bidrequest = givenBidRequest( + identity(), + givenImp(imp -> imp.id("id1").ext(extWithOguryKeys))); // when final Result>> result = target.makeHttpRequests(bidrequest); // then assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .extracting(Imp::getExt) .hasSize(1) .first() - .satisfies(ext -> { - assertThat(ext.get("extra_field").asText()).isEqualTo("extra_value"); - assertThat(ext.get("prebid")).isNull(); - }); - + .isEqualTo(mapper.valueToTree(Map.of( + "extra_field", "extra_value", + "adUnitId", "1", + "assetKey", "key"))); assertThat(result.getErrors()).isEmpty(); } @Test public void makeHttpRequestsShouldConvertPriceIfCurrencyIsDifferentFromUSD() { // given - final ObjectNode ext = givenExtWithEmptyBidder(); - final Site site = givenSite(); - - final BidRequest bidrequest = givenBidRequest(bidRequest -> - bidRequest.site(site) - .imp(List.of( - Imp.builder() - .id("id1") - .bidfloorcur("CA") - .ext(ext) - .bidfloor(BigDecimal.valueOf(1.5)) - .build()))); + final BidRequest bidrequest = givenBidRequest( + bidRequest -> bidRequest.site(givenSite()), + givenImp(imp -> imp.id("id1") + .bidfloorcur("CA") + .bidfloor(BigDecimal.valueOf(1.5)) + .ext(givenEmptyImpExt()))); // when final Result>> result = target.makeHttpRequests(bidrequest); // then assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .hasSize(1) .first() @@ -329,25 +268,19 @@ public void makeHttpRequestsShouldConvertPriceIfCurrencyIsDifferentFromUSD() { @Test public void makeHttpRequestsShouldNotConvertPriceIfCurrencyIsUSD() { // given - final ObjectNode ext = givenExtWithEmptyBidder(); - final Site site = givenSite(); - - final BidRequest bidrequest = givenBidRequest(bidRequest -> - bidRequest.site(site) - .imp(List.of( - Imp.builder() - .id("id1") - .bidfloorcur("USD") - .ext(ext) - .bidfloor(BigDecimal.valueOf(1.5)) - .build()))); + final BidRequest bidrequest = givenBidRequest( + bidRequest -> bidRequest.site(givenSite()), + givenImp(imp -> imp.id("id1") + .bidfloorcur("USD") + .bidfloor(BigDecimal.valueOf(1.5)) + .ext(givenEmptyImpExt()))); // when final Result>> result = target.makeHttpRequests(bidrequest); // then assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .hasSize(1) .first() @@ -362,24 +295,18 @@ public void makeHttpRequestsShouldNotConvertPriceIfCurrencyIsUSD() { @Test public void makeHttpRequestsShouldNotConvertPriceIfCurrencyIsAbsent() { // given - final ObjectNode ext = givenExtWithEmptyBidder(); - final Site site = givenSite(); - - final BidRequest bidrequest = givenBidRequest(bidRequest -> - bidRequest.site(site) - .imp(List.of( - Imp.builder() - .id("id1") - .ext(ext) - .bidfloor(BigDecimal.valueOf(1.5)) - .build()))); + final BidRequest bidrequest = givenBidRequest( + bidRequest -> bidRequest.site(givenSite()), + givenImp(imp -> imp.id("id1") + .bidfloor(BigDecimal.valueOf(1.5)) + .ext(givenEmptyImpExt()))); // when final Result>> result = target.makeHttpRequests(bidrequest); // then assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .hasSize(1) .first() @@ -394,23 +321,16 @@ public void makeHttpRequestsShouldNotConvertPriceIfCurrencyIsAbsent() { @Test public void makeHttpRequestsShouldNotConvertPriceIfFloorIsAbsent() { // given - final ObjectNode ext = givenExtWithEmptyBidder(); - final Site site = givenSite(); - - final BidRequest bidrequest = givenBidRequest(bidRequest -> - bidRequest.site(site) - .imp(List.of( - Imp.builder() - .id("id1") - .ext(ext) - .build()))); + final BidRequest bidrequest = givenBidRequest( + bidRequest -> bidRequest.site(givenSite()), + givenImp(imp -> imp.id("id1").ext(givenEmptyImpExt()))); // when final Result>> result = target.makeHttpRequests(bidrequest); // then assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .hasSize(1) .first() @@ -600,54 +520,40 @@ private static String givenBidResponse(UnaryOperator bidCustomiz .build()); } - private BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { - return bidRequestCustomizer.apply(BidRequest.builder() - .id("id")) - .build(); + private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer, + Imp... imps) { + + final BidRequest.BidRequestBuilder bidRequestBuilder = BidRequest.builder() + .id("id"); + + if (imps != null) { + final List impressions = Arrays.stream(imps).toList(); + bidRequestBuilder.imp(impressions); + } + + return bidRequestCustomizer.apply(bidRequestBuilder).build(); } private BidRequest givenBidRequest() { - final ObjectNode bidder = givenExtWithBidderWithOguryKeys(); - return givenBidRequest(bidRequest -> bidRequest.device(Device.builder() + return givenBidRequest( + bidRequest -> bidRequest.device(Device.builder() .ua("ua") .ip("0.0.0.0") .ipv6("ip6") .language("en-US") - .build()) - .imp(List.of(Imp.builder() + .build()), + givenImp(imp -> imp .id("imp_id") .bidfloor(BigDecimal.TWO) .bidfloorcur("CAD") - .ext(bidder) - .build()))); - } + .ext(givenImpExtWithOguryKeys()))); - private BidRequest givenModifiedBidRequest() { - final ObjectNode oguryKeys = mapper.createObjectNode(); - oguryKeys.putIfAbsent("adUnitId", TextNode.valueOf("1")); - oguryKeys.putIfAbsent("assetKey", TextNode.valueOf("key")); - - return givenBidRequest(bidRequest -> bidRequest.device(Device.builder() - .ua("ua") - .ip("0.0.0.0") - .ipv6("ip6") - .language("en-US") - .build()) - .imp(List.of(Imp.builder() - .id("imp_id") - .bidfloor(BigDecimal.ONE) - .bidfloorcur("USD") - .tagid("imp_id") - .ext(oguryKeys) - .bidfloorcur("USD") - .build()))); } - private ObjectNode givenExtWithEmptyBidder() { - final ObjectNode bidder = mapper.createObjectNode(); - bidder.putIfAbsent("bidder", mapper.createObjectNode()); - - return bidder; + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer + .apply(Imp.builder()) + .build(); } private Site givenSite() { @@ -658,24 +564,27 @@ private Site givenSite() { .build(); } - private ObjectNode givenExtWithBidderWithOguryKeys() { - final ObjectNode ogury = mapper.createObjectNode(); - ogury.putIfAbsent("adUnitId", TextNode.valueOf("1")); - ogury.putIfAbsent("assetKey", TextNode.valueOf("key")); - final ObjectNode bidder = mapper.createObjectNode(); - bidder.putIfAbsent("bidder", ogury); + private ObjectNode givenEmptyImpExt() { + final ObjectNode ext = mapper.createObjectNode(); + ext.putIfAbsent("bidder", mapper.createObjectNode()); - return bidder; + return ext; } - private static BidderCall givenHttpCall(String body) { - return givenHttpCall(HttpResponseStatus.OK.code(), body); + private ObjectNode givenImpExtWithOguryKeys() { + final ObjectNode ext = mapper.createObjectNode(); + final ObjectNode ogury = ext.putObject("bidder"); + + ogury.put("adUnitId", "1"); + ogury.put("assetKey", "key"); + + return ext; } - private static BidderCall givenHttpCall(int statusCode, String body) { + private static BidderCall givenHttpCall(String body) { return BidderCall.succeededHttp( HttpRequest.builder().payload(null).build(), - HttpResponse.of(statusCode, null, body), + HttpResponse.of(HttpResponseStatus.OK.code(), null, body), null); } }