From 07efcf6a9357456ca3187bda3ac8598c318874bf Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Wed, 3 Jun 2026 22:03:17 +0300 Subject: [PATCH 01/10] Port Floxis: New Adapter --- .../server/bidder/floxis/FloxisBidder.java | 183 ++++++++ .../ext/request/floxis/ExtImpFloxis.java | 13 + .../config/bidder/FloxisConfiguration.java | 43 ++ src/main/resources/bidder-config/floxis.yaml | 24 + .../static/bidder-params/floxis.json | 20 + .../bidder/floxis/FloxisBidderTest.java | 423 ++++++++++++++++++ .../java/org/prebid/server/it/FloxisTest.java | 32 ++ .../floxis/test-auction-floxis-request.json | 22 + .../floxis/test-auction-floxis-response.json | 44 ++ .../floxis/test-floxis-bid-request.json | 56 +++ .../floxis/test-floxis-bid-response.json | 23 + .../server/it/test-application.properties | 2 + 12 files changed, 885 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java create mode 100644 src/main/resources/bidder-config/floxis.yaml create mode 100644 src/main/resources/static/bidder-params/floxis.json create mode 100644 src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/FloxisTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java new file mode 100644 index 00000000000..0942f2196b9 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java @@ -0,0 +1,183 @@ +package org.prebid.server.bidder.floxis; + +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 io.vertx.core.http.HttpMethod; +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.floxis.ExtImpFloxis; +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.List; +import java.util.Map; +import java.util.Objects; + +public class FloxisBidder implements Bidder { + + private static final TypeReference> FLOXIS_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String HOST_MACRO = "{{Host}}"; + private static final String SEAT_MACRO = "{{SeatId}}"; + + // Fixed allowlist mapping the bidder's region param to a Floxis RTB host. Routing is + // never derived from request-supplied hostnames; an unknown or empty region falls back + // to us-e. + private static final Map REGION_HOSTS = Map.of( + "us-e", "rtb-us-e.floxis.tech", + "eu", "rtb-eu.floxis.tech", + "apac", "rtb-apac.floxis.tech"); + + private static final String DEFAULT_REGION = "us-e"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public FloxisBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + if (CollectionUtils.isEmpty(request.getImp())) { + return Result.withError(BidderError.badInput("no impressions in the bid request")); + } + + final ExtImpFloxis extImp; + try { + extImp = parseImpExt(request.getImp().getFirst()); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + // The request body is forwarded unchanged; no caller-owned struct is mutated. + return Result.withValue(HttpRequest.builder() + .method(HttpMethod.POST) + .uri(resolveUrl(endpointUrl, extImp)) + .headers(HttpUtil.headers()) + .impIds(BidderUtil.impIds(request)) + .payload(request) + .body(mapper.encodeToBytes(request)) + .build()); + } + + private ExtImpFloxis parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), FLOXIS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("invalid imp.ext.bidder for imp %s: %s".formatted(imp.getId(), e.getMessage())); + } + } + + private static String resolveHost(String region) { + final String host = region == null ? null : REGION_HOSTS.get(region); + return host != null ? host : REGION_HOSTS.get(DEFAULT_REGION); + } + + private static String resolveUrl(String endpoint, ExtImpFloxis extImp) { + return endpoint + .replace(HOST_MACRO, resolveHost(extImp.getRegion())) + .replace(SEAT_MACRO, HttpUtil.encodeUrl(extImp.getSeat())); + } + + @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())); + } + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Result.empty(); + } + + final List errors = new ArrayList<>(); + final List bids = new ArrayList<>(); + for (SeatBid seatBid : bidResponse.getSeatbid()) { + if (seatBid == null || CollectionUtils.isEmpty(seatBid.getBid())) { + continue; + } + for (Bid bid : seatBid.getBid()) { + try { + bids.add(BidderBid.of(bid, getMediaTypeForBid(bidRequest.getImp(), bid), bidResponse.getCur())); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + } + } + } + + return Result.of(bids, errors); + } + + // Resolves the bid's media type. When bid.mtype (OpenRTB 2.6) is set it is treated as + // authoritative. When unset, a single-format imp's media type is used; multi-format imps + // without mtype cannot be disambiguated and surface an error. + private static BidType getMediaTypeForBid(List imps, Bid bid) { + final Integer mtype = bid.getMtype(); + if (mtype != null && mtype != 0) { + return switch (mtype) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "unsupported bid.mtype %d for impression %s".formatted(mtype, bid.getImpid())); + }; + } + + for (Imp imp : imps) { + if (!Objects.equals(imp.getId(), bid.getImpid())) { + continue; + } + int formats = 0; + BidType resolved = null; + if (imp.getBanner() != null) { + formats++; + resolved = BidType.banner; + } + if (imp.getVideo() != null) { + formats++; + resolved = BidType.video; + } + if (imp.getAudio() != null) { + formats++; + resolved = BidType.audio; + } + if (imp.getXNative() != null) { + formats++; + resolved = BidType.xNative; + } + if (formats == 1) { + return resolved; + } else if (formats > 1) { + throw new PreBidException( + "bid for multi-format imp %s requires bid.mtype to disambiguate".formatted(bid.getImpid())); + } else { + throw new PreBidException( + "unable to resolve media type for impression %s".formatted(bid.getImpid())); + } + } + + throw new PreBidException("unable to find impression %s for bid".formatted(bid.getImpid())); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java new file mode 100644 index 00000000000..f69ddc85a1e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.floxis; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpFloxis { + + String seat; + + @JsonProperty("region") + String region; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java new file mode 100644 index 00000000000..7e0eff122bb --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.floxis.FloxisBidder; +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/floxis.yaml", + factory = YamlPropertySourceFactory.class) +public class FloxisConfiguration { + + private static final String BIDDER_NAME = "floxis"; + + @Bean("floxisConfigurationProperties") + @ConfigurationProperties("adapters.floxis") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps floxisBidderDeps(BidderConfigurationProperties floxisConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(floxisConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new FloxisBidder(config.getEndpoint(), mapper)) + .assemble(); + } + +} diff --git a/src/main/resources/bidder-config/floxis.yaml b/src/main/resources/bidder-config/floxis.yaml new file mode 100644 index 00000000000..2869dce9f92 --- /dev/null +++ b/src/main/resources/bidder-config/floxis.yaml @@ -0,0 +1,24 @@ +adapters: + floxis: + endpoint: https://{{Host}}/pbs?seat={{SeatId}} + modifying-vast-xml-allowed: false + meta-info: + maintainer-email: prebid@floxis.tech + app-media-types: + - banner + - video + - native + - audio + site-media-types: + - banner + - video + - native + - audio + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: floxis + redirect: + url: "https://px-us-e.floxis.tech/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&us_privacy={{us_privacy}}&dest={{redirect_url}}" + support-cors: false + uid-macro: "${USER_ID}" diff --git a/src/main/resources/static/bidder-params/floxis.json b/src/main/resources/static/bidder-params/floxis.json new file mode 100644 index 00000000000..6d6d2ecd640 --- /dev/null +++ b/src/main/resources/static/bidder-params/floxis.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Floxis Adapter Params", + "description": "A schema which validates params accepted by the Floxis adapter", + "type": "object", + "additionalProperties": false, + "properties": { + "seat": { + "type": "string", + "minLength": 1, + "description": "The Floxis seat ID this publisher buys through" + }, + "region": { + "type": "string", + "enum": ["us-e", "eu", "apac"], + "description": "The Floxis RTB region; defaults to us-e when omitted" + } + }, + "required": ["seat"] +} diff --git a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java new file mode 100644 index 00000000000..86daf8562c5 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java @@ -0,0 +1,423 @@ +package org.prebid.server.bidder.floxis; + +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Site; +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.floxis.ExtImpFloxis; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +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 FloxisBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://{{Host}}/pbs?seat={{SeatId}}"; + + private final FloxisBidder target = new FloxisBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new FloxisBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenNoImpressions() { + // given + final BidRequest bidRequest = BidRequest.builder().id("req-1").imp(null).build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("no impressions in the bid request")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp + .id("imp-1") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("invalid imp.ext.bidder for imp imp-1:"); + }); + } + + @Test + public void makeHttpRequestsShouldUrlEscapeSeatAndUseEuHost() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("a b&c", "eu"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://rtb-eu.floxis.tech/pbs?seat=a+b%26c"); + } + + @Test + public void makeHttpRequestsShouldUseApacHost() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("seat-apac", "apac"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://rtb-apac.floxis.tech/pbs?seat=seat-apac"); + } + + @Test + public void makeHttpRequestsShouldUseUseHostForExplicitUsE() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "us-e"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://rtb-us-e.floxis.tech/pbs?seat=abc"); + } + + @Test + public void makeHttpRequestsShouldDefaultToUseHostWhenRegionMissing() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", null))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://rtb-us-e.floxis.tech/pbs?seat=abc"); + } + + @Test + public void makeHttpRequestsShouldDefaultToUseHostWhenRegionUnknown() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "mars"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://rtb-us-e.floxis.tech/pbs?seat=abc"); + } + + @Test + public void makeHttpRequestsShouldRouteFromFirstImpAndForwardAllImps() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-eu", "eu"))), + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-other", "apac"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://rtb-eu.floxis.tech/pbs?seat=seat-eu"); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("imp-1", "imp-2"); + } + + @Test + public void makeHttpRequestsShouldForwardRequestBodyUnchanged() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "eu"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .containsExactly(bidRequest); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCannotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), "invalid"); + + // when + final Result> result = target.makeBids(httpCall, BidRequest.builder().build()); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response)); + } + + @Test + public void makeBidsShouldReturnEmptyWhenResponseHasNoSeatBid() throws Exception { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, BidRequest.builder().build()); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldSkipNullSeatBidAndEmptyBids() throws Exception { + // given + final BidResponse bidResponse = BidResponse.builder() + .cur("USD") + .seatbid(asList( + null, + SeatBid.builder().bid(null).build(), + SeatBid.builder() + .bid(singletonList(Bid.builder().impid("imp-1").mtype(1).build())) + .build())) + .build(); + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, givenBidRequest(identity())); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getType) + .containsExactly(BidType.banner); + } + + @Test + public void makeBidsShouldResolveBidTypeFromMTypeAndPassCurrency() throws Exception { + // given + final BidResponse bidResponse = BidResponse.builder() + .cur("EUR") + .seatbid(singletonList(SeatBid.builder() + .bid(asList( + Bid.builder().impid("imp-1").mtype(1).build(), + Bid.builder().impid("imp-1").mtype(2).build(), + Bid.builder().impid("imp-1").mtype(3).build(), + Bid.builder().impid("imp-1").mtype(4).build())) + .build())) + .build(); + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, givenBidRequest(identity())); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getType, BidderBid::getBidCurrency) + .containsExactly( + tuple(BidType.banner, "EUR"), + tuple(BidType.video, "EUR"), + tuple(BidType.audio, "EUR"), + tuple(BidType.xNative, "EUR")); + } + + @Test + public void makeBidsShouldReturnErrorWhenMTypeUnsupported() throws Exception { + // given + final BidResponse bidResponse = givenBidResponse(Bid.builder().impid("imp-1").mtype(7).build()); + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, givenBidRequest(identity())); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badServerResponse( + "unsupported bid.mtype 7 for impression imp-1")); + } + + @Test + public void makeBidsShouldResolveBidTypeFromSingleFormatImpWhenMTypeAbsent() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList( + Imp.builder().id("b").banner(Banner.builder().build()).build(), + Imp.builder().id("v").video(Video.builder().build()).build(), + Imp.builder().id("a").audio(Audio.builder().build()).build(), + Imp.builder().id("n").xNative(Native.builder().build()).build())) + .build(); + final BidResponse bidResponse = BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(asList( + Bid.builder().impid("b").build(), + Bid.builder().impid("v").build(), + Bid.builder().impid("a").build(), + Bid.builder().impid("n").build())) + .build())) + .build(); + final BidderCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getType) + .containsExactly(BidType.banner, BidType.video, BidType.audio, BidType.xNative); + } + + @Test + public void makeBidsShouldReturnErrorForMultiFormatImpWithoutMType() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("imp-1") + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .build())) + .build(); + final BidResponse bidResponse = givenBidResponse(Bid.builder().impid("imp-1").build()); + final BidderCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badServerResponse( + "bid for multi-format imp imp-1 requires bid.mtype to disambiguate")); + } + + @Test + public void makeBidsShouldReturnErrorForImpWithoutFormatAndWithoutMType() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().id("imp-1").build())) + .build(); + final BidResponse bidResponse = givenBidResponse(Bid.builder().impid("imp-1").build()); + final BidderCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badServerResponse( + "unable to resolve media type for impression imp-1")); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidImpIdNotFound() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().id("imp-1").banner(Banner.builder().build()).build())) + .build(); + final BidResponse bidResponse = givenBidResponse(Bid.builder().impid("no-such-imp").build()); + final BidderCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badServerResponse( + "unable to find impression no-such-imp for bid")); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return BidRequest.builder() + .id("req-1") + .imp(singletonList(givenImp(impCustomizer))) + .site(Site.builder().id("271").build()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("imp-1") + .banner(Banner.builder().format(singletonList(Format.builder().w(300).h(250).build())).build()) + .ext(givenImpExt("pub-seat-1", null))) + .build(); + } + + private static com.fasterxml.jackson.databind.node.ObjectNode givenImpExt(String seat, String region) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpFloxis.of(seat, region))); + } + + private static BidResponse givenBidResponse(Bid bid) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(singletonList(bid)).build())) + .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/FloxisTest.java b/src/test/java/org/prebid/server/it/FloxisTest.java new file mode 100644 index 00000000000..429948bfb45 --- /dev/null +++ b/src/test/java/org/prebid/server/it/FloxisTest.java @@ -0,0 +1,32 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class FloxisTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromFloxis() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/floxis-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/floxis/test-floxis-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/floxis/test-floxis-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/floxis/test-auction-floxis-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/floxis/test-auction-floxis-response.json", response, singletonList("floxis")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-request.json b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-request.json new file mode 100644 index 00000000000..f65ab526cb2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-request.json @@ -0,0 +1,22 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "floxis": { + "seat": "pub-seat-1" + } + } + } + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-response.json b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-response.json new file mode 100644 index 00000000000..07b4c9cb073 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-response.json @@ -0,0 +1,44 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "exp": 300, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "floxis" + } + }, + "origbidcpm": 3.33, + "origbidcur": "USD" + } + } + ], + "seat": "floxis", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "floxis": "{{ floxis.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json new file mode 100644 index 00000000000..b0dde18ca04 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-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": { + "seat": "pub-seat-1" + } + } + } + ], + "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/floxis/test-floxis-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-response.json new file mode 100644 index 00000000000..c9c23670bfa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-response.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "cur": "USD", + "seatbid": [ + { + "seat": "floxis", + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300, + "mtype": 1 + } + ] + } + ] +} 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 1569de649f5..2f1ad6d9efc 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -261,6 +261,8 @@ adapters.flatads.enabled=true adapters.flatads.endpoint=http://localhost:8090/flatads-exchange adapters.flipp.enabled=true adapters.flipp.endpoint=http://localhost:8090/flipp-exchange +adapters.floxis.enabled=true +adapters.floxis.endpoint=http://localhost:8090/floxis-exchange?seat={{SeatId}} adapters.audiencenetwork.enabled=true adapters.audiencenetwork.endpoint=http://localhost:8090/audienceNetwork-exchange adapters.audiencenetwork.platform-id=101 From f6797a385aa8a6c3bdc0eeafc7bc3960043a6b18 Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Fri, 5 Jun 2026 16:31:12 +0300 Subject: [PATCH 02/10] Floxis: set vendor-id to 1609 (IAB TCF registration approved) --- src/main/resources/bidder-config/floxis.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/bidder-config/floxis.yaml b/src/main/resources/bidder-config/floxis.yaml index 2869dce9f92..624cefa9956 100644 --- a/src/main/resources/bidder-config/floxis.yaml +++ b/src/main/resources/bidder-config/floxis.yaml @@ -15,7 +15,7 @@ adapters: - native - audio supported-vendors: - vendor-id: 0 + vendor-id: 1609 usersync: cookie-family-name: floxis redirect: From 41c57c27dbb4118224bd50494947ca029326480d Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Sat, 6 Jun 2026 12:30:40 +0300 Subject: [PATCH 03/10] Floxis: declare ortb-version 2.6 + multi-imp seat guard + IT downconversion guard --- .../server/bidder/floxis/FloxisBidder.java | 19 +++++++- src/main/resources/bidder-config/floxis.yaml | 1 + .../bidder/floxis/FloxisBidderTest.java | 48 ++++++++++++++++++- .../java/org/prebid/server/it/FloxisTest.java | 2 + .../floxis/test-auction-floxis-request.json | 13 +++++ .../floxis/test-floxis-bid-request.json | 17 +++++-- 6 files changed, 93 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java index 0942f2196b9..eda36abd27b 100644 --- a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java +++ b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java @@ -63,7 +63,7 @@ public Result>> makeHttpRequests(BidRequest request final ExtImpFloxis extImp; try { - extImp = parseImpExt(request.getImp().getFirst()); + extImp = resolveCommonImpExt(request.getImp()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } @@ -79,6 +79,23 @@ public Result>> makeHttpRequests(BidRequest request .build()); } + // A single request routes to one Floxis host/seat (the seat is the URL query key). All imps + // must therefore share the same seat and region; a mismatch is a misconfigured request rather + // than something to silently route on imp[0]'s key. + private ExtImpFloxis resolveCommonImpExt(List imps) { + final ExtImpFloxis first = parseImpExt(imps.getFirst()); + for (Imp imp : imps.subList(1, imps.size())) { + final ExtImpFloxis current = parseImpExt(imp); + if (!Objects.equals(current.getSeat(), first.getSeat()) + || !Objects.equals(current.getRegion(), first.getRegion())) { + throw new PreBidException( + "all impressions must target the same Floxis seat and region; imp %s differs from imp %s" + .formatted(imp.getId(), imps.getFirst().getId())); + } + } + return first; + } + private ExtImpFloxis parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), FLOXIS_EXT_TYPE_REFERENCE).getBidder(); diff --git a/src/main/resources/bidder-config/floxis.yaml b/src/main/resources/bidder-config/floxis.yaml index 624cefa9956..49d5de6f3b5 100644 --- a/src/main/resources/bidder-config/floxis.yaml +++ b/src/main/resources/bidder-config/floxis.yaml @@ -1,6 +1,7 @@ adapters: floxis: endpoint: https://{{Host}}/pbs?seat={{SeatId}} + ortb-version: "2.6" modifying-vast-xml-allowed: false meta-info: maintainer-email: prebid@floxis.tech diff --git a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java index 86daf8562c5..d8919e0641e 100644 --- a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java @@ -154,13 +154,13 @@ public void makeHttpRequestsShouldDefaultToUseHostWhenRegionUnknown() { } @Test - public void makeHttpRequestsShouldRouteFromFirstImpAndForwardAllImps() { + public void makeHttpRequestsShouldRouteOnceAndForwardAllImpsWhenSeatAndRegionMatch() { // given final BidRequest bidRequest = BidRequest.builder() .id("req-1") .imp(asList( givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-eu", "eu"))), - givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-other", "apac"))))) + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-eu", "eu"))))) .site(Site.builder().id("271").build()) .build(); @@ -179,6 +179,50 @@ public void makeHttpRequestsShouldRouteFromFirstImpAndForwardAllImps() { .containsExactly("imp-1", "imp-2"); } + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpsTargetDifferentSeat() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-eu", "eu"))), + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-other", "eu"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput( + "all impressions must target the same Floxis seat and region; " + + "imp imp-2 differs from imp imp-1")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpsTargetDifferentRegion() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-eu", "eu"))), + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-eu", "apac"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput( + "all impressions must target the same Floxis seat and region; " + + "imp imp-2 differs from imp imp-1")); + } + @Test public void makeHttpRequestsShouldForwardRequestBodyUnchanged() { // given diff --git a/src/test/java/org/prebid/server/it/FloxisTest.java b/src/test/java/org/prebid/server/it/FloxisTest.java index 429948bfb45..8a67859fe12 100644 --- a/src/test/java/org/prebid/server/it/FloxisTest.java +++ b/src/test/java/org/prebid/server/it/FloxisTest.java @@ -8,6 +8,7 @@ import java.io.IOException; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 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; @@ -19,6 +20,7 @@ public class FloxisTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromFloxis() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/floxis-exchange")) + .withQueryParam("seat", equalTo("pub-seat-1")) .withRequestBody(equalToJson(jsonFrom("openrtb2/floxis/test-floxis-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/floxis/test-floxis-bid-response.json")))); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-request.json b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-request.json index f65ab526cb2..4ce042710fa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-request.json @@ -14,6 +14,19 @@ } } ], + "source": { + "schain": { + "complete": 1, + "ver": "1.0", + "nodes": [ + { + "asi": "floxis.tech", + "sid": "pub-seat-1", + "hp": 1 + } + ] + } + }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json index b0dde18ca04..ec7ab98fbd5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json @@ -17,7 +17,18 @@ } ], "source": { - "tid": "${json-unit.any-string}" + "tid": "${json-unit.any-string}", + "schain": { + "complete": 1, + "ver": "1.0", + "nodes": [ + { + "asi": "floxis.tech", + "sid": "pub-seat-1", + "hp": 1 + } + ] + } }, "site": { "domain": "www.example.com", @@ -39,9 +50,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { From 86a34b685cf98a138d08ae3b2de44341af22e6de Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Sat, 6 Jun 2026 15:21:42 +0300 Subject: [PATCH 04/10] Floxis: mirror Prebid.js seat/region/partner host logic Co-Authored-By: Claude Opus 4.8 --- .../server/bidder/floxis/FloxisBidder.java | 54 ++++++----- .../ext/request/floxis/ExtImpFloxis.java | 3 + .../static/bidder-params/floxis.json | 9 +- .../bidder/floxis/FloxisBidderTest.java | 90 ++++++++++++++++--- 4 files changed, 123 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java index eda36abd27b..79bf48dc8d1 100644 --- a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java +++ b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java @@ -25,8 +25,8 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Objects; +import java.util.regex.Pattern; public class FloxisBidder implements Bidder { @@ -37,15 +37,12 @@ public class FloxisBidder implements Bidder { private static final String HOST_MACRO = "{{Host}}"; private static final String SEAT_MACRO = "{{SeatId}}"; - // Fixed allowlist mapping the bidder's region param to a Floxis RTB host. Routing is - // never derived from request-supplied hostnames; an unknown or empty region falls back - // to us-e. - private static final Map REGION_HOSTS = Map.of( - "us-e", "rtb-us-e.floxis.tech", - "eu", "rtb-eu.floxis.tech", - "apac", "rtb-apac.floxis.tech"); - private static final String DEFAULT_REGION = "us-e"; + private static final String DEFAULT_PARTNER = "floxis"; + + // region/partner are interpolated into the request host, so each must be a valid DNS label — + // otherwise a value carrying URL delimiters could rewrite the request origin. + private static final Pattern HOST_LABEL = Pattern.compile("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$"); private final String endpointUrl; private final JacksonMapper mapper; @@ -61,9 +58,9 @@ public Result>> makeHttpRequests(BidRequest request return Result.withError(BidderError.badInput("no impressions in the bid request")); } - final ExtImpFloxis extImp; + final String uri; try { - extImp = resolveCommonImpExt(request.getImp()); + uri = resolveUrl(endpointUrl, resolveCommonImpExt(request.getImp())); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } @@ -71,7 +68,7 @@ public Result>> makeHttpRequests(BidRequest request // The request body is forwarded unchanged; no caller-owned struct is mutated. return Result.withValue(HttpRequest.builder() .method(HttpMethod.POST) - .uri(resolveUrl(endpointUrl, extImp)) + .uri(uri) .headers(HttpUtil.headers()) .impIds(BidderUtil.impIds(request)) .payload(request) @@ -79,17 +76,19 @@ public Result>> makeHttpRequests(BidRequest request .build()); } - // A single request routes to one Floxis host/seat (the seat is the URL query key). All imps - // must therefore share the same seat and region; a mismatch is a misconfigured request rather - // than something to silently route on imp[0]'s key. + // A single request routes to one Floxis host/seat (the seat is the URL query key, partner+region + // the host). All imps must therefore share the same seat, region and partner; a mismatch is a + // misconfigured request rather than something to silently route on imp[0]'s key. private ExtImpFloxis resolveCommonImpExt(List imps) { final ExtImpFloxis first = parseImpExt(imps.getFirst()); for (Imp imp : imps.subList(1, imps.size())) { final ExtImpFloxis current = parseImpExt(imp); if (!Objects.equals(current.getSeat(), first.getSeat()) - || !Objects.equals(current.getRegion(), first.getRegion())) { + || !Objects.equals(current.getRegion(), first.getRegion()) + || !Objects.equals(current.getPartner(), first.getPartner())) { throw new PreBidException( - "all impressions must target the same Floxis seat and region; imp %s differs from imp %s" + "all impressions must target the same Floxis seat, region and partner; " + + "imp %s differs from imp %s" .formatted(imp.getId(), imps.getFirst().getId())); } } @@ -104,14 +103,27 @@ private ExtImpFloxis parseImpExt(Imp imp) { } } - private static String resolveHost(String region) { - final String host = region == null ? null : REGION_HOSTS.get(region); - return host != null ? host : REGION_HOSTS.get(DEFAULT_REGION); + // Bidding host: the supply partner's regional subdomain (floxis itself has no partner prefix). + private static String resolveBidHost(String region, String partner) { + final String resolvedRegion = isBlank(region) ? DEFAULT_REGION : region; + final String resolvedPartner = isBlank(partner) ? DEFAULT_PARTNER : partner; + if (!HOST_LABEL.matcher(resolvedRegion).matches() || !HOST_LABEL.matcher(resolvedPartner).matches()) { + throw new PreBidException( + "invalid Floxis region or partner; both must be DNS labels: region=%s partner=%s" + .formatted(resolvedRegion, resolvedPartner)); + } + return resolvedPartner.equals(DEFAULT_PARTNER) + ? resolvedRegion + ".floxis.tech" + : resolvedPartner + "-" + resolvedRegion + ".floxis.tech"; + } + + private static boolean isBlank(String value) { + return value == null || value.isEmpty(); } private static String resolveUrl(String endpoint, ExtImpFloxis extImp) { return endpoint - .replace(HOST_MACRO, resolveHost(extImp.getRegion())) + .replace(HOST_MACRO, resolveBidHost(extImp.getRegion(), extImp.getPartner())) .replace(SEAT_MACRO, HttpUtil.encodeUrl(extImp.getSeat())); } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java index f69ddc85a1e..ff34a22b00a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java @@ -10,4 +10,7 @@ public class ExtImpFloxis { @JsonProperty("region") String region; + + @JsonProperty("partner") + String partner; } diff --git a/src/main/resources/static/bidder-params/floxis.json b/src/main/resources/static/bidder-params/floxis.json index 6d6d2ecd640..621f3239533 100644 --- a/src/main/resources/static/bidder-params/floxis.json +++ b/src/main/resources/static/bidder-params/floxis.json @@ -12,8 +12,13 @@ }, "region": { "type": "string", - "enum": ["us-e", "eu", "apac"], - "description": "The Floxis RTB region; defaults to us-e when omitted" + "pattern": "^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$", + "description": "The Floxis region (DNS label, interpolated into the bidding host); defaults to us-e when omitted" + }, + "partner": { + "type": "string", + "pattern": "^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$", + "description": "The white-label partner (DNS label, interpolated into the bidding host); defaults to floxis when omitted" } }, "required": ["seat"] diff --git a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java index d8919e0641e..a2f29c8bbf4 100644 --- a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java @@ -90,7 +90,7 @@ public void makeHttpRequestsShouldUrlEscapeSeatAndUseEuHost() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getUri) - .containsExactly("https://rtb-eu.floxis.tech/pbs?seat=a+b%26c"); + .containsExactly("https://eu.floxis.tech/pbs?seat=a+b%26c"); } @Test @@ -105,7 +105,7 @@ public void makeHttpRequestsShouldUseApacHost() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getUri) - .containsExactly("https://rtb-apac.floxis.tech/pbs?seat=seat-apac"); + .containsExactly("https://apac.floxis.tech/pbs?seat=seat-apac"); } @Test @@ -120,7 +120,7 @@ public void makeHttpRequestsShouldUseUseHostForExplicitUsE() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getUri) - .containsExactly("https://rtb-us-e.floxis.tech/pbs?seat=abc"); + .containsExactly("https://us-e.floxis.tech/pbs?seat=abc"); } @Test @@ -135,11 +135,11 @@ public void makeHttpRequestsShouldDefaultToUseHostWhenRegionMissing() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getUri) - .containsExactly("https://rtb-us-e.floxis.tech/pbs?seat=abc"); + .containsExactly("https://us-e.floxis.tech/pbs?seat=abc"); } @Test - public void makeHttpRequestsShouldDefaultToUseHostWhenRegionUnknown() { + public void makeHttpRequestsShouldUseArbitraryValidRegionLabelAsSubdomain() { // given final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "mars"))); @@ -150,7 +150,50 @@ public void makeHttpRequestsShouldDefaultToUseHostWhenRegionUnknown() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getUri) - .containsExactly("https://rtb-us-e.floxis.tech/pbs?seat=abc"); + .containsExactly("https://mars.floxis.tech/pbs?seat=abc"); + } + + @Test + public void makeHttpRequestsShouldReturnBadInputWhenRegionIsNotAValidHostLabel() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "evil.com/x?"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input)); + } + + @Test + public void makeHttpRequestsShouldUsePartnerPrefixedHostWhenPartnerProvided() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "us-e", "acme"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://acme-us-e.floxis.tech/pbs?seat=abc"); + } + + @Test + public void makeHttpRequestsShouldReturnBadInputWhenPartnerIsNotAValidHostLabel() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "us-e", "evil.com/x?"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input)); } @Test @@ -171,7 +214,7 @@ public void makeHttpRequestsShouldRouteOnceAndForwardAllImpsWhenSeatAndRegionMat assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getUri) - .containsExactly("https://rtb-eu.floxis.tech/pbs?seat=seat-eu"); + .containsExactly("https://eu.floxis.tech/pbs?seat=seat-eu"); assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) @@ -197,7 +240,7 @@ public void makeHttpRequestsShouldReturnErrorWhenImpsTargetDifferentSeat() { assertThat(result.getValue()).isEmpty(); assertThat(result.getErrors()).hasSize(1) .containsOnly(BidderError.badInput( - "all impressions must target the same Floxis seat and region; " + "all impressions must target the same Floxis seat, region and partner; " + "imp imp-2 differs from imp imp-1")); } @@ -219,7 +262,29 @@ public void makeHttpRequestsShouldReturnErrorWhenImpsTargetDifferentRegion() { assertThat(result.getValue()).isEmpty(); assertThat(result.getErrors()).hasSize(1) .containsOnly(BidderError.badInput( - "all impressions must target the same Floxis seat and region; " + "all impressions must target the same Floxis seat, region and partner; " + + "imp imp-2 differs from imp imp-1")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpsTargetDifferentPartner() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-eu", "eu", "acme"))), + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-eu", "eu", "other"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput( + "all impressions must target the same Floxis seat, region and partner; " + "imp imp-2 differs from imp imp-1")); } @@ -448,7 +513,12 @@ private static Imp givenImp(UnaryOperator impCustomizer) { } private static com.fasterxml.jackson.databind.node.ObjectNode givenImpExt(String seat, String region) { - return mapper.valueToTree(ExtPrebid.of(null, ExtImpFloxis.of(seat, region))); + return givenImpExt(seat, region, null); + } + + private static com.fasterxml.jackson.databind.node.ObjectNode givenImpExt(String seat, String region, + String partner) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpFloxis.of(seat, region, partner))); } private static BidResponse givenBidResponse(Bid bid) { From ee616ff758f454af96bcc5d63cb5becb0ac76d3a Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Mon, 15 Jun 2026 21:15:29 +0300 Subject: [PATCH 05/10] Floxis adapter: annotate ExtImpFloxis.seat with @JsonProperty for consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seat lacked the explicit @JsonProperty that region and partner carry; add it so all three params are annotated uniformly (no behavioural change — seat already maps to the 'seat' key). --- .../server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java index ff34a22b00a..7d261681797 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java @@ -6,6 +6,7 @@ @Value(staticConstructor = "of") public class ExtImpFloxis { + @JsonProperty("seat") String seat; @JsonProperty("region") From 0db0e0c1d4c2cb2ff8b22dfa2573e6e9bc22143a Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Mon, 15 Jun 2026 22:38:24 +0300 Subject: [PATCH 06/10] Floxis adapter: pin fixed .floxis.tech domain in endpoint (region/partner are subdomain labels) Per the dev-guide, a bidder endpoint domain must not be fully variable. Move the fixed .floxis.tech suffix into the bidder-config endpoint and have resolveBidHost return just the validated region/partner subdomain label. Resolved URLs are unchanged (e.g. https://us-e.floxis.tech/pbs); matches the fixed-suffix pattern of merged region-routed adapters (rubicon, clydo, mediago, algorix). --- .../java/org/prebid/server/bidder/floxis/FloxisBidder.java | 7 ++++--- src/main/resources/bidder-config/floxis.yaml | 2 +- .../org/prebid/server/bidder/floxis/FloxisBidderTest.java | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java index 79bf48dc8d1..b390f0f39bf 100644 --- a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java +++ b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java @@ -103,7 +103,8 @@ private ExtImpFloxis parseImpExt(Imp imp) { } } - // Bidding host: the supply partner's regional subdomain (floxis itself has no partner prefix). + // {{Host}} subdomain label for the endpoint template (which pins the fixed .floxis.tech + // domain): the region, or partner-region for a named supply partner (floxis has no prefix). private static String resolveBidHost(String region, String partner) { final String resolvedRegion = isBlank(region) ? DEFAULT_REGION : region; final String resolvedPartner = isBlank(partner) ? DEFAULT_PARTNER : partner; @@ -113,8 +114,8 @@ private static String resolveBidHost(String region, String partner) { .formatted(resolvedRegion, resolvedPartner)); } return resolvedPartner.equals(DEFAULT_PARTNER) - ? resolvedRegion + ".floxis.tech" - : resolvedPartner + "-" + resolvedRegion + ".floxis.tech"; + ? resolvedRegion + : resolvedPartner + "-" + resolvedRegion; } private static boolean isBlank(String value) { diff --git a/src/main/resources/bidder-config/floxis.yaml b/src/main/resources/bidder-config/floxis.yaml index 49d5de6f3b5..0a060c2d3f1 100644 --- a/src/main/resources/bidder-config/floxis.yaml +++ b/src/main/resources/bidder-config/floxis.yaml @@ -1,6 +1,6 @@ adapters: floxis: - endpoint: https://{{Host}}/pbs?seat={{SeatId}} + endpoint: https://{{Host}}.floxis.tech/pbs?seat={{SeatId}} ortb-version: "2.6" modifying-vast-xml-allowed: false meta-info: diff --git a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java index a2f29c8bbf4..d1fa11c09ab 100644 --- a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java @@ -35,7 +35,7 @@ public class FloxisBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://{{Host}}/pbs?seat={{SeatId}}"; + private static final String ENDPOINT_URL = "https://{{Host}}.floxis.tech/pbs?seat={{SeatId}}"; private final FloxisBidder target = new FloxisBidder(ENDPOINT_URL, jacksonMapper); From 21d1900f4e5597f4163c72fa027d12b0bf6a66a9 Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Thu, 18 Jun 2026 11:12:27 +0300 Subject: [PATCH 07/10] Floxis adapter: apply maintainer review feedback Drop redundant in-adapter validation covered elsewhere: the empty-imp guard (framework guarantees at least one imp), the region/partner DNS-label regex (already enforced by floxis.json), and the custom isBlank helper (use StringUtils.isBlank). Restructure makeHttpRequests to parse the first imp ext then validate the rest in a loop; extract extractBids and countFormats helpers; URL-encode the dynamic host parts; drop redundant @JsonProperty on ExtImpFloxis and one-line @PropertySource; remove explanatory comments. Tests updated to match. --- .../server/bidder/floxis/FloxisBidder.java | 151 ++++++++---------- .../ext/request/floxis/ExtImpFloxis.java | 4 - .../config/bidder/FloxisConfiguration.java | 3 +- .../bidder/floxis/FloxisBidderTest.java | 43 +---- 4 files changed, 80 insertions(+), 121 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java index b390f0f39bf..c3790a0e8a4 100644 --- a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java +++ b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java @@ -8,6 +8,7 @@ import com.iab.openrtb.response.SeatBid; 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; @@ -26,7 +27,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.regex.Pattern; public class FloxisBidder implements Bidder { @@ -40,10 +40,6 @@ public class FloxisBidder implements Bidder { private static final String DEFAULT_REGION = "us-e"; private static final String DEFAULT_PARTNER = "floxis"; - // region/partner are interpolated into the request host, so each must be a valid DNS label — - // otherwise a value carrying URL delimiters could rewrite the request origin. - private static final Pattern HOST_LABEL = Pattern.compile("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$"); - private final String endpointUrl; private final JacksonMapper mapper; @@ -54,21 +50,28 @@ public FloxisBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - if (CollectionUtils.isEmpty(request.getImp())) { - return Result.withError(BidderError.badInput("no impressions in the bid request")); + final List imps = request.getImp(); + final Imp firstImp = imps.getFirst(); + + final ExtImpFloxis firstImpExt; + try { + firstImpExt = parseImpExt(firstImp); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); } - final String uri; try { - uri = resolveUrl(endpointUrl, resolveCommonImpExt(request.getImp())); + for (int i = 1; i < imps.size(); i++) { + final Imp imp = imps.get(i); + validateImpExt(parseImpExt(imp), firstImpExt, imp.getId(), firstImp.getId()); + } } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } - // The request body is forwarded unchanged; no caller-owned struct is mutated. return Result.withValue(HttpRequest.builder() .method(HttpMethod.POST) - .uri(uri) + .uri(resolveUrl(endpointUrl, firstImpExt)) .headers(HttpUtil.headers()) .impIds(BidderUtil.impIds(request)) .payload(request) @@ -76,25 +79,6 @@ public Result>> makeHttpRequests(BidRequest request .build()); } - // A single request routes to one Floxis host/seat (the seat is the URL query key, partner+region - // the host). All imps must therefore share the same seat, region and partner; a mismatch is a - // misconfigured request rather than something to silently route on imp[0]'s key. - private ExtImpFloxis resolveCommonImpExt(List imps) { - final ExtImpFloxis first = parseImpExt(imps.getFirst()); - for (Imp imp : imps.subList(1, imps.size())) { - final ExtImpFloxis current = parseImpExt(imp); - if (!Objects.equals(current.getSeat(), first.getSeat()) - || !Objects.equals(current.getRegion(), first.getRegion()) - || !Objects.equals(current.getPartner(), first.getPartner())) { - throw new PreBidException( - "all impressions must target the same Floxis seat, region and partner; " - + "imp %s differs from imp %s" - .formatted(imp.getId(), imps.getFirst().getId())); - } - } - return first; - } - private ExtImpFloxis parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), FLOXIS_EXT_TYPE_REFERENCE).getBidder(); @@ -103,23 +87,18 @@ private ExtImpFloxis parseImpExt(Imp imp) { } } - // {{Host}} subdomain label for the endpoint template (which pins the fixed .floxis.tech - // domain): the region, or partner-region for a named supply partner (floxis has no prefix). - private static String resolveBidHost(String region, String partner) { - final String resolvedRegion = isBlank(region) ? DEFAULT_REGION : region; - final String resolvedPartner = isBlank(partner) ? DEFAULT_PARTNER : partner; - if (!HOST_LABEL.matcher(resolvedRegion).matches() || !HOST_LABEL.matcher(resolvedPartner).matches()) { + private static void validateImpExt(ExtImpFloxis impExt, + ExtImpFloxis firstImpExt, + String impId, + String firstImpId) { + + if (!Objects.equals(impExt.getSeat(), firstImpExt.getSeat()) + || !Objects.equals(impExt.getRegion(), firstImpExt.getRegion()) + || !Objects.equals(impExt.getPartner(), firstImpExt.getPartner())) { throw new PreBidException( - "invalid Floxis region or partner; both must be DNS labels: region=%s partner=%s" - .formatted(resolvedRegion, resolvedPartner)); + "all impressions must target the same Floxis seat, region and partner; " + + "imp %s differs from imp %s".formatted(impId, firstImpId)); } - return resolvedPartner.equals(DEFAULT_PARTNER) - ? resolvedRegion - : resolvedPartner + "-" + resolvedRegion; - } - - private static boolean isBlank(String value) { - return value == null || value.isEmpty(); } private static String resolveUrl(String endpoint, ExtImpFloxis extImp) { @@ -128,6 +107,14 @@ private static String resolveUrl(String endpoint, ExtImpFloxis extImp) { .replace(SEAT_MACRO, HttpUtil.encodeUrl(extImp.getSeat())); } + private static String resolveBidHost(String region, String partner) { + final String resolvedRegion = StringUtils.isBlank(region) ? DEFAULT_REGION : region; + final String resolvedPartner = StringUtils.isBlank(partner) ? DEFAULT_PARTNER : partner; + return resolvedPartner.equals(DEFAULT_PARTNER) + ? HttpUtil.encodeUrl(resolvedRegion) + : HttpUtil.encodeUrl(resolvedPartner) + "-" + HttpUtil.encodeUrl(resolvedRegion); + } + @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { final BidResponse bidResponse; @@ -136,7 +123,10 @@ public Result> makeBids(BidderCall httpCall, BidRequ } catch (DecodeException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } + return extractBids(bidResponse, bidRequest); + } + private static Result> extractBids(BidResponse bidResponse, BidRequest bidRequest) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Result.empty(); } @@ -159,9 +149,6 @@ public Result> makeBids(BidderCall httpCall, BidRequ return Result.of(bids, errors); } - // Resolves the bid's media type. When bid.mtype (OpenRTB 2.6) is set it is treated as - // authoritative. When unset, a single-format imp's media type is used; multi-format imps - // without mtype cannot be disambiguated and surface an error. private static BidType getMediaTypeForBid(List imps, Bid bid) { final Integer mtype = bid.getMtype(); if (mtype != null && mtype != 0) { @@ -175,39 +162,43 @@ private static BidType getMediaTypeForBid(List imps, Bid bid) { }; } - for (Imp imp : imps) { - if (!Objects.equals(imp.getId(), bid.getImpid())) { - continue; - } - int formats = 0; - BidType resolved = null; - if (imp.getBanner() != null) { - formats++; - resolved = BidType.banner; - } - if (imp.getVideo() != null) { - formats++; - resolved = BidType.video; - } - if (imp.getAudio() != null) { - formats++; - resolved = BidType.audio; - } - if (imp.getXNative() != null) { - formats++; - resolved = BidType.xNative; - } - if (formats == 1) { - return resolved; - } else if (formats > 1) { - throw new PreBidException( - "bid for multi-format imp %s requires bid.mtype to disambiguate".formatted(bid.getImpid())); - } else { - throw new PreBidException( - "unable to resolve media type for impression %s".formatted(bid.getImpid())); - } + final Imp imp = imps.stream() + .filter(currentImp -> Objects.equals(currentImp.getId(), bid.getImpid())) + .findFirst() + .orElseThrow(() -> new PreBidException( + "unable to find impression %s for bid".formatted(bid.getImpid()))); + + if (countFormats(imp) != 1) { + throw new PreBidException( + "unable to resolve a single media type for impression %s; set bid.mtype" + .formatted(bid.getImpid())); } - throw new PreBidException("unable to find impression %s for bid".formatted(bid.getImpid())); + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getAudio() != null) { + return BidType.audio; + } else { + return BidType.xNative; + } + } + + private static int countFormats(Imp imp) { + int formats = 0; + if (imp.getBanner() != null) { + formats++; + } + if (imp.getVideo() != null) { + formats++; + } + if (imp.getAudio() != null) { + formats++; + } + if (imp.getXNative() != null) { + formats++; + } + return formats; } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java index 7d261681797..e1302852cde 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java @@ -1,17 +1,13 @@ package org.prebid.server.proto.openrtb.ext.request.floxis; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; @Value(staticConstructor = "of") public class ExtImpFloxis { - @JsonProperty("seat") String seat; - @JsonProperty("region") String region; - @JsonProperty("partner") String partner; } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java index 7e0eff122bb..128e31b20e3 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java @@ -16,8 +16,7 @@ import jakarta.validation.constraints.NotBlank; @Configuration -@PropertySource(value = "classpath:/bidder-config/floxis.yaml", - factory = YamlPropertySourceFactory.class) +@PropertySource(value = "classpath:/bidder-config/floxis.yaml", factory = YamlPropertySourceFactory.class) public class FloxisConfiguration { private static final String BIDDER_NAME = "floxis"; diff --git a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java index d1fa11c09ab..ed31f14cc1b 100644 --- a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java @@ -45,20 +45,6 @@ public void creationShouldFailOnInvalidEndpointUrl() { .isThrownBy(() -> new FloxisBidder("invalid_url", jacksonMapper)); } - @Test - public void makeHttpRequestsShouldReturnErrorWhenNoImpressions() { - // given - final BidRequest bidRequest = BidRequest.builder().id("req-1").imp(null).build(); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("no impressions in the bid request")); - } - @Test public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { // given @@ -154,17 +140,18 @@ public void makeHttpRequestsShouldUseArbitraryValidRegionLabelAsSubdomain() { } @Test - public void makeHttpRequestsShouldReturnBadInputWhenRegionIsNotAValidHostLabel() { + public void makeHttpRequestsShouldUrlEncodeHostParts() { // given - final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "evil.com/x?"))); + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "a b", "x y"))); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input)); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://x+y-a+b.floxis.tech/pbs?seat=abc"); } @Test @@ -182,20 +169,6 @@ public void makeHttpRequestsShouldUsePartnerPrefixedHostWhenPartnerProvided() { .containsExactly("https://acme-us-e.floxis.tech/pbs?seat=abc"); } - @Test - public void makeHttpRequestsShouldReturnBadInputWhenPartnerIsNotAValidHostLabel() { - // given - final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "us-e", "evil.com/x?"))); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input)); - } - @Test public void makeHttpRequestsShouldRouteOnceAndForwardAllImpsWhenSeatAndRegionMatch() { // given @@ -455,7 +428,7 @@ public void makeBidsShouldReturnErrorForMultiFormatImpWithoutMType() throws Exce assertThat(result.getValue()).isEmpty(); assertThat(result.getErrors()).hasSize(1) .containsOnly(BidderError.badServerResponse( - "bid for multi-format imp imp-1 requires bid.mtype to disambiguate")); + "unable to resolve a single media type for impression imp-1; set bid.mtype")); } @Test @@ -474,7 +447,7 @@ public void makeBidsShouldReturnErrorForImpWithoutFormatAndWithoutMType() throws assertThat(result.getValue()).isEmpty(); assertThat(result.getErrors()).hasSize(1) .containsOnly(BidderError.badServerResponse( - "unable to resolve media type for impression imp-1")); + "unable to resolve a single media type for impression imp-1; set bid.mtype")); } @Test From 1682006ad2d79ef6dd90efccb55eb73ffe7d14af Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Sat, 8 Aug 2026 23:05:26 +0300 Subject: [PATCH 08/10] Floxis adapter: use Uri for endpoint macros and apply review feedback Merge master and switch the endpoint from manual {{Macro}} substitution to org.prebid.server.util.Uri, dropping our HttpUtil.encodeUrl calls so dynamic parts are encoded once by the template instead of twice. Update the usersync redirect URL to the single-brace macro form. Compare imp exts with equals (Lombok @Value supplies it) rather than field by field. Move ObjectNode to imports. Drop the seat query param from the integration-test endpoint and its wiremock stub; seat routing is covered by unit tests, plus a new one pinning that a macro-less endpoint resolves unchanged. Master removed UsersyncerCreator (#4464); usersync is now assembled from bidder config, so drop the wiring. --- .../server/bidder/floxis/FloxisBidder.java | 28 +++++++++---------- .../config/bidder/FloxisConfiguration.java | 7 ----- src/main/resources/bidder-config/floxis.yaml | 4 +-- .../bidder/floxis/FloxisBidderTest.java | 28 +++++++++++++++---- .../java/org/prebid/server/it/FloxisTest.java | 2 -- .../server/it/test-application.properties | 2 +- 6 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java index c3790a0e8a4..67e11c124c6 100644 --- a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java +++ b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java @@ -23,6 +23,7 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.Uri; import java.util.ArrayList; import java.util.List; @@ -34,17 +35,17 @@ public class FloxisBidder implements Bidder { new TypeReference<>() { }; - private static final String HOST_MACRO = "{{Host}}"; - private static final String SEAT_MACRO = "{{SeatId}}"; + private static final String HOST_MACRO = "Host"; + private static final String SEAT_MACRO = "SeatId"; private static final String DEFAULT_REGION = "us-e"; private static final String DEFAULT_PARTNER = "floxis"; - private final String endpointUrl; + private final Uri endpointUrl; private final JacksonMapper mapper; public FloxisBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.endpointUrl = Uri.of(endpointUrl); this.mapper = Objects.requireNonNull(mapper); } @@ -71,7 +72,7 @@ public Result>> makeHttpRequests(BidRequest request return Result.withValue(HttpRequest.builder() .method(HttpMethod.POST) - .uri(resolveUrl(endpointUrl, firstImpExt)) + .uri(resolveUrl(firstImpExt)) .headers(HttpUtil.headers()) .impIds(BidderUtil.impIds(request)) .payload(request) @@ -92,27 +93,26 @@ private static void validateImpExt(ExtImpFloxis impExt, String impId, String firstImpId) { - if (!Objects.equals(impExt.getSeat(), firstImpExt.getSeat()) - || !Objects.equals(impExt.getRegion(), firstImpExt.getRegion()) - || !Objects.equals(impExt.getPartner(), firstImpExt.getPartner())) { + if (!impExt.equals(firstImpExt)) { throw new PreBidException( "all impressions must target the same Floxis seat, region and partner; " + "imp %s differs from imp %s".formatted(impId, firstImpId)); } } - private static String resolveUrl(String endpoint, ExtImpFloxis extImp) { - return endpoint - .replace(HOST_MACRO, resolveBidHost(extImp.getRegion(), extImp.getPartner())) - .replace(SEAT_MACRO, HttpUtil.encodeUrl(extImp.getSeat())); + private String resolveUrl(ExtImpFloxis extImp) { + return endpointUrl + .replaceMacro(HOST_MACRO, resolveBidHost(extImp.getRegion(), extImp.getPartner())) + .replaceMacro(SEAT_MACRO, extImp.getSeat()) + .expand(); } private static String resolveBidHost(String region, String partner) { final String resolvedRegion = StringUtils.isBlank(region) ? DEFAULT_REGION : region; final String resolvedPartner = StringUtils.isBlank(partner) ? DEFAULT_PARTNER : partner; return resolvedPartner.equals(DEFAULT_PARTNER) - ? HttpUtil.encodeUrl(resolvedRegion) - : HttpUtil.encodeUrl(resolvedPartner) + "-" + HttpUtil.encodeUrl(resolvedRegion); + ? resolvedRegion + : resolvedPartner + "-" + resolvedRegion; } @Override diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java index 128e31b20e3..896ac465fbe 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java @@ -5,16 +5,12 @@ 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/floxis.yaml", factory = YamlPropertySourceFactory.class) public class FloxisConfiguration { @@ -29,14 +25,11 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps floxisBidderDeps(BidderConfigurationProperties floxisConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(floxisConfigurationProperties) - .usersyncerCreator(UsersyncerCreator.create(externalUrl)) .bidderCreator(config -> new FloxisBidder(config.getEndpoint(), mapper)) .assemble(); } - } diff --git a/src/main/resources/bidder-config/floxis.yaml b/src/main/resources/bidder-config/floxis.yaml index 0a060c2d3f1..68f99974e8e 100644 --- a/src/main/resources/bidder-config/floxis.yaml +++ b/src/main/resources/bidder-config/floxis.yaml @@ -1,6 +1,6 @@ adapters: floxis: - endpoint: https://{{Host}}.floxis.tech/pbs?seat={{SeatId}} + endpoint: https://{Host}.floxis.tech/pbs?seat={SeatId} ortb-version: "2.6" modifying-vast-xml-allowed: false meta-info: @@ -20,6 +20,6 @@ adapters: usersync: cookie-family-name: floxis redirect: - url: "https://px-us-e.floxis.tech/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&us_privacy={{us_privacy}}&dest={{redirect_url}}" + url: "https://px-us-e.floxis.tech/sync?gdpr={gdpr}&gdpr_consent={gdpr_consent}&gpp={gpp}&gpp_sid={gpp_sid}&us_privacy={us_privacy}&dest={redirect_url}" support-cors: false uid-macro: "${USER_ID}" diff --git a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java index ed31f14cc1b..738677f1a6f 100644 --- a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java @@ -1,5 +1,6 @@ package org.prebid.server.bidder.floxis; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; @@ -35,7 +36,7 @@ public class FloxisBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://{{Host}}.floxis.tech/pbs?seat={{SeatId}}"; + private static final String ENDPOINT_URL = "https://{Host}.floxis.tech/pbs?seat={SeatId}"; private final FloxisBidder target = new FloxisBidder(ENDPOINT_URL, jacksonMapper); @@ -76,7 +77,7 @@ public void makeHttpRequestsShouldUrlEscapeSeatAndUseEuHost() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getUri) - .containsExactly("https://eu.floxis.tech/pbs?seat=a+b%26c"); + .containsExactly("https://eu.floxis.tech/pbs?seat=a%20b%26c"); } @Test @@ -151,7 +152,7 @@ public void makeHttpRequestsShouldUrlEncodeHostParts() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getUri) - .containsExactly("https://x+y-a+b.floxis.tech/pbs?seat=abc"); + .containsExactly("https://x%20y-a%20b.floxis.tech/pbs?seat=abc"); } @Test @@ -169,6 +170,22 @@ public void makeHttpRequestsShouldUsePartnerPrefixedHostWhenPartnerProvided() { .containsExactly("https://acme-us-e.floxis.tech/pbs?seat=abc"); } + @Test + public void makeHttpRequestsShouldLeaveEndpointUnchangedWhenItDeclaresNoMacros() { + // given + final FloxisBidder bidder = new FloxisBidder("http://localhost:8090/floxis-exchange", jacksonMapper); + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "eu"))); + + // when + final Result>> result = bidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("http://localhost:8090/floxis-exchange"); + } + @Test public void makeHttpRequestsShouldRouteOnceAndForwardAllImpsWhenSeatAndRegionMatch() { // given @@ -485,12 +502,11 @@ private static Imp givenImp(UnaryOperator impCustomizer) { .build(); } - private static com.fasterxml.jackson.databind.node.ObjectNode givenImpExt(String seat, String region) { + private static ObjectNode givenImpExt(String seat, String region) { return givenImpExt(seat, region, null); } - private static com.fasterxml.jackson.databind.node.ObjectNode givenImpExt(String seat, String region, - String partner) { + private static ObjectNode givenImpExt(String seat, String region, String partner) { return mapper.valueToTree(ExtPrebid.of(null, ExtImpFloxis.of(seat, region, partner))); } diff --git a/src/test/java/org/prebid/server/it/FloxisTest.java b/src/test/java/org/prebid/server/it/FloxisTest.java index 8a67859fe12..429948bfb45 100644 --- a/src/test/java/org/prebid/server/it/FloxisTest.java +++ b/src/test/java/org/prebid/server/it/FloxisTest.java @@ -8,7 +8,6 @@ import java.io.IOException; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 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; @@ -20,7 +19,6 @@ public class FloxisTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromFloxis() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/floxis-exchange")) - .withQueryParam("seat", equalTo("pub-seat-1")) .withRequestBody(equalToJson(jsonFrom("openrtb2/floxis/test-floxis-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/floxis/test-floxis-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 5e8c1f854e6..57f1d879822 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -259,7 +259,7 @@ adapters.flatads.endpoint=http://localhost:8090/flatads-exchange adapters.flipp.enabled=true adapters.flipp.endpoint=http://localhost:8090/flipp-exchange adapters.floxis.enabled=true -adapters.floxis.endpoint=http://localhost:8090/floxis-exchange?seat={{SeatId}} +adapters.floxis.endpoint=http://localhost:8090/floxis-exchange adapters.audiencenetwork.enabled=true adapters.audiencenetwork.endpoint=http://localhost:8090/audienceNetwork-exchange adapters.audiencenetwork.timeout-notification-url-template=http://localhost:8090/audienceNetwork-timeout From 1f993a66b9f176c0252b6093df399dcb2072f54b Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Mon, 10 Aug 2026 09:11:17 +0300 Subject: [PATCH 09/10] Floxis adapter: add http_method to the integration-test bid request fixture The expected outgoing request predates ext.prebid.server.http_method, so the wiremock stub never matched, the exchange returned 404 and the auction came back with no bids. Every other fixture carrying that block already declares it. --- .../server/it/openrtb2/floxis/test-floxis-bid-request.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json index ec7ab98fbd5..cf7ba733859 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json @@ -58,6 +58,7 @@ "externalurl": "http://localhost:8080", "gvlid": 1, "datacenter": "local", + "http_method": "POST", "endpoint": "/openrtb2/auction" } } From 864065187163b8828bdc241d1a82a74b2739e0be Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Mon, 10 Aug 2026 09:29:15 +0300 Subject: [PATCH 10/10] Floxis adapter: resolve region and partner defaults before comparing imp exts The Go port compares resolved hosts, so imps differing only by an omitted region or partner still share a single request. The Java port compared raw ext values and rejected them. Apply the defaults when parsing so equals() sees resolved values, and cover the case the Go suite already pins. --- .../server/bidder/floxis/FloxisBidder.java | 15 ++++++----- .../bidder/floxis/FloxisBidderTest.java | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java index 67e11c124c6..2c33e7ba335 100644 --- a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java +++ b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java @@ -81,11 +81,18 @@ public Result>> makeHttpRequests(BidRequest request } private ExtImpFloxis parseImpExt(Imp imp) { + final ExtImpFloxis impExt; try { - return mapper.mapper().convertValue(imp.getExt(), FLOXIS_EXT_TYPE_REFERENCE).getBidder(); + impExt = mapper.mapper().convertValue(imp.getExt(), FLOXIS_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { throw new PreBidException("invalid imp.ext.bidder for imp %s: %s".formatted(imp.getId(), e.getMessage())); } + + // defaults applied here so imps differing only by an omitted region or partner compare equal + return ExtImpFloxis.of( + impExt.getSeat(), + StringUtils.isBlank(impExt.getRegion()) ? DEFAULT_REGION : impExt.getRegion(), + StringUtils.isBlank(impExt.getPartner()) ? DEFAULT_PARTNER : impExt.getPartner()); } private static void validateImpExt(ExtImpFloxis impExt, @@ -108,11 +115,7 @@ private String resolveUrl(ExtImpFloxis extImp) { } private static String resolveBidHost(String region, String partner) { - final String resolvedRegion = StringUtils.isBlank(region) ? DEFAULT_REGION : region; - final String resolvedPartner = StringUtils.isBlank(partner) ? DEFAULT_PARTNER : partner; - return resolvedPartner.equals(DEFAULT_PARTNER) - ? resolvedRegion - : resolvedPartner + "-" + resolvedRegion; + return partner.equals(DEFAULT_PARTNER) ? region : partner + "-" + region; } @Override diff --git a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java index 738677f1a6f..d361c2f964b 100644 --- a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java @@ -212,6 +212,32 @@ public void makeHttpRequestsShouldRouteOnceAndForwardAllImpsWhenSeatAndRegionMat .containsExactly("imp-1", "imp-2"); } + @Test + public void makeHttpRequestsShouldRouteOnceWhenImpsResolveToSameHostThroughDefaults() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-us", null))), + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-us", "us-e", "floxis"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://us-e.floxis.tech/pbs?seat=seat-us"); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("imp-1", "imp-2"); + } + @Test public void makeHttpRequestsShouldReturnErrorWhenImpsTargetDifferentSeat() { // given