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..91906d7643c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java @@ -0,0 +1,215 @@ +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.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +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.Price; +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.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +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 bidRequest) { + final List errors = new ArrayList<>(); + + final List modifiedImps = new ArrayList<>(); + final List impsWithOguryParams = new ArrayList<>(); + + for (Imp imp : bidRequest.getImp()) { + try { + final Imp modifiedImp = modifyImp(imp, bidRequest); + + modifiedImps.add(modifiedImp); + if (hasOguryParams(imp)) { + impsWithOguryParams.add(modifiedImp); + } + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (!isValidRequestKeys(bidRequest, impsWithOguryParams)) { + errors.add(BidderError.badInput( + "Invalid request. assetKey/adUnitId or request.site.publisher.id required")); + return Result.withErrors(errors); + } + + final BidRequest modifiedBidRequest = bidRequest.toBuilder() + .imp(CollectionUtils.isNotEmpty(impsWithOguryParams) ? impsWithOguryParams : modifiedImps) + .build(); + + final MultiMap headers = resolveHeaders(modifiedBidRequest.getDevice()); + final List> httpRequests = Collections.singletonList( + BidderUtil.defaultRequest(modifiedBidRequest, headers, endpointUrl, mapper)); + + return Result.of(httpRequests, errors); + } + + private ObjectNode resolveImpExtBidderHoist(Imp imp) { + return (ObjectNode) imp.getExt().get(EXT_FIELD_BIDDER); + } + + 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)) + .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(Imp imp) { + final ObjectNode impExt = imp.getExt(); + final ObjectNode impExtBidderHoist = resolveImpExtBidderHoist(imp); + + final ObjectNode modifiedImpExt = impExt.deepCopy(); + modifiedImpExt.setAll(impExtBidderHoist); + modifiedImpExt.remove(EXT_FIELD_BIDDER); + + return modifiedImpExt; + } + + 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 isValidRequestKeys(BidRequest request, List impsWithOguryParams) { + return !CollectionUtils.isEmpty(impsWithOguryParams) || Optional.ofNullable(request.getSite()) + .map(Site::getPublisher) + .map(Publisher::getId) + .isPresent(); + } + + private MultiMap resolveHeaders(Device device) { + 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()); + } + + return headers; + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final String body = httpCall.getResponse().getBody(); + + final BidResponse bidResponse = mapper.decodeValue(body, BidResponse.class); + + final List errors = new ArrayList<>(); + final List bidderBids = extractBids(bidResponse, errors); + + return Result.of(bidderBids, errors); + } catch (Exception e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + 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 -> createBidderBid(bid, bidResponse, errors)) + .filter(Objects::nonNull) + .toList(); + } + + 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; + } + } + + 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(markupType, bid.getImpid())); + }; + } +} 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..32ccccac07e --- /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={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&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..8222df6ab01 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java @@ -0,0 +1,590 @@ +package org.prebid.server.bidder.ogury; + +import com.fasterxml.jackson.core.JsonProcessingException; +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 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.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; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verifyNoInteractions; +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(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() { + 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)) + .containsExactlyInAnyOrder("0.0.0.0", "ip6")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenRequestDoesNotHaveOguryKeys() { + // given + final BidRequest bidrequest = givenBidRequest( + identity(), + givenImp(imp -> imp.ext(givenEmptyImpExt()))); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getErrors()).containsExactly( + BidderError.badInput("Invalid request. assetKey/adUnitId or request.site.publisher.id required")); + } + + @Test + public void makeHttpRequestsShouldSendOnlyImpsWithOguryParamsIfPresent() { + // given + 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::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("with_ogury_keys"); + + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldSendAllImpsWhenHasPublisherIdAndImpsWithOguryIsEmpty() { + // given + 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::getPayload) + .flatExtracting(BidRequest::getImp) + .hasSize(2); + + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldNotSendImpsWhenHasNotPublisherIdAndImpsWithOguryIsEmpty() { + // given + 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); + + // then + assertThat(result.getValue()).isEmpty(); + + assertThat(result.getErrors()).containsExactly( + BidderError.badInput("Invalid request. assetKey/adUnitId or request.site.publisher.id required")); + } + + @Test + public void makeHttpRequestsShouldCopyImpIdToTagId() { + // given + 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::getPayload) + .flatExtracting(BidRequest::getImp) + .flatExtracting(Imp::getTagid) + .containsExactly("id1"); + + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldCleanImpExtWithoutLostExtraFields() { + // given + final ObjectNode extWithOguryKeys = givenImpExtWithOguryKeys(); + extWithOguryKeys.put("extra_field", "extra_value"); + + 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::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .hasSize(1) + .first() + .isEqualTo(mapper.valueToTree(Map.of( + "extra_field", "extra_value", + "adUnitId", "1", + "assetKey", "key"))); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldConvertPriceIfCurrencyIsDifferentFromUSD() { + // given + 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::getPayload) + .flatExtracting(BidRequest::getImp) + .hasSize(1) + .first() + .satisfies(imp -> { + assertThat(imp.getBidfloorcur()).isEqualTo("USD"); + assertThat(imp.getBidfloor()).isEqualTo(BigDecimal.valueOf(1)); + }); + + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldNotConvertPriceIfCurrencyIsUSD() { + // given + 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::getPayload) + .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 makeHttpRequestsShouldNotConvertPriceIfCurrencyIsAbsent() { + // given + 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::getPayload) + .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 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::getPayload) + .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); + + // 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 makeBidderResponseShouldNotReturnErrorWhenResponseBodyIsEmpty() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + 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 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 + 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 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() { + return givenBidRequest( + bidRequest -> bidRequest.device(Device.builder() + .ua("ua") + .ip("0.0.0.0") + .ipv6("ip6") + .language("en-US") + .build()), + givenImp(imp -> imp + .id("imp_id") + .bidfloor(BigDecimal.TWO) + .bidfloorcur("CAD") + .ext(givenImpExtWithOguryKeys()))); + + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer + .apply(Imp.builder()) + .build(); + } + + private Site givenSite() { + return Site.builder() + .publisher(Publisher.builder() + .id("publiser_id") + .build()) + .build(); + } + + private ObjectNode givenEmptyImpExt() { + final ObjectNode ext = mapper.createObjectNode(); + ext.putIfAbsent("bidder", mapper.createObjectNode()); + + return ext; + } + + 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(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(HttpResponseStatus.OK.code(), 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..6fbe8ffd455 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ogury/test-auction-ogury-request.json @@ -0,0 +1,29 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "bidfloorcur" : "USD", + "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