From c6c4153f75f2b64750a0dab5f41efcaa37f9eb07 Mon Sep 17 00:00:00 2001 From: Markiyan Mykush <95693607+marki1an@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:27:37 +0300 Subject: [PATCH 1/9] Tests: Fix startup for `localstack` container (#3436) --- .../functional/testcontainers/Dependencies.groovy | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy index ef2575ea3ed..70c99a2a833 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy @@ -10,6 +10,7 @@ import org.testcontainers.lifecycle.Startables import org.testcontainers.utility.DockerImageName import static org.prebid.server.functional.util.SystemProperties.MOCKSERVER_VERSION +import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3 class Dependencies { @@ -36,21 +37,21 @@ class Dependencies { static final NetworkServiceContainer networkServiceContainer = new NetworkServiceContainer(MOCKSERVER_VERSION) .withNetwork(network) - static final LocalStackContainer localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:s3-latest")) - .withNetwork(Dependencies.network) - .withServices(LocalStackContainer.Service.S3) + static LocalStackContainer localStackContainer static void start() { if (IS_LAUNCH_CONTAINERS) { - Startables.deepStart([networkServiceContainer, mysqlContainer, localStackContainer]) - .join() + localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:s3-latest")) + .withNetwork(network) + .withServices(S3) + Startables.deepStart([networkServiceContainer, mysqlContainer, localStackContainer]).join() } } static void stop() { if (IS_LAUNCH_CONTAINERS) { [networkServiceContainer, mysqlContainer, localStackContainer].parallelStream() - .forEach({ it.stop() }) + .forEach({ it.stop() }) } } From bfe348b810f82771f82057f21a30bb5280df2782 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:30:05 +0200 Subject: [PATCH 2/9] Bizzclick: Rename to Blasto (#3435) --- .../BlastoBidder.java} | 37 +++++++--------- .../ExtImpBlasto.java} | 4 +- ...guration.java => BlastoConfiguration.java} | 22 +++++----- .../resources/bidder-config/bizzclick.yaml | 15 ------- src/main/resources/bidder-config/blasto.yaml | 22 ++++++++++ .../{bizzclick.json => blasto.json} | 10 ++--- .../BlastoBidderTest.java} | 44 ++++--------------- .../{BizzclickTest.java => BlastoTest.java} | 19 ++++---- .../test-auction-blasto-request.json} | 5 +-- .../test-auction-blasto-response.json} | 4 +- .../test-blasto-bid-request.json} | 0 .../test-blasto-bid-response.json} | 0 .../server/it/test-application.properties | 4 +- 13 files changed, 80 insertions(+), 106 deletions(-) rename src/main/java/org/prebid/server/bidder/{bizzclick/BizzclickBidder.java => blasto/BlastoBidder.java} (80%) rename src/main/java/org/prebid/server/proto/openrtb/ext/request/{bizzclick/ExtImpBizzclick.java => blasto/ExtImpBlasto.java} (77%) rename src/main/java/org/prebid/server/spring/config/bidder/{BizzclickConfiguration.java => BlastoConfiguration.java} (59%) delete mode 100644 src/main/resources/bidder-config/bizzclick.yaml create mode 100644 src/main/resources/bidder-config/blasto.yaml rename src/main/resources/static/bidder-params/{bizzclick.json => blasto.json} (72%) rename src/test/java/org/prebid/server/bidder/{bizzclick/BizzclickBidderTest.java => blasto/BlastoBidderTest.java} (91%) rename src/test/java/org/prebid/server/it/{BizzclickTest.java => BlastoTest.java} (57%) rename src/test/resources/org/prebid/server/it/openrtb2/{bizzclick/test-auction-bizzclick-request.json => blasto/test-auction-blasto-request.json} (75%) rename src/test/resources/org/prebid/server/it/openrtb2/{bizzclick/test-auction-bizzclick-response.json => blasto/test-auction-blasto-response.json} (89%) rename src/test/resources/org/prebid/server/it/openrtb2/{bizzclick/test-bizzclick-bid-request.json => blasto/test-blasto-bid-request.json} (100%) rename src/test/resources/org/prebid/server/it/openrtb2/{bizzclick/test-bizzclick-bid-response.json => blasto/test-blasto-bid-response.json} (100%) diff --git a/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java b/src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java similarity index 80% rename from src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java rename to src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java index 2fc8185e301..c317935419b 100644 --- a/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java +++ b/src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.bizzclick; +package org.prebid.server.bidder.blasto; import com.fasterxml.jackson.core.type.TypeReference; import com.iab.openrtb.request.BidRequest; @@ -9,7 +9,6 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; 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; @@ -21,7 +20,7 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.bizzclick.ExtImpBizzclick; +import org.prebid.server.proto.openrtb.ext.request.blasto.ExtImpBlasto; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -29,13 +28,12 @@ import java.util.List; import java.util.Objects; -public class BizzclickBidder implements Bidder { +public class BlastoBidder implements Bidder { - private static final TypeReference> BIZZCLICK_EXT_TYPE_REFERENCE = + private static final TypeReference> EXT_TYPE_REFERENCE = new TypeReference<>() { }; - private static final String DEFAULT_HOST = "us-e-node1"; - private static final String URL_HOST_MACRO = "{{Host}}"; + private static final String URL_SOURCE_ID_MACRO = "{{SourceId}}"; private static final String URL_ACCOUNT_ID_MACRO = "{{AccountID}}"; private static final String DEFAULT_CURRENCY = "USD"; @@ -43,7 +41,7 @@ public class BizzclickBidder implements Bidder { private final String endpointUrl; private final JacksonMapper mapper; - public BizzclickBidder(String endpointUrl, JacksonMapper mapper) { + public BlastoBidder(String endpointUrl, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @@ -51,23 +49,23 @@ public BizzclickBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { final List imps = request.getImp(); - final ExtImpBizzclick extImpBizzclick; + final ExtImpBlasto extImp; try { - extImpBizzclick = parseImpExt(imps.getFirst()); + extImp = parseImpExt(imps.getFirst()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } final List modifiedImps = imps.stream() - .map(BizzclickBidder::modifyImp) + .map(BlastoBidder::modifyImp) .toList(); - return Result.withValue(createHttpRequest(request, modifiedImps, extImpBizzclick)); + return Result.withValue(createHttpRequest(request, modifiedImps, extImp)); } - private ExtImpBizzclick parseImpExt(Imp imp) throws PreBidException { + private ExtImpBlasto parseImpExt(Imp imp) throws PreBidException { try { - return mapper.mapper().convertValue(imp.getExt(), BIZZCLICK_EXT_TYPE_REFERENCE).getBidder(); + return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { throw new PreBidException("ext.bidder not provided"); } @@ -77,7 +75,7 @@ private static Imp modifyImp(Imp imp) { return imp.toBuilder().ext(null).build(); } - private HttpRequest createHttpRequest(BidRequest request, List imps, ExtImpBizzclick ext) { + private HttpRequest createHttpRequest(BidRequest request, List imps, ExtImpBlasto ext) { final BidRequest modifiedRequest = request.toBuilder().imp(imps).build(); return HttpRequest.builder() @@ -102,13 +100,10 @@ private static MultiMap headers(Device device) { return headers; } - private String buildEndpointUrl(ExtImpBizzclick ext) { - final String host = StringUtils.isBlank(ext.getHost()) ? DEFAULT_HOST : ext.getHost(); - final String sourceId = StringUtils.isBlank(ext.getSourceId()) ? ext.getPlacementId() : ext.getSourceId(); + private String buildEndpointUrl(ExtImpBlasto extImp) { return endpointUrl - .replace(URL_HOST_MACRO, HttpUtil.encodeUrl(host)) - .replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(sourceId)) - .replace(URL_ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(ext.getAccountId())); + .replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(extImp.getSourceId())) + .replace(URL_ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(extImp.getAccountId())); } @Override diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java similarity index 77% rename from src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java rename to src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java index dae1cd49c62..99413fa5e40 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java @@ -1,10 +1,10 @@ -package org.prebid.server.proto.openrtb.ext.request.bizzclick; +package org.prebid.server.proto.openrtb.ext.request.blasto; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; @Value(staticConstructor = "of") -public class ExtImpBizzclick { +public class ExtImpBlasto { @JsonProperty("host") String host; diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java similarity index 59% rename from src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java rename to src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java index c65702aa9fa..1c57db91aba 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java @@ -1,7 +1,7 @@ package org.prebid.server.spring.config.bidder; import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.bizzclick.BizzclickBidder; +import org.prebid.server.bidder.blasto.BlastoBidder; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -16,26 +16,26 @@ import jakarta.validation.constraints.NotBlank; @Configuration -@PropertySource(value = "classpath:/bidder-config/bizzclick.yaml", factory = YamlPropertySourceFactory.class) -public class BizzclickConfiguration { +@PropertySource(value = "classpath:/bidder-config/blasto.yaml", factory = YamlPropertySourceFactory.class) +public class BlastoConfiguration { - private static final String BIDDER_NAME = "bizzclick"; + private static final String BIDDER_NAME = "blasto"; - @Bean("bizzclickConfigurationProperties") - @ConfigurationProperties("adapters.bizzclick") + @Bean("blastoConfigurationProperties") + @ConfigurationProperties("adapters.blasto") BidderConfigurationProperties configurationProperties() { return new BidderConfigurationProperties(); } @Bean - BidderDeps bizzclickBidderDeps(BidderConfigurationProperties bizzclickConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { + BidderDeps blastoBidderDeps(BidderConfigurationProperties blastoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(bizzclickConfigurationProperties) + .withConfig(blastoConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new BizzclickBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new BlastoBidder(config.getEndpoint(), mapper)) .assemble(); } } diff --git a/src/main/resources/bidder-config/bizzclick.yaml b/src/main/resources/bidder-config/bizzclick.yaml deleted file mode 100644 index f5037c1014a..00000000000 --- a/src/main/resources/bidder-config/bizzclick.yaml +++ /dev/null @@ -1,15 +0,0 @@ -adapters: - bizzclick: - endpoint: http://{{Host}}.bizzclick.com/bid?rtb_seat_id={{SourceId}}&secret_key={{AccountID}} - meta-info: - maintainer-email: support@bizzclick.com - app-media-types: - - banner - - video - - native - site-media-types: - - banner - - video - - native - supported-vendors: - vendor-id: 0 diff --git a/src/main/resources/bidder-config/blasto.yaml b/src/main/resources/bidder-config/blasto.yaml new file mode 100644 index 00000000000..d202f0acb2b --- /dev/null +++ b/src/main/resources/bidder-config/blasto.yaml @@ -0,0 +1,22 @@ +# Contact support@blasto.ai to connect with Blasto exchange. +# We have the following regional endpoint sub-domains: +# US East: t-us +# EU: t-eu +# APAC: t-apac +# Please deploy this config in each of your datacenters with the appropriate regional subdomain +adapters: + blasto: + endpoint: http://t-us.blasto.ai/bid?rtb_seat_id={{SourceId}}&secret_key={{AccountID}} + endpoint-compression: gzip + meta-info: + maintainer-email: support@blasto.ai + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/static/bidder-params/bizzclick.json b/src/main/resources/static/bidder-params/blasto.json similarity index 72% rename from src/main/resources/static/bidder-params/bizzclick.json rename to src/main/resources/static/bidder-params/blasto.json index 879ab45314f..23109fb2421 100644 --- a/src/main/resources/static/bidder-params/bizzclick.json +++ b/src/main/resources/static/bidder-params/blasto.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Bizzclick Adapter Params", - "description": "A schema which validates params accepted by the Bizzclick adapter", + "title": "Blasto Adapter Params", + "description": "A schema which validates params accepted by the Blasto adapter", "type": "object", "properties": { "accountId": { @@ -9,14 +9,14 @@ "description": "Account id", "minLength": 1 }, - "placementId": { + "sourceId": { "type": "string", - "description": "PlacementId id", + "description": "Source id", "minLength": 1 } }, "required": [ "accountId", - "placementId" + "sourceId" ] } diff --git a/src/test/java/org/prebid/server/bidder/bizzclick/BizzclickBidderTest.java b/src/test/java/org/prebid/server/bidder/blasto/BlastoBidderTest.java similarity index 91% rename from src/test/java/org/prebid/server/bidder/bizzclick/BizzclickBidderTest.java rename to src/test/java/org/prebid/server/bidder/blasto/BlastoBidderTest.java index 283047ecaff..aa19d5b55e6 100644 --- a/src/test/java/org/prebid/server/bidder/bizzclick/BizzclickBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/blasto/BlastoBidderTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.bizzclick; +package org.prebid.server.bidder.blasto; import com.fasterxml.jackson.core.JsonProcessingException; import com.iab.openrtb.request.BidRequest; @@ -19,7 +19,7 @@ import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.bizzclick.ExtImpBizzclick; +import org.prebid.server.proto.openrtb.ext.request.blasto.ExtImpBlasto; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -34,19 +34,19 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.groups.Tuple.tuple; -public class BizzclickBidderTest extends VertxTest { +public class BlastoBidderTest extends VertxTest { - private static final String ENDPOINT = "https://{{Host}}/uri?source={{SourceId}}&account={{AccountID}}"; + private static final String ENDPOINT = "https://test.com/uri?source={{SourceId}}&account={{AccountID}}"; private static final String DEFAULT_HOST = "host"; private static final String DEFAULT_ACCOUNT_ID = "accountId"; private static final String DEFAULT_SOURCE_ID = "sourceId"; private static final String DEFAULT_PLACEMENT_ID = "placementId"; - private final BizzclickBidder target = new BizzclickBidder(ENDPOINT, jacksonMapper); + private final BlastoBidder target = new BlastoBidder(ENDPOINT, jacksonMapper); @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new BizzclickBidder("incorrect_url", jacksonMapper)); + assertThatIllegalArgumentException().isThrownBy(() -> new BlastoBidder("incorrect_url", jacksonMapper)); } @Test @@ -206,33 +206,7 @@ public void makeHttpRequestsShouldCreateSingleRequestWithExpectedUri() { // then assertThat(result.getValue()) .extracting(HttpRequest::getUri) - .containsExactly( - String.format("https://%s/uri?source=%s&account=%s", - DEFAULT_HOST, - DEFAULT_SOURCE_ID, - DEFAULT_ACCOUNT_ID)); - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void makeHttpRequestsShouldCreateSingleRequestWithExpectedAlternativeUri() { - // given - final String expectedDefaultHost = "us-e-node1"; - final BidRequest bidRequest = givenBidRequest( - givenImp(expectedDefaultHost, DEFAULT_ACCOUNT_ID, DEFAULT_PLACEMENT_ID, null) - ); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly( - String.format("https://%s/uri?source=%s&account=%s", - expectedDefaultHost, - DEFAULT_PLACEMENT_ID, - DEFAULT_ACCOUNT_ID)); + .containsExactly("https://test.com/uri?source=sourceId&account=accountId"); assertThat(result.getErrors()).isEmpty(); } @@ -448,7 +422,7 @@ private Imp givenImp(UnaryOperator impCustomizer) { } private Imp givenImp() { - final ExtPrebid ext = ExtPrebid.of(null, ExtImpBizzclick.of( + final ExtPrebid ext = ExtPrebid.of(null, ExtImpBlasto.of( DEFAULT_HOST, DEFAULT_ACCOUNT_ID, DEFAULT_PLACEMENT_ID, DEFAULT_SOURCE_ID )); return givenImp(imp -> imp.ext(mapper.valueToTree(ext))); @@ -456,7 +430,7 @@ private Imp givenImp() { private Imp givenImp(String host, String accountId, String placementId, String sourceId) { final ExtPrebid ext = ExtPrebid.of( - null, ExtImpBizzclick.of(host, accountId, placementId, sourceId) + null, ExtImpBlasto.of(host, accountId, placementId, sourceId) ); return givenImp(imp -> imp.ext(mapper.valueToTree(ext))); } diff --git a/src/test/java/org/prebid/server/it/BizzclickTest.java b/src/test/java/org/prebid/server/it/BlastoTest.java similarity index 57% rename from src/test/java/org/prebid/server/it/BizzclickTest.java rename to src/test/java/org/prebid/server/it/BlastoTest.java index 2ef4c68dffa..e26d75e6ca1 100644 --- a/src/test/java/org/prebid/server/it/BizzclickTest.java +++ b/src/test/java/org/prebid/server/it/BlastoTest.java @@ -14,24 +14,23 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static java.util.Collections.singletonList; -public class BizzclickTest extends IntegrationTest { +public class BlastoTest extends IntegrationTest { @Test - public void openrtb2AuctionShouldRespondWithBidsFromBizzclick() throws IOException, JSONException { + public void openrtb2AuctionShouldRespondWithBidsFromBlasto() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/bizzclick-exchange")) - .withQueryParam("host", equalTo("host")) - .withQueryParam("source", equalTo("placementId")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/blasto-exchange")) + .withQueryParam("source", equalTo("sourceId")) .withQueryParam("account", equalTo("accountId")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/bizzclick/test-bizzclick-bid-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/bizzclick/test-bizzclick-bid-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/blasto/test-blasto-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/blasto/test-blasto-bid-response.json")))); // when - final Response response = responseFor("openrtb2/bizzclick/test-auction-bizzclick-request.json", + final Response response = responseFor("openrtb2/blasto/test-auction-blasto-request.json", Endpoint.openrtb2_auction); // then - assertJsonEquals("openrtb2/bizzclick/test-auction-bizzclick-response.json", response, - singletonList("bizzclick")); + assertJsonEquals("openrtb2/blasto/test-auction-blasto-response.json", response, + singletonList("blasto")); } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-request.json similarity index 75% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-request.json index bfbeccf737f..8ee8e6865d7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-request.json @@ -8,10 +8,9 @@ "h": 250 }, "ext": { - "bizzclick": { - "host": "host", + "blasto": { "accountId": "accountId", - "placementId": "placementId" + "sourceId": "sourceId" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-response.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json similarity index 89% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-response.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json index d024a8f093b..9bf200e6d9d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json @@ -22,14 +22,14 @@ } } ], - "seat": "bizzclick", + "seat": "blasto", "group": 0 } ], "cur": "USD", "ext": { "responsetimemillis": { - "bizzclick": "{{ bizzclick.response_time_ms }}" + "blasto": "{{ blasto.response_time_ms }}" }, "prebid": { "auctiontimestamp": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-bid-request.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-request.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-bid-request.json diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-bid-response.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-response.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-bid-response.json 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 dd767fd8937..7f5bea5cbd0 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -127,8 +127,8 @@ adapters.bidstack.enabled=true adapters.bidstack.endpoint=http://localhost:8090/bidstack-exchange adapters.bigoad.enabled=true adapters.bigoad.endpoint=http://localhost:8090/bigoad-exchange -adapters.bizzclick.enabled=true -adapters.bizzclick.endpoint=http://localhost:8090/bizzclick-exchange?host={{Host}}&source={{SourceId}}&account={{AccountID}} +adapters.blasto.enabled=true +adapters.blasto.endpoint=http://localhost:8090/blasto-exchange?source={{SourceId}}&account={{AccountID}} adapters.bliink.enabled=true adapters.bliink.endpoint=http://localhost:8090/bliink-exchange adapters.bluesea.enabled=true From a69e3be3e1bb9132e639ac001488807ba1483345 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:33:40 -0400 Subject: [PATCH 3/9] Bugfix: RemoteFileSyncer handling of error responses (#3440) --- .../server/execution/RemoteFileSyncer.java | 31 ++++- .../execution/RemoteFileSyncerTest.java | 108 ++++++++++++++++-- 2 files changed, 123 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java b/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java index 9a6416ba44c..b841bf8a136 100644 --- a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java +++ b/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java @@ -1,5 +1,6 @@ package org.prebid.server.execution; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.Vertx; @@ -69,12 +70,14 @@ public RemoteFileSyncer(RemoteFileProcessor processor, getFileRequestOptions = new RequestOptions() .setMethod(HttpMethod.GET) .setTimeout(timeout) - .setAbsoluteURI(downloadUrl); + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); isUpdateRequiredRequestOptions = new RequestOptions() .setMethod(HttpMethod.HEAD) .setTimeout(timeout) - .setAbsoluteURI(downloadUrl); + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); } private static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) { @@ -112,8 +115,7 @@ private Future deleteFile(String filePath) { private Future syncRemoteFile(RetryPolicy retryPolicy) { return fileSystem.open(tmpFilePath, new OpenOptions()) - .compose(tmpFile -> httpClient.request(getFileRequestOptions) - .compose(HttpClientRequest::send) + .compose(tmpFile -> sendHttpRequest(getFileRequestOptions) .compose(response -> response.pipeTo(tmpFile)) .onComplete(result -> tmpFile.close())) @@ -148,8 +150,7 @@ private void setUpDeferredUpdate() { } private void updateIfNeeded() { - httpClient.request(isUpdateRequiredRequestOptions) - .compose(HttpClientRequest::send) + sendHttpRequest(isUpdateRequiredRequestOptions) .compose(response -> fileSystem.exists(saveFilePath) .compose(exists -> exists ? isLengthChanged(response) @@ -161,6 +162,24 @@ private void updateIfNeeded() { }); } + private Future sendHttpRequest(RequestOptions requestOptions) { + return httpClient.request(requestOptions) + .compose(HttpClientRequest::send) + .compose(this::validateResponse); + } + + private Future validateResponse(HttpClientResponse response) { + final int statusCode = response.statusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + return Future.failedFuture(new PreBidException( + String.format("Got unexpected response from server with status code %s and message %s", + statusCode, + response.statusMessage()))); + } else { + return Future.succeededFuture(response); + } + } + private Future isLengthChanged(HttpClientResponse response) { final String contentLengthParameter = response.getHeader(HttpHeaders.CONTENT_LENGTH); return StringUtils.isNumeric(contentLengthParameter) && !contentLengthParameter.equals("0") diff --git a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java b/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java index 341c7764cb3..9acd719d30f 100644 --- a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java +++ b/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java @@ -1,5 +1,6 @@ package org.prebid.server.execution; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; @@ -182,6 +183,8 @@ public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); given(httpClientResponse.getHeader(HttpHeaders.CONTENT_LENGTH)) .willReturn("notnumber"); @@ -209,7 +212,10 @@ public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.getHeader(any(CharSequence.class))).willReturn(FILE_SIZE.toString()); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); + given(httpClientResponse.getHeader(any(CharSequence.class))) + .willReturn(FILE_SIZE.toString()); given(fileSystem.props(anyString())) .willReturn(Future.failedFuture(new IllegalArgumentException("ERROR"))); @@ -240,7 +246,10 @@ public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.getHeader(any(CharSequence.class))).willReturn(FILE_SIZE.toString()); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); + given(httpClientResponse.getHeader(any(CharSequence.class))) + .willReturn(FILE_SIZE.toString()); given(fileSystem.props(anyString())) .willReturn(Future.succeededFuture(fileProps)); @@ -274,8 +283,12 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.pipeTo(any())).willReturn(Future.succeededFuture()); - given(httpClientResponse.getHeader(any(CharSequence.class))).willReturn(FILE_SIZE.toString()); + given(httpClientResponse.pipeTo(any())) + .willReturn(Future.succeededFuture()); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); + given(httpClientResponse.getHeader(any(CharSequence.class))) + .willReturn(FILE_SIZE.toString()); given(fileSystem.props(anyString())) .willReturn(Future.succeededFuture(fileProps)); @@ -291,7 +304,8 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { given(fileSystem.move(anyString(), any(), any(CopyOptions.class))) .willReturn(Future.succeededFuture()); - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(remoteFileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); // when remoteFileSyncer.sync(); @@ -354,7 +368,8 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() { .willAnswer(withSelfAndPassObjectToHandler(Future.succeededFuture())) .willAnswer(withSelfAndPassObjectToHandler(Future.failedFuture(new RuntimeException()))); - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(remoteFileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); // when remoteFileSyncer.sync(); @@ -370,7 +385,8 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() { @Test public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotSet() { // given - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(remoteFileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); given(fileSystem.exists(anyString())) .willReturn(Future.succeededFuture(false)); @@ -382,6 +398,8 @@ public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotS .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); given(httpClientResponse.pipeTo(asyncFile)) .willReturn(Future.succeededFuture()); @@ -395,6 +413,7 @@ public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotS verify(fileSystem).open(eq(TMP_FILE_PATH), any()); verify(httpClient).request(any()); verify(asyncFile).close(); + verify(httpClientResponse).statusCode(); verify(remoteFileProcessor).setDataPath(any()); verify(fileSystem).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any()); @@ -419,8 +438,6 @@ public void syncForFilepathShouldRetryWhenTimeoutIsReached() { given(httpClient.request(any())) .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) - .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.pipeTo(asyncFile)) .willReturn(Future.failedFuture("Timeout")); // when @@ -429,6 +446,7 @@ public void syncForFilepathShouldRetryWhenTimeoutIsReached() { // then verify(vertx, times(RETRY_COUNT)).setTimer(eq(RETRY_INTERVAL), any()); verify(fileSystem, times(RETRY_COUNT + 1)).open(eq(TMP_FILE_PATH), any()); + verify(httpClientResponse, never()).pipeTo(any()); // Response handled verify(httpClient, times(RETRY_COUNT + 1)).request(any()); @@ -437,11 +455,81 @@ public void syncForFilepathShouldRetryWhenTimeoutIsReached() { verifyNoInteractions(remoteFileProcessor); } + @Test + public void syncShouldNotSaveFileWhenServerRespondsWithNonOkStatusCode() { + // given + given(fileSystem.exists(anyString())) + .willReturn(Future.succeededFuture(false)); + given(fileSystem.open(any(), any())) + .willReturn(Future.succeededFuture(asyncFile)); + given(fileSystem.move(anyString(), anyString(), any(CopyOptions.class))) + .willReturn(Future.succeededFuture()); + + given(httpClient.request(any())) + .willReturn(Future.succeededFuture(httpClientRequest)); + given(httpClientRequest.send()) + .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(0); + + // when + remoteFileSyncer.sync(); + + // then + verify(fileSystem, times(1)).exists(eq(FILE_PATH)); + verify(fileSystem).open(eq(TMP_FILE_PATH), any()); + verify(fileSystem).delete(eq(TMP_FILE_PATH)); + verify(asyncFile).close(); + verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); + verify(httpClient).request(any()); + verify(httpClientResponse).statusCode(); + verify(httpClientResponse, never()).pipeTo(any()); + verify(remoteFileProcessor, never()).setDataPath(any()); + verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any()); + } + + @Test + public void syncShouldNotUpdateFileWhenServerRespondsWithNonOkStatusCode() { + // given + remoteFileSyncer = new RemoteFileSyncer( + remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); + + givenTriggerUpdate(); + + given(fileSystem.open(any(), any())) + .willReturn(Future.succeededFuture(asyncFile)); + given(fileSystem.move(anyString(), anyString(), any(CopyOptions.class))) + .willReturn(Future.succeededFuture()); + + given(httpClient.request(any())) + .willReturn(Future.succeededFuture(httpClientRequest)); + given(httpClientRequest.send()) + .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(0); + + // when + remoteFileSyncer.sync(); + + // then + verify(fileSystem, times(1)).exists(eq(FILE_PATH)); + verify(fileSystem, never()).open(any(), any()); + verify(fileSystem, never()).delete(any()); + verify(fileSystem, never()).move(any(), any(), any(), any()); + verify(asyncFile, never()).close(); + verify(httpClient, times(1)).request(any()); + verify(httpClientResponse).statusCode(); + verify(httpClientResponse, never()).pipeTo(any()); + verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any()); + } + private void givenTriggerUpdate() { given(fileSystem.exists(anyString())) .willReturn(Future.succeededFuture(true)); - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(remoteFileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); given(vertx.setPeriodic(eq(UPDATE_INTERVAL), any())) .willAnswer(withReturnObjectAndPassObjectToHandler(123L, 123L, 1)) From 8ecf1a48f6aec951f3b6b9e50f57519ed7d98df9 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:57:31 +0200 Subject: [PATCH 4/9] Core: Add `video.poddedupe` field (#3424) --- src/main/java/com/iab/openrtb/request/Video.java | 6 ++++++ .../server/functional/model/request/auction/Video.groovy | 2 ++ .../prebid/server/functional/tests/OrtbConverterSpec.groovy | 2 ++ 3 files changed, 10 insertions(+) diff --git a/src/main/java/com/iab/openrtb/request/Video.java b/src/main/java/com/iab/openrtb/request/Video.java index f967886bf89..369d576a3ac 100644 --- a/src/main/java/com/iab/openrtb/request/Video.java +++ b/src/main/java/com/iab/openrtb/request/Video.java @@ -254,6 +254,12 @@ public class Video { */ List companiontype; + /** + * Indicates pod deduplication settings that will be applied to bid responses. Refer to + * List: Pod Deduplication in AdCOM 1.0. + */ + List poddedupe; + /** * An array of objects (Section 3.2.35) * indicating the floor prices for video creatives of various durations that the buyer may bid with. diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy index bc2ef7f5a5c..a70ee05eac3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy @@ -43,6 +43,8 @@ class Video { List companionad List api List companiontype + @JsonProperty("poddedupe") + List podDeduplication static Video getDefaultVideo() { new Video(mimes: ["video/mp4"], weight: 300, height: 200) diff --git a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy index 6ffaedca01e..7eb388cf202 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy @@ -561,6 +561,7 @@ class OrtbConverterSpec extends BaseSpec { mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber plcmt = PBSUtils.getRandomEnum(VideoPlcmtSubtype) + podDeduplication = [PBSUtils.randomNumber] } } @@ -584,6 +585,7 @@ class OrtbConverterSpec extends BaseSpec { mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber plcmt = PBSUtils.getRandomEnum(VideoPlcmtSubtype) + podDeduplication = [PBSUtils.randomNumber, PBSUtils.randomNumber] } } From 1afd0a1062fa97da7212cf30da55b4749cbf9807 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Tue, 17 Sep 2024 13:39:51 +0200 Subject: [PATCH 5/9] Adtonos: Add new adapter (#3446) --- .../server/bidder/adtonos/AdtonosBidder.java | 145 ++++++++++ .../ext/request/adtonos/ExtImpAdtonos.java | 11 + .../config/bidder/AdtonosConfiguration.java | 41 +++ src/main/resources/bidder-config/adtonos.yaml | 22 ++ .../static/bidder-params/adtonos.json | 15 + .../bidder/adtonos/AdtonosBidderTest.java | 271 ++++++++++++++++++ .../org/prebid/server/it/AdtonosTest.java | 35 +++ .../adtonos/test-adtonos-bid-request.json | 56 ++++ .../adtonos/test-adtonos-bid-response.json | 16 ++ .../adtonos/test-auction-adtonos-request.json | 23 ++ .../test-auction-adtonos-response.json | 34 +++ .../server/it/test-application.properties | 2 + 12 files changed, 671 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java create mode 100644 src/main/resources/bidder-config/adtonos.yaml create mode 100644 src/main/resources/static/bidder-params/adtonos.json create mode 100644 src/test/java/org/prebid/server/bidder/adtonos/AdtonosBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/AdtonosTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json diff --git a/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java b/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java new file mode 100644 index 00000000000..f780a020732 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java @@ -0,0 +1,145 @@ +package org.prebid.server.bidder.adtonos; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +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 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.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.adtonos.ExtImpAdtonos; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class AdtonosBidder implements Bidder { + + private static final TypeReference> ADTONOS_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String PUBLISHER_ID_MACRO = "{{PublisherId}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AdtonosBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + try { + final ExtImpAdtonos impExt = parseImpExt(bidRequest.getImp().getFirst()); + return Result.withValue(BidderUtil.defaultRequest(bidRequest, makeUrl(impExt), mapper)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private ExtImpAdtonos parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ADTONOS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException( + "Invalid imp.ext.bidder for impression index 0. Error Infomation: " + e.getMessage()); + } + } + + private String makeUrl(ExtImpAdtonos extImp) { + return endpointUrl.replace(PUBLISHER_ID_MACRO, extImp.getSupplierId()); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final BidResponse bidResponse; + try { + bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + + final List errors = new ArrayList<>(); + final List bids = extractBids(bidResponse, httpCall.getRequest().getPayload(), errors); + + return Result.of(bids, errors); + } + + private static List extractBids(BidResponse bidResponse, + BidRequest bidRequest, + List errors) { + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), bidRequest, errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, BidRequest bidRequest, List errors) { + try { + return BidderBid.of(bid, resolveBidType(bid, bidRequest.getImp()), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType resolveBidType(Bid bid, List imps) throws PreBidException { + final Integer markupType = bid.getMtype(); + if (markupType != null) { + switch (markupType) { + case 1 -> { + return BidType.banner; + } + case 2 -> { + return BidType.video; + } + case 3 -> { + return BidType.audio; + } + case 4 -> { + return BidType.xNative; + } + } + } + + final String impId = bid.getImpid(); + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getAudio() != null) { + return BidType.audio; + } else if (imp.getVideo() != null) { + return BidType.video; + } + throw new PreBidException("Unsupported bidtype for bid: " + bid.getId()); + } + } + + throw new PreBidException("Failed to find impression: " + impId); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java new file mode 100644 index 00000000000..121d025f654 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.adtonos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAdtonos { + + @JsonProperty("supplierId") + String supplierId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java new file mode 100644 index 00000000000..8a86c88ac81 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.adtonos.AdtonosBidder; +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/adtonos.yaml", factory = YamlPropertySourceFactory.class) +public class AdtonosConfiguration { + + private static final String BIDDER_NAME = "adtonos"; + + @Bean("adtonosConfigurationProperties") + @ConfigurationProperties("adapters.adtonos") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps adtonosBidderDeps(BidderConfigurationProperties adtonosConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(adtonosConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AdtonosBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/adtonos.yaml b/src/main/resources/bidder-config/adtonos.yaml new file mode 100644 index 00000000000..e1a19fbc6eb --- /dev/null +++ b/src/main/resources/bidder-config/adtonos.yaml @@ -0,0 +1,22 @@ +adapters: + adtonos: + endpoint: https://exchange.adtonos.com/bid/{{PublisherId}} + geoscope: + - global + meta-info: + maintainer-email: support@adtonos.com + app-media-types: + - video + - audio + site-media-types: + - audio + dooh-media-types: + - audio + supported-vendors: + vendor-id: 682 + usersync: + cookie-family-name: adtonos + redirect: + url: https://play.adtonos.com/redir?to={{redirect_url}} + support-cors: false + uid-macro: '@UUID@' diff --git a/src/main/resources/static/bidder-params/adtonos.json b/src/main/resources/static/bidder-params/adtonos.json new file mode 100644 index 00000000000..b1ea833f1e0 --- /dev/null +++ b/src/main/resources/static/bidder-params/adtonos.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdTonos Adapter Params", + "description": "A schema which validates params accepted by the AdTonos adapter", + "type": "object", + "properties": { + "supplierId": { + "type": "string", + "description": "ID of the supplier account in AdTonos platform" + } + }, + "required": [ + "supplierId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/adtonos/AdtonosBidderTest.java b/src/test/java/org/prebid/server/bidder/adtonos/AdtonosBidderTest.java new file mode 100644 index 00000000000..2d66b2f28ac --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/adtonos/AdtonosBidderTest.java @@ -0,0 +1,271 @@ +package org.prebid.server.bidder.adtonos; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +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.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.adtonos.ExtImpAdtonos; + +import java.util.List; +import java.util.function.UnaryOperator; + +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.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; + +public class AdtonosBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://randomurl.com?param={{PublisherId}}"; + + private final AdtonosBidder target = new AdtonosBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new AdtonosBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldCreateExpectedUrl() { + // given + final ExtImpAdtonos impExt = ExtImpAdtonos.of("publisherId"); + final BidRequest bidRequest = givenBidRequest(impBuilder -> + impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, impExt)))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://randomurl.com?param=publisherId"); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = target.makeBids(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.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidIfMTypeIsOne() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(1).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(1).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfMTypeIsTwo() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(2).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(2).build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBidIfMTypeIsThree() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(3).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(3).build(), audio, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidIfMTypeIsFour() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(4).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBidsForAudioImpsIfMTypeMissed() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(impBuilder -> + impBuilder.audio(Audio.builder().build())))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().impid("123").build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnVideoBidsForVideoImpsIfMTypeMissed() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(impBuilder -> + impBuilder.video(Video.builder().build())))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().impid("123").build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorsForBidsThatDoesNotMatchSupportedMediaTypes() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(identity()))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().id("456").impid("123").build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .extracting(BidderError::getMessage) + .containsExactly("Unsupported bidtype for bid: 456"); + } + + @Test + public void makeBidsShouldReturnErrorsForBidsThatDoesNotContainMTypeAndImpMatch() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(identity()))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().impid("789").build(), + Bid.builder().id("123").mtype(1).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().id("123").mtype(1).build(), banner, "USD")); + assertThat(result.getErrors()).hasSize(1) + .extracting(BidderError::getMessage) + .containsExactly("Failed to find impression: 789"); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator impCustomizer) { + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList(impCustomizer.apply(Imp.builder().id("123")).build()))) + .build(); + } + + private static BidResponse givenBidResponse(Bid... bids) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(List.of(bids)) + .build())) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("123")) + .banner(Banner.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdtonos.of("testPubId")))) + .build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/AdtonosTest.java b/src/test/java/org/prebid/server/it/AdtonosTest.java new file mode 100644 index 00000000000..389edc02a5e --- /dev/null +++ b/src/test/java/org/prebid/server/it/AdtonosTest.java @@ -0,0 +1,35 @@ +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 AdtonosTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheAdtonosBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adtonos-exchange/testPublisherId")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/adtonos/test-adtonos-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/adtonos/test-adtonos-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/adtonos/test-auction-adtonos-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/adtonos/test-auction-adtonos-response.json", response, + singletonList("adtonos")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-request.json new file mode 100644 index 00000000000..ab7be17fc96 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "supplierId": "testPublisherId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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" + ], + "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/adtonos/test-adtonos-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-response.json new file mode 100644 index 00000000000..c9191a06125 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-response.json @@ -0,0 +1,16 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "mtype": 1, + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId" + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-request.json new file mode 100644 index 00000000000..8077266f37e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "adtonos": { + "supplierId": "testPublisherId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json new file mode 100644 index 00000000000..e6795976a7f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json @@ -0,0 +1,34 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "mtype": 1, + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId", + "ext": { + "origbidcpm": 3.33, + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "adtonos", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "adtonos": "{{ adtonos.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} 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 7f5bea5cbd0..eebc53e085f 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -64,6 +64,8 @@ adapters.adtelligent.aliases.copper6.enabled=true adapters.adtelligent.aliases.copper6.endpoint=http://localhost:8090/copper6-exchange adapters.adtelligent.aliases.indicue.enabled=true adapters.adtelligent.aliases.indicue.endpoint=http://localhost:8090/indicue-exchange +adapters.adtonos.enabled=true +adapters.adtonos.endpoint=http://localhost:8090/adtonos-exchange/{{PublisherId}} adapters.adtrgtme.enabled=true adapters.adtrgtme.endpoint=http://localhost:8090/adtrgtme-exchange adapters.advangelists.enabled=true From b5c8d8ee912c2133d72d70effb2ef971d3b32257 Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Tue, 17 Sep 2024 19:31:06 +0300 Subject: [PATCH 6/9] Github Actions: Add support for multiplatform docker image (x86-64, arm) (#3430) --- .github/workflows/docker-image-publish.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/docker-image-publish.yml b/.github/workflows/docker-image-publish.yml index 286b03d03d3..39964eb69aa 100644 --- a/.github/workflows/docker-image-publish.yml +++ b/.github/workflows/docker-image-publish.yml @@ -55,11 +55,18 @@ jobs: with: images: ${{ matrix.package-name }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . file: ${{ matrix.dockerfile-path }} push: true + platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 1d5400b3ad75fc375fcfdc572558ae7ba6b3495b Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:31:44 +0200 Subject: [PATCH 7/9] Modules: Response Correction Module (#3409) --- extra/bundle/pom.xml | 5 + extra/modules/pb-response-correction/pom.xml | 15 + .../pb-response-correction/src/lombok.config | 1 + ...ResponseCorrectionModuleConfiguration.java | 37 ++ .../correction/core/CorrectionsProvider.java | 25 + .../core/config/model/AppVideoHtmlConfig.java | 15 + .../correction/core/config/model/Config.java | 13 + .../core/correction/Correction.java | 11 + .../core/correction/CorrectionProducer.java | 11 + .../appvideohtml/AppVideoHtmlCorrection.java | 137 +++++ .../AppVideoHtmlCorrectionProducer.java | 28 + ...orrectionAllProcessedBidResponsesHook.java | 104 ++++ .../v1/ResponseCorrectionModule.java | 32 ++ .../v1/model/InvocationResultImpl.java | 38 ++ .../core/CorrectionsProviderTest.java | 58 ++ .../AppVideoHtmlCorrectionProducerTest.java | 66 +++ .../AppVideoHtmlCorrectionTest.java | 197 +++++++ ...ctionAllProcessedBidResponsesHookTest.java | 118 ++++ extra/modules/pom.xml | 1 + .../server/functional/model/ModuleName.groovy | 5 +- .../model/config/AppVideoHtml.groovy | 15 + .../config/ModuleHookImplementation.groovy | 1 + .../model/config/Ortb2BlockingConfig.groovy | 2 - .../model/config/PbResponseCorrection.groovy | 13 + .../model/config/PbsModulesConfig.groovy | 1 + .../tests/module/ModuleBaseSpec.groovy | 7 + .../AnalyticsTagsModuleSpec.groovy | 3 +- .../ResponseCorrectionSpec.groovy | 507 ++++++++++++++++++ 28 files changed, 1461 insertions(+), 5 deletions(-) create mode 100644 extra/modules/pb-response-correction/pom.xml create mode 100644 extra/modules/pb-response-correction/src/lombok.config create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java create mode 100644 extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java create mode 100644 extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java create mode 100644 extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java create mode 100644 extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy rename src/test/groovy/org/prebid/server/functional/tests/module/{ => analyticstag}/AnalyticsTagsModuleSpec.groovy (99%) create mode 100644 src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index bd14db698fc..ad9d306f578 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -40,6 +40,11 @@ pb-richmedia-filter ${project.version} + + org.prebid.server.hooks.modules + pb-response-correction + ${project.version} + diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml new file mode 100644 index 00000000000..abf009733b2 --- /dev/null +++ b/extra/modules/pb-response-correction/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.12.0-SNAPSHOT + + + pb-response-correction + + pb-response-correction + Response correction module + diff --git a/extra/modules/pb-response-correction/src/lombok.config b/extra/modules/pb-response-correction/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/pb-response-correction/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java new file mode 100644 index 00000000000..816cf122214 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.pb.response.correction.config; + +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml.AppVideoHtmlCorrection; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml.AppVideoHtmlCorrectionProducer; +import org.prebid.server.hooks.modules.pb.response.correction.v1.ResponseCorrectionModule; +import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; +import org.prebid.server.json.ObjectMapperProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@ConditionalOnProperty(prefix = "hooks." + ResponseCorrectionModule.CODE, name = "enabled", havingValue = "true") +@Configuration +public class ResponseCorrectionModuleConfiguration { + + @Bean + AppVideoHtmlCorrectionProducer appVideoHtmlCorrectionProducer( + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + return new AppVideoHtmlCorrectionProducer( + new AppVideoHtmlCorrection(ObjectMapperProvider.mapper(), logSamplingRate)); + } + + @Bean + CorrectionsProvider correctionsProvider(List correctionsProducers) { + return new CorrectionsProvider(correctionsProducers); + } + + @Bean + ResponseCorrectionModule responseCorrectionModule(CorrectionsProvider correctionsProvider) { + return new ResponseCorrectionModule(correctionsProvider, ObjectMapperProvider.mapper()); + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java new file mode 100644 index 00000000000..2afe514bf7d --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +import java.util.List; +import java.util.Objects; + +public class CorrectionsProvider { + + private final List correctionsProducers; + + public CorrectionsProvider(List correctionsProducers) { + this.correctionsProducers = Objects.requireNonNull(correctionsProducers); + } + + public List corrections(Config config, BidRequest bidRequest) { + return correctionsProducers.stream() + .filter(correctionProducer -> correctionProducer.shouldProduce(config, bidRequest)) + .map(CorrectionProducer::produce) + .toList(); + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java new file mode 100644 index 00000000000..06b0990f149 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class AppVideoHtmlConfig { + + boolean enabled; + + @JsonProperty("excluded-bidders") + List excludedBidders; +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java new file mode 100644 index 00000000000..17cd2453b16 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class Config { + + boolean enabled; + + @JsonProperty("app-video-html") + AppVideoHtmlConfig appVideoHtmlConfig; +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java new file mode 100644 index 00000000000..3f7abf1c5c5 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction; + +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; + +import java.util.List; + +public interface Correction { + + List apply(Config config, List bidderResponses); +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java new file mode 100644 index 00000000000..6cd19836b96 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; + +public interface CorrectionProducer { + + boolean shouldProduce(Config config, BidRequest bidRequest); + + Correction produce(); +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java new file mode 100644 index 00000000000..3df769e52cb --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java @@ -0,0 +1,137 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.Bid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +public class AppVideoHtmlCorrection implements Correction { + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger( + LoggerFactory.getLogger(AppVideoHtmlCorrection.class)); + + private static final Pattern VAST_XML_PATTERN = Pattern.compile("<\\w*VAST\\w+", Pattern.CASE_INSENSITIVE); + private static final TypeReference> EXT_BID_PREBID_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String NATIVE_ADM_MESSAGE = "Bid %s of bidder %s has an JSON ADM, that appears to be native"; + private static final String ADM_WITH_NO_ASSETS_MESSAGE = "Bid %s of bidder %s has a JSON ADM, but without assets"; + private static final String CHANGING_BID_MEDIA_TYPE_MESSAGE = "Bid %s of bidder %s: changing media type to banner"; + + private final ObjectMapper mapper; + private final double logSamplingRate; + + public AppVideoHtmlCorrection(ObjectMapper mapper, double logSamplingRate) { + this.mapper = mapper; + this.logSamplingRate = logSamplingRate; + } + + @Override + public List apply(Config config, List bidderResponses) { + final Collection excludedBidders = CollectionUtils.emptyIfNull( + config.getAppVideoHtmlConfig().getExcludedBidders()); + + return bidderResponses.stream() + .map(response -> modify(response, excludedBidders)) + .toList(); + } + + private BidderResponse modify(BidderResponse response, Collection excludedBidders) { + final String bidder = response.getBidder(); + if (excludedBidders.contains(bidder)) { + return response; + } + + final BidderSeatBid seatBid = response.getSeatBid(); + final List modifiedBids = seatBid.getBids().stream() + .map(bidderBid -> modifyBid(bidder, bidderBid)) + .toList(); + + return response.with(seatBid.with(modifiedBids)); + } + + private BidderBid modifyBid(String bidder, BidderBid bidderBid) { + final Bid bid = bidderBid.getBid(); + final String bidId = bid.getId(); + final String adm = bid.getAdm(); + + if (adm == null || isVideoWithVastXml(bidderBid.getType(), adm) || hasNativeAdm(adm, bidId, bidder)) { + return bidderBid; + } + + conditionalLogger.warn(CHANGING_BID_MEDIA_TYPE_MESSAGE.formatted(bidId, bidder), logSamplingRate); + + final ExtBidPrebid prebid = parseExtBidPrebid(bid); + + final ExtBidPrebidMeta modifiedMeta = Optional.ofNullable(prebid) + .map(ExtBidPrebid::getMeta) + .map(ExtBidPrebidMeta::toBuilder) + .orElseGet(ExtBidPrebidMeta::builder) + .mediaType(BidType.video.getName()) + .build(); + + final ExtBidPrebid modifiedPrebid = Optional.ofNullable(prebid) + .map(ExtBidPrebid::toBuilder) + .orElseGet(ExtBidPrebid::builder) + .meta(modifiedMeta) + .type(BidType.banner) + .build(); + + final ObjectNode modifiedBidExt = mapper.valueToTree(ExtPrebid.of(modifiedPrebid, null)); + + return bidderBid.toBuilder() + .type(BidType.banner) + .bid(bid.toBuilder().ext(modifiedBidExt).build()) + .build(); + } + + private boolean hasNativeAdm(String adm, String bidId, String bidder) { + final JsonNode admNode; + try { + admNode = mapper.readTree(adm); + } catch (JsonProcessingException e) { + return false; + } + + final boolean hasAssets = admNode.has("assets"); + final String warningMessage = hasAssets + ? NATIVE_ADM_MESSAGE.formatted(bidId, bidder) + : ADM_WITH_NO_ASSETS_MESSAGE.formatted(bidId, bidder); + + conditionalLogger.warn(warningMessage, logSamplingRate); + return hasAssets; + } + + private static boolean isVideoWithVastXml(BidType type, String adm) { + return type == BidType.video && VAST_XML_PATTERN.matcher(adm).matches(); + } + + private ExtBidPrebid parseExtBidPrebid(Bid bid) { + try { + return mapper.convertValue(bid.getExt(), EXT_BID_PREBID_TYPE_REFERENCE).getPrebid(); + } catch (Exception e) { + return null; + } + } + +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java new file mode 100644 index 00000000000..f7a05137bf0 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +public class AppVideoHtmlCorrectionProducer implements CorrectionProducer { + + private final AppVideoHtmlCorrection correctionInstance; + + public AppVideoHtmlCorrectionProducer(AppVideoHtmlCorrection correction) { + this.correctionInstance = correction; + } + + @Override + public boolean shouldProduce(Config config, BidRequest bidRequest) { + final AppVideoHtmlConfig appVideoHtmlConfig = config.getAppVideoHtmlConfig(); + final boolean enabled = appVideoHtmlConfig != null && appVideoHtmlConfig.isEnabled(); + return enabled && bidRequest.getApp() != null; + } + + @Override + public Correction produce() { + return correctionInstance; + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java new file mode 100644 index 00000000000..625c857a23e --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java @@ -0,0 +1,104 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.v1.model.InvocationResultImpl; +import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesHook; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; + +import java.util.List; +import java.util.Objects; + +public class ResponseCorrectionAllProcessedBidResponsesHook implements AllProcessedBidResponsesHook { + + private static final String CODE = "pb-response-correction-all-processed-bid-responses-hook"; + + private final CorrectionsProvider correctionsProvider; + private final ObjectMapper mapper; + + public ResponseCorrectionAllProcessedBidResponsesHook(CorrectionsProvider correctionsProvider, ObjectMapper mapper) { + this.correctionsProvider = Objects.requireNonNull(correctionsProvider); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Future> call(AllProcessedBidResponsesPayload payload, + AuctionInvocationContext context) { + + final Config config; + try { + config = moduleConfig(context.accountConfig()); + } catch (PreBidException e) { + return failure(e.getMessage()); + } + + if (config == null || !config.isEnabled()) { + return noAction(); + } + + final BidRequest bidRequest = context.auctionContext().getBidRequest(); + + final List corrections = correctionsProvider.corrections(config, bidRequest); + if (corrections.isEmpty()) { + return noAction(); + } + + final InvocationResult invocationResult = InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(initialPayload -> AllProcessedBidResponsesPayloadImpl.of( + applyCorrections(initialPayload.bidResponses(), config, corrections))) + .build(); + + return Future.succeededFuture(invocationResult); + } + + private Config moduleConfig(ObjectNode accountConfig) { + try { + return mapper.treeToValue(accountConfig, Config.class); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static List applyCorrections(List bidderResponses, Config config, List corrections) { + List result = bidderResponses; + for (Correction correction : corrections) { + result = correction.apply(config, result); + } + return result; + } + + private Future> failure(String message) { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.failure) + .message(message) + .action(InvocationAction.no_action) + .build()); + } + + private static Future> noAction() { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java new file mode 100644 index 00000000000..5ea3b583acd --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java @@ -0,0 +1,32 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; +import java.util.Collections; + +public class ResponseCorrectionModule implements Module { + + public static final String CODE = "pb-response-correction"; + + private final Collection> hooks; + + public ResponseCorrectionModule(CorrectionsProvider correctionsProvider, ObjectMapper mapper) { + this.hooks = Collections.singleton( + new ResponseCorrectionAllProcessedBidResponsesHook(correctionsProvider, mapper)); + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java new file mode 100644 index 00000000000..1a39413583c --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1.model; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; + +import java.util.List; + +@Accessors(fluent = true) +@Builder +@Value +public class InvocationResultImpl implements InvocationResult { + + InvocationStatus status; + + String message; + + InvocationAction action; + + PayloadUpdate payloadUpdate; + + List errors; + + List warnings; + + List debugMessages; + + Object moduleContext; + + Tags analyticsTags; +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java new file mode 100644 index 00000000000..c5e7ac2d3f0 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core; + +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.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class CorrectionsProviderTest { + + @Mock + private CorrectionProducer correctionProducer; + + private CorrectionsProvider target; + + @BeforeEach + public void setUp() { + target = new CorrectionsProvider(singletonList(correctionProducer)); + } + + @Test + public void correctionsShouldReturnEmptyListIfAllCorrectionsDisabled() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(false); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).isEmpty(); + } + + @Test + public void correctionsShouldReturnProducedCorrection() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(true); + + final Correction correction = mock(Correction.class); + given(correctionProducer.produce()).willReturn(correction); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).containsExactly(correction); + } +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java new file mode 100644 index 00000000000..15305d6bed2 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java @@ -0,0 +1,66 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Site; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.json.ObjectMapperProvider; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class AppVideoHtmlCorrectionProducerTest { + + private final AppVideoHtmlCorrection CORRECTION_INSTANCE = + new AppVideoHtmlCorrection(ObjectMapperProvider.mapper(), 0.1); + + private final AppVideoHtmlCorrectionProducer target = new AppVideoHtmlCorrectionProducer(CORRECTION_INSTANCE); + + @Test + public void produceShouldReturnCorrectionInstance() { + // when & then + assertThat(target.produce()).isSameAs(CORRECTION_INSTANCE); + } + + @Test + public void shouldProduceReturnFalseWhenAppVideoHtmlConfigIsDisabled() { + // given + final Config givenConfig = givenConfig(false); + final BidRequest givenRequest = BidRequest.builder().app(App.builder().build()).build(); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isFalse(); + } + + @Test + public void shouldProduceReturnFalseWhenBidRequestIsNotAppRequest() { + // given + final Config givenConfig = givenConfig(true); + final BidRequest givenRequest = BidRequest.builder().site(Site.builder().build()).build(); + + // when + target.shouldProduce(givenConfig, givenRequest); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isFalse(); + } + + @Test + public void shouldProduceReturnTrueWhenConfigIsEnabledAndBidRequestHasApp() { + // given + final Config givenConfig = givenConfig(true); + final BidRequest givenRequest = BidRequest.builder().app(App.builder().build()).build(); + + // when + target.shouldProduce(givenConfig, givenRequest); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isTrue(); + } + + private static Config givenConfig(boolean enabled) { + return Config.of(true, AppVideoHtmlConfig.of(enabled, null)); + } + +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java new file mode 100644 index 00000000000..537e79943cd --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java @@ -0,0 +1,197 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AppVideoHtmlCorrectionTest { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + private final AppVideoHtmlCorrection target = new AppVideoHtmlCorrection(MAPPER, 0.1); + + @Test + public void applyShouldNotChangeBidResponsesFromExcludedBidders() { + // given + final Config givenConfig = givenConfig(List.of("bidderA", "bidderB")); + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", null, 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + private static Config givenConfig(List excludedBidders) { + return Config.of(true, AppVideoHtmlConfig.of(true, excludedBidders)); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenAdmIsNull() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + final BidderBid givenBid = givenBid(null, BidType.video); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of(List.of(givenBid)), 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + private static BidderBid givenBid(String adm, BidType type) { + return givenBid(adm, type, null); + } + + private static BidderBid givenBid(String adm, BidType type, ObjectNode bidExt) { + final Bid bid = Bid.builder().adm(adm).ext(bidExt).build(); + return BidderBid.of(bid, type, "USD"); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenBidIsVideoAndHasVastXmlInAdm() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of( + List.of(givenBid(" actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenBidHasNativeAdm() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of( + List.of(givenBid("{\"field\":1,\"assets\":[{\"id\":2}]}", BidType.video))), 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + @Test + public void applyShouldChangeTypeToBannerAndAddMetaTypeVideoWhenAdmIsJsonButNotNative() { + // given + final Config givenConfig = givenConfig(); + + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("{\"field\":1}", BidType.video))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("{\"field\":1}", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + + private static Config givenConfig() { + return Config.of(true, AppVideoHtmlConfig.of(true, null)); + } + + @Test + public void applyShouldChangeTypeToBannerAndAddMetaTypeVideoWhenAdmIsVastXmlAndTypeIsNotVideo() { + // given + final Config givenConfig = givenConfig(); + + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.xNative))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + + @Test + public void applyShouldChangeTypeToBannerAndOverwriteMetaTypeToVideoWhenAdmIsNotVastXmlAndTypeIsVideo() { + // given + final Config givenConfig = givenConfig(); + + final ExtBidPrebid givenPrebid = ExtBidPrebid.builder() + .bidid("someId") + .meta(ExtBidPrebidMeta.builder().adapterCode("someCode").mediaType("banner").build()) + .build(); + final ObjectNode givenBidExt = MAPPER.valueToTree(ExtPrebid.of(givenPrebid, null)); + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.video, givenBidExt))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .bidid("someId") + .meta(ExtBidPrebidMeta.builder().adapterCode("someCode").mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java new file mode 100644 index 00000000000..0171f17cc04 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java @@ -0,0 +1,118 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +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.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.Map; + +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.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class ResponseCorrectionAllProcessedBidResponsesHookTest { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + @Mock + private CorrectionsProvider correctionsProvider; + + private ResponseCorrectionAllProcessedBidResponsesHook target; + + @Mock + private AllProcessedBidResponsesPayload payload; + + @Mock(strictness = Mock.Strictness.LENIENT) + private AuctionInvocationContext invocationContext; + + @BeforeEach + public void setUp() { + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.of(true, null))); + given(invocationContext.auctionContext()) + .willReturn(AuctionContext.builder().bidRequest(BidRequest.builder().build()).build()); + + target = new ResponseCorrectionAllProcessedBidResponsesHook(correctionsProvider, MAPPER); + } + + @Test + public void callShouldReturnFailedResultOnInvalidConfiguration() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Map.of("enabled", emptyList()))); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.failure); + assertThat(invocationResult.message()).startsWith("Cannot deserialize value of type"); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionOnDisabledConfig() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.of(false, null))); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionIfThereIsNoApplicableCorrections() { + // given + given(correctionsProvider.corrections(any(), any())).willReturn(emptyList()); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnUpdate() { + // given + final Correction correction = mock(Correction.class); + given(correctionsProvider.corrections(any(), any())).willReturn(singletonList(correction)); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + assertThat(invocationResult.payloadUpdate()).isNotNull(); + }); + } +} diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 64def14267f..f9c7a337e5d 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -21,6 +21,7 @@ confiant-ad-quality pb-richmedia-filter fiftyone-devicedetection + pb-response-correction diff --git a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy index eedb8412ba5..5efcdf40709 100644 --- a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy @@ -4,8 +4,9 @@ import com.fasterxml.jackson.annotation.JsonValue enum ModuleName { - PB_RICHMEDIA_FILTER('pb-richmedia-filter'), - ORTB2_BLOCKING('ortb2-blocking') + PB_RICHMEDIA_FILTER("pb-richmedia-filter"), + PB_RESPONSE_CORRECTION ("pb-response-correction"), + ORTB2_BLOCKING("ortb2-blocking") @JsonValue final String code diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy new file mode 100644 index 00000000000..6486e292ed5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AppVideoHtml { + + Boolean enabled + List excludedBidders +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy index 11173093f85..0d8333b3375 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy @@ -7,6 +7,7 @@ import org.prebid.server.functional.model.ModuleName enum ModuleHookImplementation { PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES("pb-richmedia-filter-all-processed-bid-responses-hook"), + RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES("pb-response-correction-all-processed-bid-responses-hook"), ORTB2_BLOCKING_BIDDER_REQUEST("ortb2-blocking-bidder-request"), ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response") diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy index fbbe08089fc..6b5b8f4adb0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy @@ -1,7 +1,5 @@ package org.prebid.server.functional.model.config -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy new file mode 100644 index 00000000000..46af75deac6 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class PbResponseCorrection { + + Boolean enabled + AppVideoHtml appVideoHtml +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy index 74a6ddab94d..f9121ae0b3a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy @@ -11,4 +11,5 @@ class PbsModulesConfig { RichmediaFilter pbRichmediaFilter Ortb2BlockingConfig ortb2Blocking + PbResponseCorrection pbResponseCorrection } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy index 4a342e602a4..19cb2cd53de 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy @@ -5,6 +5,7 @@ import org.prebid.server.functional.model.config.ExecutionPlan import org.prebid.server.functional.tests.BaseSpec import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES @@ -22,6 +23,12 @@ class ModuleBaseSpec extends BaseSpec { repository.removeAllDatabaseData() } + protected static Map getResponseCorrectionConfig(Endpoint endpoint = OPENRTB2_AUCTION) { + ["hooks.${PB_RESPONSE_CORRECTION.code}.enabled" : true, + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RESPONSE_CORRECTION, [ALL_PROCESSED_BID_RESPONSES]))] + .collectEntries { key, value -> [(key.toString()): value.toString()] } + } + protected static Map getRichMediaFilterSettings(String scriptPattern, boolean filterMraidEnabled = true, Endpoint endpoint = OPENRTB2_AUCTION) { diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/AnalyticsTagsModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy similarity index 99% rename from src/test/groovy/org/prebid/server/functional/tests/module/AnalyticsTagsModuleSpec.groovy rename to src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy index 511c2101eee..8a99628b70c 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/AnalyticsTagsModuleSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy @@ -1,4 +1,4 @@ -package org.prebid.server.functional.tests.module +package org.prebid.server.functional.tests.module.analyticstag import org.prebid.server.functional.model.config.AccountAnalyticsConfig import org.prebid.server.functional.model.config.AccountConfig @@ -16,6 +16,7 @@ import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ModuleActivityName import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy new file mode 100644 index 00000000000..2c6c4dfd146 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy @@ -0,0 +1,507 @@ +package org.prebid.server.functional.tests.module.requestcorrenction + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.AppVideoHtml +import org.prebid.server.functional.model.config.PbResponseCorrection +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.response.auction.Adm +import org.prebid.server.functional.model.response.auction.BidExt +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.Meta +import org.prebid.server.functional.model.response.auction.Prebid +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultBidRequest +import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultVideoRequest +import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO +import static org.prebid.server.functional.model.response.auction.MediaType.BANNER +import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO + +class ResponseCorrectionSpec extends ModuleBaseSpec { + + private final PrebidServerService pbsServiceWithResponseCorrectionModule = pbsServiceFactory.getService( + ["adapter-defaults.modifying-vast-xml-allowed": "false", + "adapters.generic.modifying-vast-xml-allowed": "false"] + + responseCorrectionConfig) + + def "PBS shouldn't modify response when in account correction module disabled"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(VIDEO) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest, responseCorrectionEnabled, appVideoHtmlEnabled) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + responseCorrectionEnabled | appVideoHtmlEnabled + false | true + true | false + false | false + } + + def "PBS shouldn't modify response with adm obj when request includes #distributionChannel distribution channel"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request video imp" + def bidRequest = getDefaultVideoRequest(distributionChannel) + + and: "Set bidder response with adm obj" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].adm = new Adm() + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + distributionChannel << [SITE, DOOH] + } + + def "PBS shouldn't modify response for excluded bidder when bidder specified in config"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(VIDEO) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module and excluded bidders" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest).tap { + config.hooks.modules.pbResponseCorrection.appVideoHtml.excludedBidders = [GENERIC] + } + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response and emit warning when requested video impression respond with adm without VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection[0].contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response without adm obj when request includes #mediaType media type"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [mediaType] + + and: "Response shouldn't contain media type for prebid meta" + assert !response?.seatbid?.bid?.ext?.prebid?.meta?.mediaType?.flatten()?.size() + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + mediaType << [BANNER, AUDIO] + } + + def "PBS shouldn't modify response and emit logs when requested impression with native and adm value is asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(NATIVE) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection[0].contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + assert responseCorrection.size() == 1 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [NATIVE] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response when requested video impression respond with empty adm"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(null) + seatbid[0].bid[0].nurl = PBSUtils.randomString + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response when requested video impression respond with adm VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase("<${PBSUtils.randomString}VAST${PBSUtils.randomString}")) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS should modify response when requested #mediaType impression respond with adm VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase("<${PBSUtils.randomString}VAST${PBSUtils.randomString}")) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should contain single seatBid with proper meta media type" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + mediaType << [BANNER, AUDIO, NATIVE] + } + + def "PBS shouldn't modify response meta.mediaType to video and emit logs when requested impression with video and adm obj with asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and audio imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + } + + and: "Response should contain seatBib" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain media type for prebid meta" + assert !response?.seatbid?.bid?.ext?.prebid?.meta?.mediaType?.flatten()?.size() + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS should modify meta.mediaType and type for original response and also emit logs when response contains native meta.mediaType and adm without asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(NATIVE) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].adm = new Adm() + seatbid[0].bid[0].ext = new BidExt(prebid: new Prebid(meta: new Meta(mediaType: NATIVE))) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 2 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic has a JSON ADM, but without assets" as String) + } + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should media type for prebid meta" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + private static Account accountConfigWithResponseCorrectionModule(BidRequest bidRequest, Boolean enabledResponseCorrection = true, Boolean enabledAppVideoHtml = true) { + def modulesConfig = new PbsModulesConfig(pbResponseCorrection: new PbResponseCorrection( + enabled: enabledResponseCorrection, appVideoHtml: new AppVideoHtml(enabled: enabledAppVideoHtml))) + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) + new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + } +} From dd5b29b86d3b73b2287143a6c5e087ac8a96f395 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Thu, 19 Sep 2024 08:56:49 +0200 Subject: [PATCH 8/9] Core: Add hb_env=amp for Amp Requests (#3433) --- .../server/auction/BidResponseCreator.java | 31 +++++++------ .../auction/TargetingKeywordsCreator.java | 18 +++----- .../functional/tests/TargetingSpec.groovy | 25 ++++++++++- .../auction/BidResponseCreatorTest.java | 43 ++++++++++++++++++- .../auction/TargetingKeywordsCreatorTest.java | 36 ++++++++-------- .../server/it/amp/test-amp-response.json | 3 ++ 6 files changed, 109 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index b843b7f07e4..567f73e63de 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -122,6 +122,8 @@ public class BidResponseCreator { private static final Integer MAX_TARGETING_KEY_LENGTH = 11; private static final String DEFAULT_TARGETING_KEY_PREFIX = "hb"; public static final String DEFAULT_DEBUG_KEY = "prebid"; + private static final String TARGETING_ENV_APP_VALUE = "mobile-app"; + private static final String TARGETING_ENV_AMP_VALUE = "amp"; private final CoreCacheService coreCacheService; private final BidderCatalog bidderCatalog; @@ -1325,13 +1327,11 @@ private Bid toBid(BidInfo bidInfo, final String cacheId = cacheInfo != null ? cacheInfo.getCacheId() : null; final String videoCacheId = cacheInfo != null ? cacheInfo.getVideoCacheId() : null; - final boolean isApp = bidRequest.getApp() != null; - final Map targetingKeywords; final String bidderCode = targetingInfo.getBidderCode(); if (shouldIncludeTargetingInResponse(targeting, bidInfo.getTargetingInfo())) { final TargetingKeywordsCreator keywordsCreator = resolveKeywordsCreator( - bidType, targeting, isApp, bidRequest, account, bidWarnings); + bidType, targeting, bidRequest, account, bidWarnings); final boolean isWinningBid = targetingInfo.isWinningBid(); final String categoryDuration = bidInfo.getCategory(); @@ -1552,16 +1552,15 @@ private Events createEvents(String bidder, private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { final Map keywordsCreatorByBidType = - keywordsCreatorByBidType(targeting, isApp, bidRequest, account, bidWarnings); + keywordsCreatorByBidType(targeting, bidRequest, account, bidWarnings); return keywordsCreatorByBidType.getOrDefault( - bidType, keywordsCreator(targeting, isApp, bidRequest, account, bidWarnings)); + bidType, keywordsCreator(targeting, bidRequest, account, bidWarnings)); } /** @@ -1569,7 +1568,6 @@ private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, * instance if it is present. */ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { @@ -1577,7 +1575,7 @@ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, final JsonNode priceGranularityNode = targeting.getPricegranularity(); return priceGranularityNode == null || priceGranularityNode.isNull() ? null - : createKeywordsCreator(targeting, isApp, priceGranularityNode, bidRequest, account, bidWarnings); + : createKeywordsCreator(targeting, priceGranularityNode, bidRequest, account, bidWarnings); } /** @@ -1586,7 +1584,6 @@ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, */ private Map keywordsCreatorByBidType( ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { @@ -1602,21 +1599,21 @@ private Map keywordsCreatorByBidType( final boolean isBannerNull = banner == null || banner.isNull(); if (!isBannerNull) { result.put( - BidType.banner, createKeywordsCreator(targeting, isApp, banner, bidRequest, account, bidWarnings)); + BidType.banner, createKeywordsCreator(targeting, banner, bidRequest, account, bidWarnings)); } final ObjectNode video = mediaTypePriceGranularity.getVideo(); final boolean isVideoNull = video == null || video.isNull(); if (!isVideoNull) { result.put( - BidType.video, createKeywordsCreator(targeting, isApp, video, bidRequest, account, bidWarnings)); + BidType.video, createKeywordsCreator(targeting, video, bidRequest, account, bidWarnings)); } final ObjectNode xNative = mediaTypePriceGranularity.getXNative(); final boolean isNativeNull = xNative == null || xNative.isNull(); if (!isNativeNull) { result.put( - BidType.xNative, createKeywordsCreator(targeting, isApp, xNative, bidRequest, account, bidWarnings) + BidType.xNative, createKeywordsCreator(targeting, xNative, bidRequest, account, bidWarnings) ); } @@ -1624,7 +1621,6 @@ BidType.xNative, createKeywordsCreator(targeting, isApp, xNative, bidRequest, ac } private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targeting, - boolean isApp, JsonNode priceGranularity, BidRequest bidRequest, Account account, @@ -1632,13 +1628,20 @@ private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targe final int resolvedTruncateAttrChars = resolveTruncateAttrChars(targeting, account); final String resolveKeyPrefix = resolveAndValidateKeyPrefix( bidRequest, account, resolvedTruncateAttrChars, bidWarnings); + + final String env = Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAmp) + .map(ignored -> TARGETING_ENV_AMP_VALUE) + .orElse(bidRequest.getApp() == null ? null : TARGETING_ENV_APP_VALUE); + return TargetingKeywordsCreator.create( parsePriceGranularity(priceGranularity), BooleanUtils.toBoolean(targeting.getIncludewinners()), BooleanUtils.toBoolean(targeting.getIncludebidderkeys()), BooleanUtils.toBoolean(targeting.getAlwaysincludedeals()), BooleanUtils.isTrue(targeting.getIncludeformat()), - isApp, + env, resolvedTruncateAttrChars, cacheHost, cachePath, diff --git a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java index 9472f734336..2896e153adf 100644 --- a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java +++ b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java @@ -32,10 +32,6 @@ public class TargetingKeywordsCreator { * It will exist only if the incoming bidRequest defiend request.app instead of request.site. */ private static final String ENV_KEY = "_env"; - /** - * Used as a value for ENV_KEY. - */ - private static final String ENV_APP_VALUE = "mobile-app"; /** * Name of the Bidder. For example, "appnexus" or "rubicon". */ @@ -87,7 +83,7 @@ public class TargetingKeywordsCreator { private final boolean includeBidderKeys; private final boolean alwaysIncludeDeals; private final boolean includeFormat; - private final boolean isApp; + private final String env; private final int truncateAttrChars; private final String cacheHost; private final String cachePath; @@ -99,7 +95,7 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, boolean includeBidderKeys, boolean alwaysIncludeDeals, boolean includeFormat, - boolean isApp, + String env, int truncateAttrChars, String cacheHost, String cachePath, @@ -111,7 +107,7 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, this.includeBidderKeys = includeBidderKeys; this.alwaysIncludeDeals = alwaysIncludeDeals; this.includeFormat = includeFormat; - this.isApp = isApp; + this.env = env; this.truncateAttrChars = truncateAttrChars; this.cacheHost = cacheHost; this.cachePath = cachePath; @@ -127,7 +123,7 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul boolean includeBidderKeys, boolean alwaysIncludeDeals, boolean includeFormat, - boolean isApp, + String env, int truncateAttrChars, String cacheHost, String cachePath, @@ -139,7 +135,7 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul includeBidderKeys, alwaysIncludeDeals, includeFormat, - isApp, + env, truncateAttrChars, cacheHost, cachePath, @@ -230,8 +226,8 @@ private Map makeFor(String bidder, if (StringUtils.isNotBlank(dealId)) { keywordMap.put(this.keyPrefix + DEAL_KEY, dealId); } - if (isApp) { - keywordMap.put(this.keyPrefix + ENV_KEY, ENV_APP_VALUE); + if (env != null) { + keywordMap.put(this.keyPrefix + ENV_KEY, env); } if (StringUtils.isNotBlank(categoryDuration)) { keywordMap.put(this.keyPrefix + CATEGORY_DURATION_KEY, categoryDuration); diff --git a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy index 6005a232262..67ff7907901 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy @@ -30,10 +30,12 @@ import static org.prebid.server.functional.testcontainers.Dependencies.getNetwor class TargetingSpec extends BaseSpec { private static final Integer TARGETING_PARAM_NAME_MAX_LENGTH = 20 + private static final Integer TARGETING_KEYS_SIZE = 14 private static final Integer MAX_AMP_TARGETING_TRUNCATION_LENGTH = 11 private static final String DEFAULT_TARGETING_PREFIX = "hb" private static final Integer TARGETING_PREFIX_LENGTH = 11 private static final Integer MAX_TRUNCATE_ATTR_CHARS = 255 + private static final String HB_ENV_AMP = "amp" def "PBS should include targeting bidder specific keys when alwaysIncludeDeals is true and deal bid wins"() { given: "Bid request with alwaysIncludeDeals = true" @@ -668,7 +670,7 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain default targeting prefix" def targeting = ampResponse.targeting - assert targeting.size() == 12 + assert targeting.size() == TARGETING_KEYS_SIZE assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } } @@ -694,7 +696,7 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain targeting response with custom prefix" def targeting = ampResponse.targeting - assert targeting.size() == 12 + assert targeting.size() == TARGETING_KEYS_SIZE assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } } @@ -1100,6 +1102,25 @@ class TargetingSpec extends BaseSpec { assert ampData.secondUnknownField == secondUnknownValue } + def "PBS amp should always send hb_env=amp when stored request does not contain app"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default bid request" + def ampStoredRequest = BidRequest.defaultBidRequest + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Amp response should contain amp hb_env" + def targeting = ampResponse.targeting + assert targeting["hb_env"] == HB_ENV_AMP + } + private static PrebidServerService getEnabledWinBidsPbsService() { pbsServiceFactory.getService(["auction.cache.only-winning-bids": "true"]) } diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 674a3fcf245..17de10e38b5 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -82,6 +82,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAdservertargetingRule; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAmp; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; @@ -1551,7 +1552,7 @@ public void shouldPopulateTargetingKeywords() { final AuctionContext auctionContext = givenAuctionContext( givenBidRequest( - identity(), + request -> request.app(App.builder().build()), extBuilder -> extBuilder.targeting(givenTargeting()), givenImp()), contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); @@ -1569,7 +1570,45 @@ public void shouldPopulateTargetingKeywords() { tuple("hb_pb", "5.00"), tuple("hb_pb_bidder1", "5.00"), tuple("hb_bidder", "bidder1"), - tuple("hb_bidder_bidder1", "bidder1")); + tuple("hb_bidder_bidder1", "bidder1"), + tuple("hb_env", "mobile-app"), + tuple("hb_env_bidder1", "mobile-app")); + + verify(coreCacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any()); + } + + @Test + public void shouldPopulateTargetingKeywordsForAmpRequest() { + // given + final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build(); + final List bidderResponses = singletonList(BidderResponse.of("bidder1", + givenSeatBid(BidderBid.of(bid, banner, "USD")), 100)); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest( + request -> request.app(App.builder().build()), + extBuilder -> extBuilder + .targeting(givenTargeting()) + .amp(ExtRequestPrebidAmp.of(Map.of("key", "value"))), + givenImp()), + contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); + + // when + final BidResponse bidResponse = target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); + + // then + assertThat(bidResponse.getSeatbid()) + .flatExtracting(SeatBid::getBid).hasSize(1) + .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getTargeting()) + .flatExtracting(Map::entrySet) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("hb_pb", "5.00"), + tuple("hb_pb_bidder1", "5.00"), + tuple("hb_bidder", "bidder1"), + tuple("hb_bidder_bidder1", "bidder1"), + tuple("hb_env", "amp"), + tuple("hb_env_bidder1", "amp")); verify(coreCacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any()); } diff --git a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java index f63b6d32772..879bd7873c2 100644 --- a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java @@ -39,7 +39,7 @@ public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() { true, false, false, - false, + null, 0, null, null, @@ -70,7 +70,7 @@ public void shouldReturnTargetingKeywordsWithEntireKeysOpenrtb() { true, false, false, - false, + null, 0, null, null, @@ -105,7 +105,7 @@ public void shouldReturnTargetingKeywordsForWinningBidOpenrtb() { true, false, true, - false, + null, 0, null, null, @@ -148,7 +148,7 @@ public void shouldIncludeFormatOpenrtb() { true, false, true, - false, + null, 0, null, null, @@ -174,7 +174,7 @@ public void shouldNotIncludeCacheIdAndDealIdAndSizeOpenrtb() { true, false, false, - false, + null, 0, null, null, @@ -201,7 +201,7 @@ public void shouldReturnEnvKeyForAppRequestOpenrtb() { true, false, false, - true, + "mobile-app", 0, null, null, @@ -229,7 +229,7 @@ public void shouldNotIncludeWinningBidTargetingIfIncludeWinnersFlagIsFalse() { true, false, false, - false, + null, 0, null, null, @@ -255,7 +255,7 @@ public void shouldIncludeWinningBidTargetingIfIncludeWinnersFlagIsTrue() { true, false, false, - false, + null, 0, null, null, @@ -281,7 +281,7 @@ public void shouldNotIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse() false, false, false, - false, + null, 0, null, null, @@ -307,7 +307,7 @@ public void shouldIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsTrue() { true, false, false, - false, + null, 0, null, null, @@ -333,7 +333,7 @@ public void shouldTruncateTargetingBidderKeywordsIfTruncateAttrCharsIsDefined() true, false, false, - false, + null, 20, null, null, @@ -360,7 +360,7 @@ public void shouldTruncateTargetingWithoutBidderSuffixKeywordsIfTruncateAttrChar false, false, false, - false, + null, 7, null, null, @@ -387,7 +387,7 @@ public void shouldTruncateTargetingAndDropDuplicatedWhenTruncateIsTooShort() { true, false, true, - true, + "mobile-app", 6, null, null, @@ -415,7 +415,7 @@ public void shouldNotTruncateTargetingKeywordsIfTruncateAttrCharsIsNotDefined() true, false, false, - false, + null, 0, null, null, @@ -448,7 +448,7 @@ public void shouldTruncateKeysFromResolver() { true, false, false, - false, + null, 20, null, null, @@ -480,7 +480,7 @@ public void shouldIncludeKeywordsFromResolver() { true, false, false, - false, + null, 0, null, null, @@ -506,7 +506,7 @@ public void shouldIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsTrue() { false, true, false, - false, + null, 0, null, null, @@ -532,7 +532,7 @@ public void shouldNotIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsFalse() { false, false, false, - false, + null, 0, null, null, diff --git a/src/test/resources/org/prebid/server/it/amp/test-amp-response.json b/src/test/resources/org/prebid/server/it/amp/test-amp-response.json index fcac32fb76b..92abd79998c 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-amp-response.json +++ b/src/test/resources/org/prebid/server/it/amp/test-amp-response.json @@ -12,6 +12,9 @@ "hb_cache_id": "fea00992-651c-44c8-b16a-b9af99fdf2dd", "hb_bidder_generic": "generic", "hb_size_genericAlias": "300x250", + "hb_env": "amp", + "hb_env_generic": "amp", + "hb_env_genericAlias": "amp", "hb_cache_host": "{{ cache.host }}", "hb_cache_path": "{{ cache.path }}", "hb_cache_host_generic": "{{ cache.host }}", From f588358e373d9ab669d75458ba56ed8deb9b26f8 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Thu, 19 Sep 2024 12:11:56 +0200 Subject: [PATCH 9/9] Tests: Temporary disable not-stable test (#3450) --- src/test/java/org/prebid/server/it/PriceFloorsTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/org/prebid/server/it/PriceFloorsTest.java b/src/test/java/org/prebid/server/it/PriceFloorsTest.java index b32be1580d2..3dd300524c0 100644 --- a/src/test/java/org/prebid/server/it/PriceFloorsTest.java +++ b/src/test/java/org/prebid/server/it/PriceFloorsTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.prebid.server.model.Endpoint; @@ -30,6 +31,8 @@ import static org.prebid.server.util.IntegrationTestsUtil.jsonFrom; import static org.prebid.server.util.IntegrationTestsUtil.responseFor; +// TODO: Investigate the root cause of unstable behavior in this class and remove the disabled state once resolved. +@Disabled @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @TestPropertySource( locations = {"test-application.properties"},