From 83aecd38d73a30a11ac6aa49fc03300a817ee10f Mon Sep 17 00:00:00 2001 From: Viktor Kryshtal Date: Mon, 1 Dec 2025 13:45:48 +0200 Subject: [PATCH 1/6] Add goldbach bidder --- .../bidder/goldbach/GoldbachBidder.java | 270 ++++++++ .../proto/ExtImpGoldbachBidRequest.java | 16 + .../goldbach/proto/ExtRequestGoldbach.java | 16 + .../bidder/goldbach/proto/GoldbachImpExt.java | 11 + .../ext/request/goldbach/ExtImpGoldbach.java | 20 + .../config/bidder/GoldbachConfiguration.java | 41 ++ .../resources/bidder-config/goldbach.yaml | 16 + .../static/bidder-params/goldbach.json | 38 ++ .../bidder/goldbach/GoldbachBidderTest.java | 601 ++++++++++++++++++ .../org/prebid/server/it/GoldbachTest.java | 36 ++ .../test-auction-goldbach-request.json | 23 + .../test-auction-goldbach-response.json | 40 ++ .../goldbach/test-goldbach-bid-request.json | 51 ++ .../goldbach/test-goldbach-bid-response.json | 23 + .../server/it/test-application.properties | 2 + 15 files changed, 1204 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java create mode 100644 src/main/java/org/prebid/server/bidder/goldbach/proto/ExtImpGoldbachBidRequest.java create mode 100644 src/main/java/org/prebid/server/bidder/goldbach/proto/ExtRequestGoldbach.java create mode 100644 src/main/java/org/prebid/server/bidder/goldbach/proto/GoldbachImpExt.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/goldbach/ExtImpGoldbach.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/GoldbachConfiguration.java create mode 100644 src/main/resources/bidder-config/goldbach.yaml create mode 100644 src/main/resources/static/bidder-params/goldbach.json create mode 100644 src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/GoldbachTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-auction-goldbach-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-auction-goldbach-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java b/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java new file mode 100644 index 00000000000..b637405ee3e --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java @@ -0,0 +1,270 @@ +package org.prebid.server.bidder.goldbach; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +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 io.netty.handler.codec.http.HttpResponseStatus; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.goldbach.proto.ExtImpGoldbachBidRequest; +import org.prebid.server.bidder.goldbach.proto.ExtRequestGoldbach; +import org.prebid.server.bidder.goldbach.proto.GoldbachImpExt; +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.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.goldbach.ExtImpGoldbach; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +public class GoldbachBidder implements Bidder { + + private static final TypeReference> GOLDBACH_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final TypeReference> EXT_PREBID_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public GoldbachBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + + final ExtRequestGoldbach extRequestGoldbach = parseRequestExt(request, errors); + final Map> publisherToImps = groupImpressionsByPublisherId(request.getImp(), errors); + final List> httpRequests = publisherToImps.entrySet().stream() + .map(publisherIdAndImps -> + makeHttpRequestForPublisher(request, + extRequestGoldbach, + publisherIdAndImps.getKey(), + publisherIdAndImps.getValue(), + errors)) + .filter(Objects::nonNull) + .toList(); + + return Result.of(httpRequests, errors); + } + + private ExtRequestGoldbach parseRequestExt(BidRequest bidRequest, List errors) { + return Optional.ofNullable(bidRequest.getExt()) + .map(extRequest -> extRequest.getProperty("goldbach")) + .map(extRequestGoldbachRaw -> parseRequestExtGoldbach(extRequestGoldbachRaw, errors)) + .orElse(null); + } + + private ExtRequestGoldbach parseRequestExtGoldbach(JsonNode extRequestGoldbachRaw, List errors) { + try { + return mapper.mapper().treeToValue(extRequestGoldbachRaw, ExtRequestGoldbach.class); + } catch (IllegalArgumentException | JsonProcessingException e) { + errors.add(BidderError.badInput("Failed to deserialize Goldbach bid request extension: " + e.getMessage())); + return null; + } + } + + private Map> groupImpressionsByPublisherId(List impressions, List errors) { + final Map> publisherToImps = new HashMap<>(); + for (final Imp imp : impressions) { + try { + final ExtImpGoldbach extImp = parseImpExt(imp); + final Imp updatedImp = modifyImp(imp, extImp); + + publisherToImps.computeIfAbsent(extImp.getPublisherId(), k -> new ArrayList<>()) + .add(updatedImp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (publisherToImps.isEmpty()) { + errors.add(BidderError.badInput("No valid impressions found")); + } + return publisherToImps; + } + + private ExtImpGoldbach parseImpExt(Imp imp) { + final ExtPrebid extImp; + try { + extImp = mapper.mapper().convertValue(imp.getExt(), GOLDBACH_EXT_TYPE_REFERENCE); + } catch (IllegalArgumentException e) { + throw new PreBidException("Failed to deserialize Goldbach imp extension: " + e.getMessage()); + } + + if (extImp == null) { + throw new PreBidException("imp.ext is missing"); + } + final ExtImpGoldbach extImpGoldbach = extImp.getBidder(); + + if (extImpGoldbach == null) { + throw new PreBidException("imp.ext.bidder is missing"); + } + + return extImpGoldbach; + } + + private Imp modifyImp(Imp imp, ExtImpGoldbach extImp) { + final GoldbachImpExt goldbachImpExt = GoldbachImpExt.of( + ExtImpGoldbachBidRequest.of(extImp.getSlotId(), extImp.getCustomTargeting())); + return imp.toBuilder() + .ext(mapper.mapper().valueToTree(goldbachImpExt)) + .build(); + } + + private HttpRequest makeHttpRequestForPublisher( + BidRequest bidRequest, + ExtRequestGoldbach extRequestGoldbach, + String publisherId, + List imps, + List errors) { + try { + final BidRequest modifiedBidRequest = modifyBidRequest(bidRequest, extRequestGoldbach, publisherId, imps); + return BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + return null; + } + } + + private BidRequest modifyBidRequest( + BidRequest bidRequest, + ExtRequestGoldbach extRequestGoldbach, + String publisherId, + List imps) { + final ExtRequest modifiedExtRequest = modifyExtRequest(bidRequest.getExt(), extRequestGoldbach, publisherId); + return bidRequest.toBuilder() + .id("%s_%s".formatted(bidRequest.getId(), publisherId)) + .imp(imps) + .ext(modifiedExtRequest) + .build(); + } + + private ExtRequest modifyExtRequest(ExtRequest extRequest, + ExtRequestGoldbach extRequestGoldbach, + String publisherId) { + final ExtRequestGoldbach modifiedExtRequestGoldbach = ExtRequestGoldbach.builder() + .publisherId(publisherId) + .mockResponse(extRequestGoldbach != null ? extRequestGoldbach.getMockResponse() : null) + .build(); + + final ExtRequest modifiedExtRequest = ExtRequest.empty(); + Optional.ofNullable(extRequest) + .map(ExtRequest::getProperties) + .ifPresent(modifiedExtRequest::addProperties); + modifiedExtRequest.addProperty("goldbach", serializeExtRequestGoldbach(modifiedExtRequestGoldbach)); + return modifiedExtRequest; + } + + private JsonNode serializeExtRequestGoldbach(ExtRequestGoldbach extRequestGoldbach) { + try { + return mapper.mapper().valueToTree(extRequestGoldbach); + } catch (IllegalArgumentException e) { + throw new PreBidException("Failed to serialize Goldbach bid request extension: " + e.getMessage()); + } + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + if (httpCall.getResponse().getStatusCode() != HttpResponseStatus.CREATED.code()) { + return Result.withError( + BidderError.badServerResponse( + "unexpected status code: %d. Run with request.debug = 1 for more info".formatted( + httpCall.getResponse().getStatusCode()))); + } + + final BidResponse bidResponse; + try { + bidResponse = decodeBodyToBidResponse(httpCall); + } catch (PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + + return bidsFromResponse(bidResponse); + } + + private BidResponse decodeBodyToBidResponse(BidderCall httpCall) { + try { + return mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + throw new PreBidException("Failed to parse response as BidResponse: " + e.getMessage()); + } + } + + private Result> bidsFromResponse(BidResponse bidResponse) { + final List errors = new ArrayList<>(); + final List bidderBids = Stream + .ofNullable(bidResponse) + .map(BidResponse::getSeatbid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> makeBid(bid, bidResponse, errors)) + .filter(Objects::nonNull) + .toList(); + + if (bidderBids.isEmpty()) { + errors.add(BidderError.badServerResponse("No valid bids found in response")); + } + + return Result.of(bidderBids, errors); + } + + private BidderBid makeBid(Bid bid, BidResponse bidResponse, List errors) { + try { + return BidderBid.of( + bid, + getBidType(bid), + bidResponse.getCur()); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + return null; + } + } + + private BidType getBidType(Bid bid) { + return Optional.ofNullable(bid.getExt()) + .map(this::parseBidExt) + .map(ExtPrebid::getPrebid) + .map(ExtBidPrebid::getType) + .orElseThrow(() -> new PreBidException("No media type for bid " + bid.getId())); + } + + private ExtPrebid parseBidExt(ObjectNode bidExt) { + try { + return mapper.mapper().convertValue(bidExt, EXT_PREBID_TYPE_REFERENCE); + } catch (IllegalArgumentException e) { + throw new PreBidException("Failed to deserialize ext for bid: " + e.getMessage()); + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtImpGoldbachBidRequest.java b/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtImpGoldbachBidRequest.java new file mode 100644 index 00000000000..c9e31294166 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtImpGoldbachBidRequest.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.goldbach.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Value(staticConstructor = "of") +public class ExtImpGoldbachBidRequest { + + @JsonProperty("slotId") + String slotId; + + Map> targetings; +} diff --git a/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtRequestGoldbach.java b/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtRequestGoldbach.java new file mode 100644 index 00000000000..6c18c7ac89a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtRequestGoldbach.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.goldbach.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Value(staticConstructor = "of") +@Builder(toBuilder = true) +public class ExtRequestGoldbach { + + @JsonProperty("publisherId") + String publisherId; + + @JsonProperty("mockResponse") + Boolean mockResponse; +} diff --git a/src/main/java/org/prebid/server/bidder/goldbach/proto/GoldbachImpExt.java b/src/main/java/org/prebid/server/bidder/goldbach/proto/GoldbachImpExt.java new file mode 100644 index 00000000000..06c0f40eecd --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/goldbach/proto/GoldbachImpExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.goldbach.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class GoldbachImpExt { + + @JsonProperty("goldbach") + ExtImpGoldbachBidRequest extImp; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/goldbach/ExtImpGoldbach.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/goldbach/ExtImpGoldbach.java new file mode 100644 index 00000000000..84d3c0cf615 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/goldbach/ExtImpGoldbach.java @@ -0,0 +1,20 @@ +package org.prebid.server.proto.openrtb.ext.request.goldbach; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Value(staticConstructor = "of") +public class ExtImpGoldbach { + + @JsonProperty("publisherId") + String publisherId; + + @JsonProperty("slotId") + String slotId; + + @JsonProperty("customTargeting") + Map> customTargeting; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GoldbachConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GoldbachConfiguration.java new file mode 100644 index 00000000000..e30eb3cbe70 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/GoldbachConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.goldbach.GoldbachBidder; +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/goldbach.yaml", factory = YamlPropertySourceFactory.class) +public class GoldbachConfiguration { + + private static final String BIDDER_NAME = "goldbach"; + + @Bean("goldbachConfigurationProperties") + @ConfigurationProperties("adapters.goldbach") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps goldbachBidderDeps(BidderConfigurationProperties goldbachConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(goldbachConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new GoldbachBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/goldbach.yaml b/src/main/resources/bidder-config/goldbach.yaml new file mode 100644 index 00000000000..d8982f564fa --- /dev/null +++ b/src/main/resources/bidder-config/goldbach.yaml @@ -0,0 +1,16 @@ +adapters: + goldbach: + endpoint: https://goldlayer-api.prod.gbads.net/openrtb/2.5/auction + endpoint-compression: gzip + meta-info: + maintainer-email: engineering@goldbach.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 580 diff --git a/src/main/resources/static/bidder-params/goldbach.json b/src/main/resources/static/bidder-params/goldbach.json new file mode 100644 index 00000000000..c93bbf7b600 --- /dev/null +++ b/src/main/resources/static/bidder-params/goldbach.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Goldbach Adapter Params", + "description": "A schema which validates params accepted by the Goldbach adapter", + + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "minLength": 1, + "description": "Publisher Environment ID" + }, + "slotId": { + "type": "string", + "minLength": 1, + "description": "Slot Id" + }, + "customTargeting": { + "type": "object", + "description": "Custom Targeting Parameters", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + }, + + "required": ["publisherId", "slotId"] +} diff --git a/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java b/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java new file mode 100644 index 00000000000..6492fdc1c51 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java @@ -0,0 +1,601 @@ +package org.prebid.server.bidder.goldbach; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.impl.headers.HeadersMultiMap; +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.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.goldbach.ExtImpGoldbach; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +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.assertj.core.api.Assertions.tuple; + +public class GoldbachBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/"; + + private final GoldbachBidder target = new GoldbachBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new GoldbachBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedRequestUrl() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> + assertThat(httpRequest.getUri()).isEqualTo(ENDPOINT_URL)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedRequestMethod() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> + assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.POST)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedRequestHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> + assertThat(httpRequest.getHeaders()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder( + tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE), + tuple( + HttpUtil.ACCEPT_HEADER.toString(), + HttpHeaderValues.APPLICATION_JSON.toString()))); + } + + @Test + public void makeHttpRequestsShouldExtendBidRequestIdWithPublisherId() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> + assertThat(httpRequest.getPayload().getId()).isEqualTo("testBidRequestId_testPublisherId")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfGoldbachBidRequestExtensionIsInvalid() { + // given + final ExtRequest extRequest = ExtRequest.empty(); + extRequest.addProperty("goldbach", TextNode.valueOf("Invalid request.ext")); + final BidRequest bidRequest = givenBidRequest( + request -> request.ext(extRequest), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1); + assertThat(result.getErrors()).hasSize(1).allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Failed to deserialize Goldbach bid request extension: "); + }); + } + + @Test + public void makeHttpRequestsShouldAddPublisherIdToGoldbachBidRequestExtension() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> + assertThat(httpRequest.getPayload().getExt().getProperties()) + .containsEntry("goldbach", mapper.createObjectNode().put("publisherId", "testPublisherId"))); + } + + @Test + public void makeHttpRequestsShouldPreserveMockResponseFieldInGoldbachBidRequestExtension() { + // given + final ExtRequest extRequest = ExtRequest.empty(); + extRequest.addProperty("goldbach", mapper.createObjectNode().put("mockResponse", true)); + final BidRequest bidRequest = givenBidRequest( + request -> request.ext(extRequest), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> + assertThat(httpRequest.getPayload().getExt().getProperties()) + .containsEntry("goldbach", mapper.createObjectNode() + .put("publisherId", "testPublisherId") + .put("mockResponse", true))); + } + + @Test + public void makeHttpRequestsShouldPreserveOtherBidRequestExtensions() { + // given + final ExtRequest extRequest = ExtRequest.empty(); + final JsonNode anotherExtension = TextNode.valueOf("anotherExtensionValue"); + extRequest.addProperty("anotherExtension", anotherExtension); + final BidRequest bidRequest = givenBidRequest( + builder -> builder.ext(extRequest), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> + assertThat(httpRequest.getPayload().getExt().getProperties()) + .containsEntry("anotherExtension", anotherExtension)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtIsMissing() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(null)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(2).containsExactlyInAnyOrder( + BidderError.badInput("imp.ext is missing"), + BidderError.badInput("No valid impressions found")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtBidderIsMissing() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(mapper.createObjectNode())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(2).containsExactlyInAnyOrder( + BidderError.badInput("imp.ext.bidder is missing"), + BidderError.badInput("No valid impressions found")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtBidderIsInvalid() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> + imp.ext(mapper.createObjectNode().put("bidder", "Invalid imp.ext.bidder"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(2).satisfiesExactlyInAnyOrder( + error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Failed to deserialize Goldbach imp extension: "); + }, + error -> + assertThat(error).isEqualTo(BidderError.badInput("No valid impressions found"))); + } + + @Test + public void makeHttpRequestsShouldReplaceImpExt() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> + assertThat(httpRequest.getPayload().getImp()).hasSize(1).allSatisfy(imp -> + assertThat(imp.getExt()).isEqualTo(mapper.createObjectNode().set( + "goldbach", + mapper.createObjectNode().put("slotId", "testSlotId"))))); + } + + @Test + public void makeHttpRequestsShouldCopyCustomTargetingToOutputImpExt() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext( + givenImpExt( + "testPublisherId", + "testSlotId", + Map.of("key", List.of("value1", "value2"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ObjectNode expectedImpExt = mapper.createObjectNode() + .set("goldbach", mapper.createObjectNode() + .put("slotId", "testSlotId") + .set("targetings", mapper.createObjectNode() + .set("key", mapper.createArrayNode().add("value1").add("value2")))); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> + assertThat(httpRequest.getPayload().getImp()).hasSize(1).allSatisfy(imp -> + assertThat(imp.getExt()).isEqualTo(expectedImpExt))); + } + + @Test + public void makeHttpRequestsShouldParseSingleStringAsArrayInCustomTargeting() { + // given + final ObjectNode impExt = mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .put("publisherId", "testPublisherId") + .put("slotId", "testSlotId") + .set("customTargeting", mapper.createObjectNode() + .put("key1", "value1") + .set("key2", mapper.createArrayNode().add("value2").add("value3")))); + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(impExt)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ObjectNode expectedImpExt = + mapper.createObjectNode() + .set("goldbach", mapper.createObjectNode() + .put("slotId", "testSlotId") + .set("targetings", mapper.createObjectNode() + .set("key1", mapper.createArrayNode().add("value1")) + .set("key2", mapper.createArrayNode().add("value2").add("value3")))); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> + assertThat(httpRequest.getPayload().getImp()).hasSize(1).allSatisfy(imp -> + assertThat(imp.getExt()).isEqualTo(expectedImpExt))); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfThereAreNoImpressions() { + // given + final BidRequest bidRequest = givenBidRequest(Collections.emptyList()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsExactly(BidderError.badInput("No valid impressions found")); + } + + @Test + public void makeHttpRequestsShouldGroupImpressionsByPublisherId() { + // given + final BidRequest bidRequest = givenBidRequest(List.of( + Imp.builder() + .id("imp1") + .ext(givenImpExt("publisherId1", "slot1")) + .build(), + Imp.builder() + .id("imp2") + .ext(givenImpExt("publisherId2", "slot2")) + .build(), + Imp.builder() + .id("imp3") + .ext(givenImpExt("publisherId1", "slot3")) + .build() + )); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2).satisfiesExactlyInAnyOrder( + request -> { + assertThat(request.getImpIds()).containsExactlyInAnyOrder("imp1", "imp3"); + assertThat(request.getPayload().getId()).isEqualTo("testBidRequestId_publisherId1"); + assertThat(request.getPayload().getImp()).extracting(Imp::getId) + .containsExactlyInAnyOrder("imp1", "imp3"); + }, + request -> { + assertThat(request.getImpIds()).containsExactlyInAnyOrder("imp2"); + assertThat(request.getPayload().getId()).isEqualTo("testBidRequestId_publisherId2"); + assertThat(request.getPayload().getImp()).extracting(Imp::getId) + .containsExactlyInAnyOrder("imp2"); + } + ); + } + + @Test + public void makeHttpRequestsShouldReturnErrorAndRequestWithOtherImpressionsIfThereAreImpressionsWithErrors() { + // given + final BidRequest bidRequest = givenBidRequest(List.of( + Imp.builder() + .id("imp1") + .ext(givenImpExt("publisherId1", "slot1")) + .build(), + Imp.builder() + .id("imp2") + .ext(givenImpExt("publisherId2", "slot2")) + .build(), + Imp.builder() + .id("invalidImp") + .ext(mapper.createObjectNode()) + .build() + )); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1).containsExactly(BidderError.badInput("imp.ext.bidder is missing")); + assertThat(result.getValue()).hasSize(2).satisfiesExactlyInAnyOrder( + request -> { + assertThat(request.getImpIds()).containsExactlyInAnyOrder("imp1"); + assertThat(request.getPayload().getId()).isEqualTo("testBidRequestId_publisherId1"); + assertThat(request.getPayload().getImp()).extracting(Imp::getId) + .containsExactlyInAnyOrder("imp1"); + }, + request -> { + assertThat(request.getImpIds()).containsExactlyInAnyOrder("imp2"); + assertThat(request.getPayload().getId()).isEqualTo("testBidRequestId_publisherId2"); + assertThat(request.getPayload().getImp()).extracting(Imp::getId) + .containsExactlyInAnyOrder("imp2"); + } + ); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseHasInvalidStatus() { + // given + final BidderCall httpCall = BidderCall.succeededHttp( + null, + HttpResponse.of(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), HeadersMultiMap.headers(), null), + null); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsExactly( + BidderError.badServerResponse("unexpected status code: 500. Run with request.debug = 1 for more info")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseHasInvalidBody() { + // given + final BidderCall httpCall = BidderCall.succeededHttp( + null, + HttpResponse.of(HttpResponseStatus.CREATED.code(), HeadersMultiMap.headers(), "\"Invalid body\""), + null); + + // 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 parse response as BidResponse: "); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfThereAreNoBids() { + // given + final BidderCall httpCall = givenHttpCall(); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("No valid bids found in response")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfBidHasInvalidExt() { + // given + final BidderCall httpCall = givenHttpCall( + Bid.builder().ext(mapper.createObjectNode().put("prebid", "Invalid")).build()); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(2).satisfiesExactlyInAnyOrder( + error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Failed to deserialize ext for bid: "); + }, + error -> assertThat(error) + .isEqualTo(BidderError.badServerResponse("No valid bids found in response")) + ); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfBidDoesntHaveMediaType() { + // given + final ObjectNode bidExt = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode()); + final BidderCall httpCall = givenHttpCall(Bid.builder().id("testBidId").ext(bidExt).build()); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsExactlyInAnyOrder( + BidderError.badInput("No media type for bid testBidId"), + BidderError.badServerResponse("No valid bids found in response") + ); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBidWithNoErrorsForValidInput() { + // given + final ObjectNode bidExt = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode().put("type", "banner")); + final Bid bid = Bid.builder().id("testBidId").ext(bidExt).build(); + final BidderCall httpCall = givenHttpCall(bid); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly( + BidderBid.of( + bid, + BidType.banner, + "USD" + ) + ); + } + + @Test + public void makeBidsShouldReturnValidBidsAndErrorsIfThereAreBothValidAndInvalidBidsInInput() { + // given + final ObjectNode validBidExt = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode().put("type", "banner")); + final ObjectNode invalidBidExt = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode()); + final Bid validBid = Bid.builder().id("validBidId").ext(validBidExt).build(); + final Bid invalidBid = Bid.builder().id("invalidBidId").ext(invalidBidExt).build(); + final BidderCall httpCall = givenHttpCall(validBid, invalidBid); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badInput("No media type for bid invalidBidId")); + assertThat(result.getValue()).containsExactly( + BidderBid.of( + validBid, + BidType.banner, + "USD" + ) + ); + } + + private static BidRequest givenBidRequest() { + return givenBidRequest(identity(), identity()); + } + + private static BidRequest givenBidRequest(List imps) { + return BidRequest.builder() + .id("testBidRequestId") + .imp(imps) + .build(); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator impCustomizer) { + + return bidRequestCustomizer.apply( + BidRequest.builder() + .id("testBidRequestId") + .imp(Collections.singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().ext(givenImpExt())) + .build(); + } + + private static ObjectNode givenImpExt() { + return givenImpExt("testPublisherId", "testSlotId", null); + } + + private static ObjectNode givenImpExt(String publisherId, String slotId) { + return givenImpExt(publisherId, slotId, null); + } + + private static ObjectNode givenImpExt(String publisherId, String + slotId, Map> customTargeting) { + return mapper.valueToTree(ExtPrebid.of( + null, + ExtImpGoldbach.of(publisherId, slotId, customTargeting))); + } + + private static BidderCall givenHttpCall(Bid... bids) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(HttpResponseStatus.CREATED.code(), null, givenBidResponse(bids)), + null); + } + + private static String givenBidResponse(Bid... bids) { + try { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } catch (JsonProcessingException e) { + throw new RuntimeException("Error encoding BidResponse to json: " + e); + } + } + +} diff --git a/src/test/java/org/prebid/server/it/GoldbachTest.java b/src/test/java/org/prebid/server/it/GoldbachTest.java new file mode 100644 index 00000000000..9b668a108af --- /dev/null +++ b/src/test/java/org/prebid/server/it/GoldbachTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.netty.handler.codec.http.HttpResponseStatus; +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 GoldbachTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromGoldbach() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/goldbach-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/goldbach/test-goldbach-bid-request.json"))) + .willReturn(aResponse() + .withStatus(HttpResponseStatus.CREATED.code()) + .withBody(jsonFrom("openrtb2/goldbach/test-goldbach-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/goldbach/test-auction-goldbach-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/goldbach/test-auction-goldbach-response.json", response, + singletonList("goldbach")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-auction-goldbach-request.json b/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-auction-goldbach-request.json new file mode 100644 index 00000000000..de9e0e18bb8 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-auction-goldbach-request.json @@ -0,0 +1,23 @@ +{ + "id": "723f8906-7f59-4f50-bba9-ed9115ab1663", + "imp": [ + { + "id": "test-imp-123", + "banner": { + "h": 250, + "w": 300, + "pos": 0 + }, + "ext": { + "goldbach": { + "slotId": "12345678/de-example.ch/slot-id/some-page", + "publisherId": "de-example.ch" + } + } + } + ], + "tmax": 5000, + "regs": { + "gdpr": 0 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-auction-goldbach-response.json b/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-auction-goldbach-response.json new file mode 100644 index 00000000000..5d9375becbc --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-auction-goldbach-response.json @@ -0,0 +1,40 @@ +{ + "id": "723f8906-7f59-4f50-bba9-ed9115ab1663", + "seatbid": [ + { + "bid": [ + { + "id": "b334cb75-41d8-4f61-801c-1e785a2fe38d", + "impid": "test-imp-123", + "price": 2.5, + "adm": "test-ad-content", + "adid": "456", + "cid": "1234", + "crid": "1234", + "exp": 300, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "goldbach" + } + }, + "origbidcpm": 2.5 + } + } + ], + "seat": "goldbach", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "goldbach": "{{ goldbach.response_time_ms }}" + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-request.json new file mode 100644 index 00000000000..e83c5c69787 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-request.json @@ -0,0 +1,51 @@ +{ + "id": "723f8906-7f59-4f50-bba9-ed9115ab1663_de-example.ch", + "imp": [ + { + "ext": { + "goldbach": { + "slotId": "12345678/de-example.ch/slot-id/some-page" + } + }, + "id": "test-imp-123", + "secure": 1, + "banner": { + "h": 250, + "w": 300, + "pos": 0 + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "goldbach": { + "publisherId": "de-example.ch" + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-response.json new file mode 100644 index 00000000000..495ff11aed0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-response.json @@ -0,0 +1,23 @@ +{ + "id": "c04f2011-87e9-4cde-9152-78fa2a63078a", + "seatbid": [ + { + "bid": [ + { + "id": "b334cb75-41d8-4f61-801c-1e785a2fe38d", + "price": 2.5, + "adm": "test-ad-content", + "adid": "456", + "impid": "test-imp-123", + "crid": "1234", + "cid": "1234", + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] +} 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 1a073dcda06..c2b79789b3b 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -275,6 +275,8 @@ adapters.gamoshi.enabled=true adapters.gamoshi.endpoint=http://localhost:8090/gamoshi-exchange adapters.globalsun.enabled=true adapters.globalsun.endpoint=http://localhost:8090/globalsun-exchange +adapters.goldbach.enabled=true +adapters.goldbach.endpoint=http://localhost:8090/goldbach-exchange adapters.gothamads.enabled=true adapters.gothamads.endpoint=http://localhost:8090/gothamads-exchange adapters.gothamads.aliases.intenze.enabled=true From 98f0e8b83800bd5210add70698bae0e7d0b5e4da Mon Sep 17 00:00:00 2001 From: Viktor Kryshtal Date: Mon, 1 Dec 2025 15:55:14 +0200 Subject: [PATCH 2/6] Resolve comments --- .../bidder/goldbach/GoldbachBidder.java | 109 +++---- ...achBidRequest.java => GoldbachExtImp.java} | 2 +- .../bidder/goldbach/proto/GoldbachImpExt.java | 11 - .../static/bidder-params/goldbach.json | 7 +- .../bidder/goldbach/GoldbachBidderTest.java | 274 +++++++----------- 5 files changed, 159 insertions(+), 244 deletions(-) rename src/main/java/org/prebid/server/bidder/goldbach/proto/{ExtImpGoldbachBidRequest.java => GoldbachExtImp.java} (88%) delete mode 100644 src/main/java/org/prebid/server/bidder/goldbach/proto/GoldbachImpExt.java diff --git a/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java b/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java index b637405ee3e..179e1076093 100644 --- a/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java +++ b/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java @@ -11,9 +11,8 @@ import com.iab.openrtb.response.SeatBid; import io.netty.handler.codec.http.HttpResponseStatus; import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.goldbach.proto.ExtImpGoldbachBidRequest; +import org.prebid.server.bidder.goldbach.proto.GoldbachExtImp; import org.prebid.server.bidder.goldbach.proto.ExtRequestGoldbach; -import org.prebid.server.bidder.goldbach.proto.GoldbachImpExt; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -62,7 +61,23 @@ public Result>> makeHttpRequests(BidRequest request final List errors = new ArrayList<>(); final ExtRequestGoldbach extRequestGoldbach = parseRequestExt(request, errors); - final Map> publisherToImps = groupImpressionsByPublisherId(request.getImp(), errors); + final Map> publisherToImps = new HashMap<>(); + for (Imp imp : request.getImp()) { + try { + final ExtImpGoldbach extImp = parseImpExt(imp); + final Imp updatedImp = modifyImp(imp, extImp); + + publisherToImps.computeIfAbsent(extImp.getPublisherId(), k -> new ArrayList<>()) + .add(updatedImp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (publisherToImps.isEmpty()) { + errors.add(BidderError.badInput("No valid impressions found")); + } + final List> httpRequests = publisherToImps.entrySet().stream() .map(publisherIdAndImps -> makeHttpRequestForPublisher(request, @@ -92,60 +107,27 @@ private ExtRequestGoldbach parseRequestExtGoldbach(JsonNode extRequestGoldbachRa } } - private Map> groupImpressionsByPublisherId(List impressions, List errors) { - final Map> publisherToImps = new HashMap<>(); - for (final Imp imp : impressions) { - try { - final ExtImpGoldbach extImp = parseImpExt(imp); - final Imp updatedImp = modifyImp(imp, extImp); - - publisherToImps.computeIfAbsent(extImp.getPublisherId(), k -> new ArrayList<>()) - .add(updatedImp); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - } - } - - if (publisherToImps.isEmpty()) { - errors.add(BidderError.badInput("No valid impressions found")); - } - return publisherToImps; - } - private ExtImpGoldbach parseImpExt(Imp imp) { - final ExtPrebid extImp; try { - extImp = mapper.mapper().convertValue(imp.getExt(), GOLDBACH_EXT_TYPE_REFERENCE); + return mapper.mapper().convertValue(imp.getExt(), GOLDBACH_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { throw new PreBidException("Failed to deserialize Goldbach imp extension: " + e.getMessage()); } - - if (extImp == null) { - throw new PreBidException("imp.ext is missing"); - } - final ExtImpGoldbach extImpGoldbach = extImp.getBidder(); - - if (extImpGoldbach == null) { - throw new PreBidException("imp.ext.bidder is missing"); - } - - return extImpGoldbach; } private Imp modifyImp(Imp imp, ExtImpGoldbach extImp) { - final GoldbachImpExt goldbachImpExt = GoldbachImpExt.of( - ExtImpGoldbachBidRequest.of(extImp.getSlotId(), extImp.getCustomTargeting())); return imp.toBuilder() - .ext(mapper.mapper().valueToTree(goldbachImpExt)) + .ext(mapper.mapper().createObjectNode().set("goldbach", mapper.mapper().valueToTree( + GoldbachExtImp.of(extImp.getSlotId(), extImp.getCustomTargeting())))) .build(); } - private HttpRequest makeHttpRequestForPublisher( - BidRequest bidRequest, - ExtRequestGoldbach extRequestGoldbach, - String publisherId, - List imps, - List errors) { + private HttpRequest makeHttpRequestForPublisher(BidRequest bidRequest, + ExtRequestGoldbach extRequestGoldbach, + String publisherId, + List imps, + List errors) { + try { final BidRequest modifiedBidRequest = modifyBidRequest(bidRequest, extRequestGoldbach, publisherId, imps); return BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper); @@ -155,11 +137,11 @@ private HttpRequest makeHttpRequestForPublisher( } } - private BidRequest modifyBidRequest( - BidRequest bidRequest, - ExtRequestGoldbach extRequestGoldbach, - String publisherId, - List imps) { + private BidRequest modifyBidRequest(BidRequest bidRequest, + ExtRequestGoldbach extRequestGoldbach, + String publisherId, + List imps) { + final ExtRequest modifiedExtRequest = modifyExtRequest(bidRequest.getExt(), extRequestGoldbach, publisherId); return bidRequest.toBuilder() .id("%s_%s".formatted(bidRequest.getId(), publisherId)) @@ -171,25 +153,23 @@ private BidRequest modifyBidRequest( private ExtRequest modifyExtRequest(ExtRequest extRequest, ExtRequestGoldbach extRequestGoldbach, String publisherId) { + final ExtRequestGoldbach modifiedExtRequestGoldbach = ExtRequestGoldbach.builder() .publisherId(publisherId) .mockResponse(extRequestGoldbach != null ? extRequestGoldbach.getMockResponse() : null) .build(); - final ExtRequest modifiedExtRequest = ExtRequest.empty(); - Optional.ofNullable(extRequest) - .map(ExtRequest::getProperties) - .ifPresent(modifiedExtRequest::addProperties); - modifiedExtRequest.addProperty("goldbach", serializeExtRequestGoldbach(modifiedExtRequestGoldbach)); - return modifiedExtRequest; - } + final ExtRequest modifiedExtRequest; - private JsonNode serializeExtRequestGoldbach(ExtRequestGoldbach extRequestGoldbach) { - try { - return mapper.mapper().valueToTree(extRequestGoldbach); - } catch (IllegalArgumentException e) { - throw new PreBidException("Failed to serialize Goldbach bid request extension: " + e.getMessage()); + if (extRequest != null) { + modifiedExtRequest = ExtRequest.of(extRequest.getPrebid()); + mapper.fillExtension(modifiedExtRequest, extRequest); + } else { + modifiedExtRequest = ExtRequest.empty(); } + + modifiedExtRequest.addProperty("goldbach", mapper.mapper().valueToTree(modifiedExtRequestGoldbach)); + return modifiedExtRequest; } @Override @@ -221,14 +201,15 @@ private BidResponse decodeBodyToBidResponse(BidderCall httpCall) { private Result> bidsFromResponse(BidResponse bidResponse) { final List errors = new ArrayList<>(); - final List bidderBids = Stream - .ofNullable(bidResponse) + final List bidderBids = Stream.ofNullable(bidResponse) .map(BidResponse::getSeatbid) .filter(Objects::nonNull) .flatMap(Collection::stream) + .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) + .filter(Objects::nonNull) .map(bid -> makeBid(bid, bidResponse, errors)) .filter(Objects::nonNull) .toList(); diff --git a/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtImpGoldbachBidRequest.java b/src/main/java/org/prebid/server/bidder/goldbach/proto/GoldbachExtImp.java similarity index 88% rename from src/main/java/org/prebid/server/bidder/goldbach/proto/ExtImpGoldbachBidRequest.java rename to src/main/java/org/prebid/server/bidder/goldbach/proto/GoldbachExtImp.java index c9e31294166..1d8d6abb9e9 100644 --- a/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtImpGoldbachBidRequest.java +++ b/src/main/java/org/prebid/server/bidder/goldbach/proto/GoldbachExtImp.java @@ -7,7 +7,7 @@ import java.util.Map; @Value(staticConstructor = "of") -public class ExtImpGoldbachBidRequest { +public class GoldbachExtImp { @JsonProperty("slotId") String slotId; diff --git a/src/main/java/org/prebid/server/bidder/goldbach/proto/GoldbachImpExt.java b/src/main/java/org/prebid/server/bidder/goldbach/proto/GoldbachImpExt.java deleted file mode 100644 index 06c0f40eecd..00000000000 --- a/src/main/java/org/prebid/server/bidder/goldbach/proto/GoldbachImpExt.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.goldbach.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Value; - -@Value(staticConstructor = "of") -public class GoldbachImpExt { - - @JsonProperty("goldbach") - ExtImpGoldbachBidRequest extImp; -} diff --git a/src/main/resources/static/bidder-params/goldbach.json b/src/main/resources/static/bidder-params/goldbach.json index c93bbf7b600..0a7c2a4bfac 100644 --- a/src/main/resources/static/bidder-params/goldbach.json +++ b/src/main/resources/static/bidder-params/goldbach.json @@ -2,7 +2,6 @@ "$schema": "http://json-schema.org/draft-04/schema#", "title": "Goldbach Adapter Params", "description": "A schema which validates params accepted by the Goldbach adapter", - "type": "object", "properties": { "publisherId": { @@ -33,6 +32,8 @@ } } }, - - "required": ["publisherId", "slotId"] + "required": [ + "publisherId", + "slotId" + ] } diff --git a/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java b/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java index 6492fdc1c51..097aeb8d936 100644 --- a/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java @@ -11,6 +11,7 @@ import com.iab.openrtb.response.SeatBid; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.impl.headers.HeadersMultiMap; import org.junit.jupiter.api.Test; @@ -27,7 +28,6 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.UnaryOperator; @@ -52,63 +52,68 @@ public void creationShouldFailOnInvalidEndpointUrl() { @Test public void makeHttpRequestsShouldReturnExpectedRequestUrl() { // given - final BidRequest bidRequest = givenBidRequest(); + final BidRequest bidRequest = givenBidRequest(givenImp()); // when final Result>> result = target.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> - assertThat(httpRequest.getUri()).isEqualTo(ENDPOINT_URL)); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly(ENDPOINT_URL); } @Test public void makeHttpRequestsShouldReturnExpectedRequestMethod() { // given - final BidRequest bidRequest = givenBidRequest(); + final BidRequest bidRequest = givenBidRequest(givenImp()); // when final Result>> result = target.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> - assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.POST)); + assertThat(result.getValue()) + .extracting(HttpRequest::getMethod) + .containsExactly(HttpMethod.POST); } @Test public void makeHttpRequestsShouldReturnExpectedRequestHeaders() { // given - final BidRequest bidRequest = givenBidRequest(); + final BidRequest bidRequest = givenBidRequest(givenImp()); // when final Result>> result = target.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> - assertThat(httpRequest.getHeaders()) - .extracting(Map.Entry::getKey, Map.Entry::getValue) - .containsExactlyInAnyOrder( - tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE), - tuple( - HttpUtil.ACCEPT_HEADER.toString(), - HttpHeaderValues.APPLICATION_JSON.toString()))); + assertThat(result.getValue()) + .extracting(HttpRequest::getHeaders) + .flatExtracting(MultiMap::entries) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder( + tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE), + tuple( + HttpUtil.ACCEPT_HEADER.toString(), + HttpHeaderValues.APPLICATION_JSON.toString())); } @Test public void makeHttpRequestsShouldExtendBidRequestIdWithPublisherId() { // given - final BidRequest bidRequest = givenBidRequest(); + final BidRequest bidRequest = givenBidRequest(givenImp()); // when final Result>> result = target.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> - assertThat(httpRequest.getPayload().getId()).isEqualTo("testBidRequestId_testPublisherId")); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getId) + .containsExactly("testBidRequestId_testPublisherId"); } @Test @@ -116,9 +121,7 @@ public void makeHttpRequestsShouldReturnErrorIfGoldbachBidRequestExtensionIsInva // given final ExtRequest extRequest = ExtRequest.empty(); extRequest.addProperty("goldbach", TextNode.valueOf("Invalid request.ext")); - final BidRequest bidRequest = givenBidRequest( - request -> request.ext(extRequest), - identity()); + final BidRequest bidRequest = givenBidRequest(request -> request.ext(extRequest), givenImp()); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -134,16 +137,16 @@ public void makeHttpRequestsShouldReturnErrorIfGoldbachBidRequestExtensionIsInva @Test public void makeHttpRequestsShouldAddPublisherIdToGoldbachBidRequestExtension() { // given - final BidRequest bidRequest = givenBidRequest(); + final BidRequest bidRequest = givenBidRequest(givenImp()); // when final Result>> result = target.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> - assertThat(httpRequest.getPayload().getExt().getProperties()) - .containsEntry("goldbach", mapper.createObjectNode().put("publisherId", "testPublisherId"))); + assertThat(result.getValue()) + .extracting(httpRequest -> httpRequest.getPayload().getExt().getProperties().get("goldbach")) + .containsExactly(mapper.createObjectNode().put("publisherId", "testPublisherId")); } @Test @@ -151,20 +154,18 @@ public void makeHttpRequestsShouldPreserveMockResponseFieldInGoldbachBidRequestE // given final ExtRequest extRequest = ExtRequest.empty(); extRequest.addProperty("goldbach", mapper.createObjectNode().put("mockResponse", true)); - final BidRequest bidRequest = givenBidRequest( - request -> request.ext(extRequest), - identity()); + final BidRequest bidRequest = givenBidRequest(request -> request.ext(extRequest), givenImp()); // when final Result>> result = target.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> - assertThat(httpRequest.getPayload().getExt().getProperties()) - .containsEntry("goldbach", mapper.createObjectNode() - .put("publisherId", "testPublisherId") - .put("mockResponse", true))); + assertThat(result.getValue()) + .extracting(httpRequest -> httpRequest.getPayload().getExt().getProperties().get("goldbach")) + .containsExactly(mapper.createObjectNode() + .put("publisherId", "testPublisherId") + .put("mockResponse", true)); } @Test @@ -173,55 +174,23 @@ public void makeHttpRequestsShouldPreserveOtherBidRequestExtensions() { final ExtRequest extRequest = ExtRequest.empty(); final JsonNode anotherExtension = TextNode.valueOf("anotherExtensionValue"); extRequest.addProperty("anotherExtension", anotherExtension); - final BidRequest bidRequest = givenBidRequest( - builder -> builder.ext(extRequest), - identity()); + final BidRequest bidRequest = givenBidRequest(builder -> builder.ext(extRequest), givenImp()); // when final Result>> result = target.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> - assertThat(httpRequest.getPayload().getExt().getProperties()) - .containsEntry("anotherExtension", anotherExtension)); - } - - @Test - public void makeHttpRequestsShouldReturnErrorIfImpExtIsMissing() { - // given - final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(null)); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).hasSize(2).containsExactlyInAnyOrder( - BidderError.badInput("imp.ext is missing"), - BidderError.badInput("No valid impressions found")); - } - - @Test - public void makeHttpRequestsShouldReturnErrorIfImpExtBidderIsMissing() { - // given - final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(mapper.createObjectNode())); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).hasSize(2).containsExactlyInAnyOrder( - BidderError.badInput("imp.ext.bidder is missing"), - BidderError.badInput("No valid impressions found")); + assertThat(result.getValue()) + .extracting(httpRequest -> httpRequest.getPayload().getExt().getProperties().get("anotherExtension")) + .containsExactly(anotherExtension); } @Test public void makeHttpRequestsShouldReturnErrorIfImpExtBidderIsInvalid() { // given - final BidRequest bidRequest = givenBidRequest(imp -> - imp.ext(mapper.createObjectNode().put("bidder", "Invalid imp.ext.bidder"))); + final BidRequest bidRequest = givenBidRequest(givenImp( + mapper.createObjectNode().put("bidder", "Invalid imp.ext.bidder"))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -240,77 +209,78 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtBidderIsInvalid() { @Test public void makeHttpRequestsShouldReplaceImpExt() { // given - final BidRequest bidRequest = givenBidRequest(); + final BidRequest bidRequest = givenBidRequest(givenImp()); // when final Result>> result = target.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> - assertThat(httpRequest.getPayload().getImp()).hasSize(1).allSatisfy(imp -> - assertThat(imp.getExt()).isEqualTo(mapper.createObjectNode().set( - "goldbach", - mapper.createObjectNode().put("slotId", "testSlotId"))))); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(mapper.createObjectNode().set( + "goldbach", + mapper.createObjectNode().put("slotId", "testSlotId"))); } @Test public void makeHttpRequestsShouldCopyCustomTargetingToOutputImpExt() { // given - final BidRequest bidRequest = givenBidRequest(imp -> imp.ext( - givenImpExt( - "testPublisherId", - "testSlotId", - Map.of("key", List.of("value1", "value2"))))); + final BidRequest bidRequest = givenBidRequest(givenImp(givenImpExt( + "testPublisherId", + "testSlotId", + Map.of("key", List.of("value1", "value2"))))); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final ObjectNode expectedImpExt = mapper.createObjectNode() - .set("goldbach", mapper.createObjectNode() - .put("slotId", "testSlotId") - .set("targetings", mapper.createObjectNode() - .set("key", mapper.createArrayNode().add("value1").add("value2")))); + final ObjectNode expectedImpExt = mapper.valueToTree(Map.of("goldbach", Map.of( + "slotId", "testSlotId", + "targetings", Map.of( + "key", List.of("value1", "value2"))))); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> - assertThat(httpRequest.getPayload().getImp()).hasSize(1).allSatisfy(imp -> - assertThat(imp.getExt()).isEqualTo(expectedImpExt))); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(expectedImpExt); } @Test public void makeHttpRequestsShouldParseSingleStringAsArrayInCustomTargeting() { // given - final ObjectNode impExt = mapper.createObjectNode() - .set("bidder", mapper.createObjectNode() - .put("publisherId", "testPublisherId") - .put("slotId", "testSlotId") - .set("customTargeting", mapper.createObjectNode() - .put("key1", "value1") - .set("key2", mapper.createArrayNode().add("value2").add("value3")))); - final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(impExt)); + final ObjectNode impExt = mapper.valueToTree(Map.of("bidder", Map.of( + "publisherId", "testPublisherId", + "slotId", "testSlotId", + "customTargeting", Map.of( + "key1", "value1", + "key2", List.of("value2", "value3"))))); + final BidRequest bidRequest = givenBidRequest(givenImp(impExt)); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final ObjectNode expectedImpExt = - mapper.createObjectNode() - .set("goldbach", mapper.createObjectNode() - .put("slotId", "testSlotId") - .set("targetings", mapper.createObjectNode() - .set("key1", mapper.createArrayNode().add("value1")) - .set("key2", mapper.createArrayNode().add("value2").add("value3")))); + final ObjectNode expectedImpExt = mapper.valueToTree(Map.of("goldbach", Map.of( + "slotId", "testSlotId", + "targetings", Map.of( + "key1", List.of("value1"), + "key2", List.of("value2", "value3"))))); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).allSatisfy(httpRequest -> - assertThat(httpRequest.getPayload().getImp()).hasSize(1).allSatisfy(imp -> - assertThat(imp.getExt()).isEqualTo(expectedImpExt))); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(expectedImpExt); } @Test public void makeHttpRequestsShouldReturnErrorIfThereAreNoImpressions() { // given - final BidRequest bidRequest = givenBidRequest(Collections.emptyList()); + final BidRequest bidRequest = givenBidRequest(); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -323,20 +293,10 @@ public void makeHttpRequestsShouldReturnErrorIfThereAreNoImpressions() { @Test public void makeHttpRequestsShouldGroupImpressionsByPublisherId() { // given - final BidRequest bidRequest = givenBidRequest(List.of( - Imp.builder() - .id("imp1") - .ext(givenImpExt("publisherId1", "slot1")) - .build(), - Imp.builder() - .id("imp2") - .ext(givenImpExt("publisherId2", "slot2")) - .build(), - Imp.builder() - .id("imp3") - .ext(givenImpExt("publisherId1", "slot3")) - .build() - )); + final BidRequest bidRequest = givenBidRequest( + givenImp("imp1", givenImpExt("publisherId1", "slot1")), + givenImp("imp2", givenImpExt("publisherId2", "slot2")), + givenImp("imp3", givenImpExt("publisherId1", "slot3"))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -347,14 +307,10 @@ public void makeHttpRequestsShouldGroupImpressionsByPublisherId() { request -> { assertThat(request.getImpIds()).containsExactlyInAnyOrder("imp1", "imp3"); assertThat(request.getPayload().getId()).isEqualTo("testBidRequestId_publisherId1"); - assertThat(request.getPayload().getImp()).extracting(Imp::getId) - .containsExactlyInAnyOrder("imp1", "imp3"); }, request -> { assertThat(request.getImpIds()).containsExactlyInAnyOrder("imp2"); assertThat(request.getPayload().getId()).isEqualTo("testBidRequestId_publisherId2"); - assertThat(request.getPayload().getImp()).extracting(Imp::getId) - .containsExactlyInAnyOrder("imp2"); } ); } @@ -362,38 +318,24 @@ public void makeHttpRequestsShouldGroupImpressionsByPublisherId() { @Test public void makeHttpRequestsShouldReturnErrorAndRequestWithOtherImpressionsIfThereAreImpressionsWithErrors() { // given - final BidRequest bidRequest = givenBidRequest(List.of( - Imp.builder() - .id("imp1") - .ext(givenImpExt("publisherId1", "slot1")) - .build(), - Imp.builder() - .id("imp2") - .ext(givenImpExt("publisherId2", "slot2")) - .build(), - Imp.builder() - .id("invalidImp") - .ext(mapper.createObjectNode()) - .build() - )); + final BidRequest bidRequest = givenBidRequest( + givenImp("imp1", givenImpExt("publisherId1", "slot1")), + givenImp("imp2", givenImpExt("publisherId2", "slot2")), + givenImp("invalidImp", mapper.createObjectNode().put("bidder", "Invalid imp.ext.bidder"))); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1).containsExactly(BidderError.badInput("imp.ext.bidder is missing")); + assertThat(result.getErrors()).hasSize(1); assertThat(result.getValue()).hasSize(2).satisfiesExactlyInAnyOrder( request -> { assertThat(request.getImpIds()).containsExactlyInAnyOrder("imp1"); assertThat(request.getPayload().getId()).isEqualTo("testBidRequestId_publisherId1"); - assertThat(request.getPayload().getImp()).extracting(Imp::getId) - .containsExactlyInAnyOrder("imp1"); }, request -> { assertThat(request.getImpIds()).containsExactlyInAnyOrder("imp2"); assertThat(request.getPayload().getId()).isEqualTo("testBidRequestId_publisherId2"); - assertThat(request.getPayload().getImp()).extracting(Imp::getId) - .containsExactlyInAnyOrder("imp2"); } ); } @@ -535,33 +477,35 @@ public void makeBidsShouldReturnValidBidsAndErrorsIfThereAreBothValidAndInvalidB } private static BidRequest givenBidRequest() { - return givenBidRequest(identity(), identity()); + return givenBidRequest(identity()); } - private static BidRequest givenBidRequest(List imps) { - return BidRequest.builder() - .id("testBidRequestId") - .imp(imps) - .build(); + private static BidRequest givenBidRequest(Imp... imps) { + return givenBidRequest(identity(), imps); } - private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { - return givenBidRequest(identity(), impCustomizer); + private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer, + Imp... imps) { + + return bidRequestCustomizer + .apply(BidRequest.builder() + .id("testBidRequestId") + .imp(List.of(imps))) + .build(); } - private static BidRequest givenBidRequest( - UnaryOperator bidRequestCustomizer, - UnaryOperator impCustomizer) { + private static Imp givenImp() { + return givenImp(givenImpExt()); + } - return bidRequestCustomizer.apply( - BidRequest.builder() - .id("testBidRequestId") - .imp(Collections.singletonList(givenImp(impCustomizer)))) - .build(); + private static Imp givenImp(ObjectNode impExt) { + return givenImp(null, impExt); } - private static Imp givenImp(UnaryOperator impCustomizer) { - return impCustomizer.apply(Imp.builder().ext(givenImpExt())) + private static Imp givenImp(String impId, ObjectNode impExt) { + return Imp.builder() + .id(impId) + .ext(impExt) .build(); } From 3fa01fdd81c29610e33ba06f8fb790e09c519ef9 Mon Sep 17 00:00:00 2001 From: Viktor Kryshtal Date: Tue, 2 Dec 2025 10:27:55 +0200 Subject: [PATCH 3/6] Resolve comments --- .../bidder/goldbach/GoldbachBidder.java | 28 ++++++------------- .../config/bidder/GoldbachConfiguration.java | 4 +-- .../bidder/goldbach/GoldbachBidderTest.java | 8 +----- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java b/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java index 179e1076093..58eb41a2e76 100644 --- a/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java +++ b/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java @@ -79,12 +79,11 @@ public Result>> makeHttpRequests(BidRequest request } final List> httpRequests = publisherToImps.entrySet().stream() - .map(publisherIdAndImps -> - makeHttpRequestForPublisher(request, - extRequestGoldbach, - publisherIdAndImps.getKey(), - publisherIdAndImps.getValue(), - errors)) + .map(publisherIdAndImps -> makeHttpRequestForPublisher( + request, + extRequestGoldbach, + publisherIdAndImps.getKey(), + publisherIdAndImps.getValue())) .filter(Objects::nonNull) .toList(); @@ -125,16 +124,10 @@ private Imp modifyImp(Imp imp, ExtImpGoldbach extImp) { private HttpRequest makeHttpRequestForPublisher(BidRequest bidRequest, ExtRequestGoldbach extRequestGoldbach, String publisherId, - List imps, - List errors) { + List imps) { - try { - final BidRequest modifiedBidRequest = modifyBidRequest(bidRequest, extRequestGoldbach, publisherId, imps); - return BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - return null; - } + final BidRequest modifiedBidRequest = modifyBidRequest(bidRequest, extRequestGoldbach, publisherId, imps); + return BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper); } private BidRequest modifyBidRequest(BidRequest bidRequest, @@ -223,10 +216,7 @@ private Result> bidsFromResponse(BidResponse bidResponse) { private BidderBid makeBid(Bid bid, BidResponse bidResponse, List errors) { try { - return BidderBid.of( - bid, - getBidType(bid), - bidResponse.getCur()); + return BidderBid.of(bid, getBidType(bid), bidResponse.getCur()); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); return null; diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GoldbachConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GoldbachConfiguration.java index e30eb3cbe70..302a25bb5b9 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/GoldbachConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/GoldbachConfiguration.java @@ -29,8 +29,8 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps goldbachBidderDeps(BidderConfigurationProperties goldbachConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(goldbachConfigurationProperties) diff --git a/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java b/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java index 097aeb8d936..c0d1a70f894 100644 --- a/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java @@ -467,13 +467,7 @@ public void makeBidsShouldReturnValidBidsAndErrorsIfThereAreBothValidAndInvalidB // then assertThat(result.getErrors()).containsExactly(BidderError.badInput("No media type for bid invalidBidId")); - assertThat(result.getValue()).containsExactly( - BidderBid.of( - validBid, - BidType.banner, - "USD" - ) - ); + assertThat(result.getValue()).containsExactly(BidderBid.of(validBid, BidType.banner, "USD")); } private static BidRequest givenBidRequest() { From b2409c5bdefa1594ce7fe1092ec35ce7d4f8f840 Mon Sep 17 00:00:00 2001 From: Viktor Kryshtal Date: Tue, 2 Dec 2025 11:01:43 +0200 Subject: [PATCH 4/6] Resolve comments --- .../org/prebid/server/bidder/goldbach/GoldbachBidder.java | 7 +++---- .../server/bidder/goldbach/proto/ExtRequestGoldbach.java | 2 -- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java b/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java index 58eb41a2e76..3a90b151f2b 100644 --- a/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java +++ b/src/main/java/org/prebid/server/bidder/goldbach/GoldbachBidder.java @@ -147,10 +147,9 @@ private ExtRequest modifyExtRequest(ExtRequest extRequest, ExtRequestGoldbach extRequestGoldbach, String publisherId) { - final ExtRequestGoldbach modifiedExtRequestGoldbach = ExtRequestGoldbach.builder() - .publisherId(publisherId) - .mockResponse(extRequestGoldbach != null ? extRequestGoldbach.getMockResponse() : null) - .build(); + final ExtRequestGoldbach modifiedExtRequestGoldbach = ExtRequestGoldbach.of( + publisherId, + extRequestGoldbach != null ? extRequestGoldbach.getMockResponse() : null); final ExtRequest modifiedExtRequest; diff --git a/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtRequestGoldbach.java b/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtRequestGoldbach.java index 6c18c7ac89a..680ef489f17 100644 --- a/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtRequestGoldbach.java +++ b/src/main/java/org/prebid/server/bidder/goldbach/proto/ExtRequestGoldbach.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.goldbach.proto; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; import lombok.Value; @Value(staticConstructor = "of") -@Builder(toBuilder = true) public class ExtRequestGoldbach { @JsonProperty("publisherId") From b33b78caac3ed2c390fd87a8bb1343a7364d2d60 Mon Sep 17 00:00:00 2001 From: Viktor Kryshtal Date: Thu, 4 Dec 2025 20:54:08 +0200 Subject: [PATCH 5/6] Fix formatting --- .../server/bidder/goldbach/GoldbachBidderTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java b/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java index c0d1a70f894..c0419c20b02 100644 --- a/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/goldbach/GoldbachBidderTest.java @@ -446,9 +446,7 @@ public void makeBidsShouldReturnBidWithNoErrorsForValidInput() { BidderBid.of( bid, BidType.banner, - "USD" - ) - ); + "USD")); } @Test @@ -511,8 +509,9 @@ private static ObjectNode givenImpExt(String publisherId, String slotId) { return givenImpExt(publisherId, slotId, null); } - private static ObjectNode givenImpExt(String publisherId, String - slotId, Map> customTargeting) { + private static ObjectNode givenImpExt(String publisherId, + String slotId, + Map> customTargeting) { return mapper.valueToTree(ExtPrebid.of( null, ExtImpGoldbach.of(publisherId, slotId, customTargeting))); From 7c8659c9c75fb33991cdd245cd00a14b1890b941 Mon Sep 17 00:00:00 2001 From: Viktor Kryshtal Date: Fri, 5 Dec 2025 14:43:02 +0200 Subject: [PATCH 6/6] Fix integration test --- .../it/openrtb2/goldbach/test-goldbach-bid-request.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-request.json index e83c5c69787..5dfc52fa333 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/goldbach/test-goldbach-bid-request.json @@ -44,6 +44,14 @@ } }, "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + }, "goldbach": { "publisherId": "de-example.ch" }