From 2f705f39ee6818d69765523d7ffa0e803eb95478 Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts Date: Sun, 6 Sep 2026 10:20:35 +0200 Subject: [PATCH 1/2] Fix OpenRTB request model compatibility --- docs/developers/openrtb-request-model.md | 30 +++ .../java/com/iab/openrtb/request/Audio.java | 2 +- .../java/com/iab/openrtb/request/Banner.java | 24 ++ .../java/com/iab/openrtb/request/Content.java | 28 ++- .../java/com/iab/openrtb/request/Data.java | 5 + .../java/com/iab/openrtb/request/Video.java | 8 +- .../model/request/auction/Audio.groovy | 2 +- .../model/request/auction/Video.groovy | 2 +- .../functional/tests/OrtbConverterSpec.groovy | 8 +- .../server/json/OpenRtbRequestModelTest.java | 219 ++++++++++++++++++ 10 files changed, 319 insertions(+), 9 deletions(-) create mode 100644 docs/developers/openrtb-request-model.md create mode 100644 src/test/java/org/prebid/server/json/OpenRtbRequestModelTest.java diff --git a/docs/developers/openrtb-request-model.md b/docs/developers/openrtb-request-model.md new file mode 100644 index 00000000000..9cd061ad4e4 --- /dev/null +++ b/docs/developers/openrtb-request-model.md @@ -0,0 +1,30 @@ +# OpenRTB request model compatibility + +The request classes in `com.iab.openrtb.request` represent OpenRTB 2.6 and retain selected legacy fields for OpenRTB 2.5 interoperability. Optional fields remain unset when absent from the input; the model does not insert specification defaults. + +## OpenRTB 2.6 fields + +`Video.podid` and `Audio.podid` are strings, as defined in the IAB [Video](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#objectvideo) and [Audio](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#objectaudio) tables. This preserves identifiers such as `pod-001` and numeric-looking strings with leading zeros. Numeric JSON input is accepted by the existing Jackson coercion and serialized as a string. Java callers must supply a `String` to the builders and recompile against the updated model. + +The separate `/openrtb2/video` endpoint uses its own numeric pod IDs, which are not these OpenRTB impression fields. + +This matches the Go models in `prebid/openrtb` used by Prebid Server: [Video](https://github.com/prebid/openrtb/blob/v20.3.0/openrtb2/video.go) and [Audio](https://github.com/prebid/openrtb/blob/v20.3.0/openrtb2/audio.go). The corresponding Video type correction was merged in [prebid/openrtb#2](https://github.com/prebid/openrtb/pull/2). + +The [Content](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#objectcontent) model includes `gtax`, `genres`, `realtime`, and `firstbroadcast`. The [Data](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#objectdata) model includes `cids` for extended content identifiers. These fields survive JSON decoding and encoding in site, app, and DOOH content. + +In the current 2.6 specification, `content.livestream` describes scheduled versus on-demand delivery, while `content.realtime` describes whether the event is happening in real time. Earlier definitions of `livestream` described live content. The model preserves the supplied numeric value; adapters should interpret it according to the OpenRTB version they use. + +## Legacy OpenRTB 2.5 fields + +The following fields were already deprecated by OpenRTB 2.5 and removed in 2.6. They are retained so that a 2.5 request does not lose them when decoded and encoded through the shared model. + +| Field | Purpose in 2.5 | Replacement in 2.6 | +| --- | --- | --- | +| `banner.wmax`, `banner.hmax` | Maximum banner dimensions | `banner.format` | +| `banner.wmin`, `banner.hmin` | Minimum banner dimensions | `banner.format` | +| `video.protocol` | Single supported video protocol | `video.protocols` | +| `content.videoquality` | Video production quality | `content.prodq` | + +See the [IAB specification change log](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#appendixb). Retaining these fields does not make them OpenRTB 2.6 attributes. The shared model preserves supplied values independently of their replacements; callers producing strictly 2.6 requests should use the replacements and omit the legacy fields. + +`user.language` is not a standard OpenRTB request attribute and is not included in the model. diff --git a/src/main/java/com/iab/openrtb/request/Audio.java b/src/main/java/com/iab/openrtb/request/Audio.java index 58901a625b3..3f1fccce76b 100644 --- a/src/main/java/com/iab/openrtb/request/Audio.java +++ b/src/main/java/com/iab/openrtb/request/Audio.java @@ -81,7 +81,7 @@ public class Audio { * within a bid request share the same podid, this indicates that * those impression opportunities belong to the same audio ad pod. */ - Integer podid; + String podid; /** * The sequence (position) of the audio ad pod within a diff --git a/src/main/java/com/iab/openrtb/request/Banner.java b/src/main/java/com/iab/openrtb/request/Banner.java index b24584b110b..3f34ab3ea21 100644 --- a/src/main/java/com/iab/openrtb/request/Banner.java +++ b/src/main/java/com/iab/openrtb/request/Banner.java @@ -44,6 +44,30 @@ public class Banner { */ Integer h; + /** + * Maximum width in device independent pixels (DIPS), retained for OpenRTB 2.5 round trips. + * Deprecated as of OpenRTB 2.5 and removed in 2.6 in favor of format. + */ + Integer wmax; + + /** + * Maximum height in device independent pixels (DIPS), retained for OpenRTB 2.5 round trips. + * Deprecated as of OpenRTB 2.5 and removed in 2.6 in favor of format. + */ + Integer hmax; + + /** + * Minimum width in device independent pixels (DIPS), retained for OpenRTB 2.5 round trips. + * Deprecated as of OpenRTB 2.5 and removed in 2.6 in favor of format. + */ + Integer wmin; + + /** + * Minimum height in device independent pixels (DIPS), retained for OpenRTB 2.5 round trips. + * Deprecated as of OpenRTB 2.5 and removed in 2.6 in favor of format. + */ + Integer hmin; + /** * Blocked banner ad types. * Values: diff --git a/src/main/java/com/iab/openrtb/request/Content.java b/src/main/java/com/iab/openrtb/request/Content.java index 486630d8eed..0419512c7a8 100644 --- a/src/main/java/com/iab/openrtb/request/Content.java +++ b/src/main/java/com/iab/openrtb/request/Content.java @@ -64,6 +64,16 @@ public class Content { */ String genre; + /** + * Taxonomy used by genres. If omitted, Content Category Taxonomy 3.1 (9) is assumed. + */ + Integer gtax; + + /** + * Genre IDs from the taxonomy specified by gtax. + */ + List genres; + /** * Album to which the content belongs; typically for audio. */ @@ -103,6 +113,12 @@ public class Content { */ Integer prodq; + /** + * Video production quality retained for OpenRTB 2.5 round trips. + * Deprecated as of OpenRTB 2.5 and removed in 2.6 in favor of prodq. + */ + Integer videoquality; + /** * Type of content (game, video, text, etc.). Refer to * List: Content Contexts in AdCOM 1.0. @@ -136,7 +152,7 @@ public class Content { List kwarray; /** - * 0 = not live, 1 = content is live (e.g., stream, live blog). + * Indicates whether the broadcast is scheduled: 0 = on-demand, 1 = scheduled (linear viewing). */ Integer livestream; @@ -182,6 +198,16 @@ public class Content { */ Channel channel; + /** + * Indicates whether the event is happening in real time: 0 = replay, 1 = real time. + */ + Integer realtime; + + /** + * Indicates whether this is the first broadcast of the content: 0 = no, 1 = yes. + */ + Integer firstbroadcast; + /** * Placeholder for exchange-specific extensions to OpenRTB. */ diff --git a/src/main/java/com/iab/openrtb/request/Data.java b/src/main/java/com/iab/openrtb/request/Data.java index fd48b7100b8..49a1ee7b7a7 100644 --- a/src/main/java/com/iab/openrtb/request/Data.java +++ b/src/main/java/com/iab/openrtb/request/Data.java @@ -28,6 +28,11 @@ public class Data { */ String name; + /** + * Extended video or audio content IDs from the source specified by name. + */ + List cids; + /** * Array of {@link Segment} (Section 3.2.22) objects that contain the actual data * values. diff --git a/src/main/java/com/iab/openrtb/request/Video.java b/src/main/java/com/iab/openrtb/request/Video.java index 369d576a3ac..b476ee4c8fb 100644 --- a/src/main/java/com/iab/openrtb/request/Video.java +++ b/src/main/java/com/iab/openrtb/request/Video.java @@ -74,6 +74,12 @@ public class Video { */ List protocols; + /** + * Video protocol retained for OpenRTB 2.5 round trips. + * Deprecated as of OpenRTB 2.5 and removed in 2.6 in favor of protocols. + */ + Integer protocol; + /** * Width of the video player in device independent pixels (DIPS). */ @@ -90,7 +96,7 @@ public class Video { * within a bid request share the same podid, this indicates that * those impression opportunities belong to the same video ad pod. */ - Integer podid; + String podid; /** * The sequence (position) of the video ad pod within a diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Audio.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Audio.groovy index 57d9bbd40ee..e262da95e35 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Audio.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Audio.groovy @@ -14,7 +14,7 @@ class Audio { List protocols Integer startdelay List rqddurs - Integer podid + String podid Integer podseq Integer sequence Integer slotinpod diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy index 53e86c5ed28..e71191dc113 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy @@ -19,7 +19,7 @@ class Video { Integer width @JsonProperty("h") Integer height - Integer podid + String podid Integer podseq List rqddurs VideoPlacementSubtypes placement diff --git a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy index 21bb80df135..af7aa064ceb 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy @@ -556,7 +556,7 @@ class OrtbConverterSpec extends BaseSpec { rqddurs = [PBSUtils.randomNumber] maxseq = PBSUtils.randomNumber poddur = PBSUtils.randomNumber - podid = PBSUtils.randomNumber + podid = PBSUtils.randomString podseq = PBSUtils.randomNumber mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber @@ -580,7 +580,7 @@ class OrtbConverterSpec extends BaseSpec { rqddurs = [PBSUtils.randomNumber] maxseq = PBSUtils.randomNumber poddur = PBSUtils.randomNumber - podid = PBSUtils.randomNumber + podid = PBSUtils.randomString podseq = PBSUtils.randomNumber mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber @@ -604,7 +604,7 @@ class OrtbConverterSpec extends BaseSpec { rqddurs = [PBSUtils.randomNumber] maxseq = PBSUtils.randomNumber poddur = PBSUtils.randomNumber - podid = PBSUtils.randomNumber + podid = PBSUtils.randomString podseq = PBSUtils.randomNumber mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber @@ -626,7 +626,7 @@ class OrtbConverterSpec extends BaseSpec { rqddurs = [PBSUtils.randomNumber] maxseq = PBSUtils.randomNumber poddur = PBSUtils.randomNumber - podid = PBSUtils.randomNumber + podid = PBSUtils.randomString podseq = PBSUtils.randomNumber mincpmpersec = BigDecimal.valueOf(1) slotinpod = PBSUtils.randomNumber diff --git a/src/test/java/org/prebid/server/json/OpenRtbRequestModelTest.java b/src/test/java/org/prebid/server/json/OpenRtbRequestModelTest.java new file mode 100644 index 00000000000..4c2984c43ee --- /dev/null +++ b/src/test/java/org/prebid/server/json/OpenRtbRequestModelTest.java @@ -0,0 +1,219 @@ +package org.prebid.server.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Content; +import com.iab.openrtb.request.Video; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OpenRtbRequestModelTest extends VertxTest { + + private final JacksonMapper target = jacksonMapper; + + @Test + public void decodeValueShouldPreserveOpenRtb25BannerSizeBounds() { + // given + final String json = """ + {"imp":[{"banner":{"wmax":970,"hmax":250,"wmin":300,"hmin":50}}]} + """; + + // when + final BidRequest result = target.decodeValue(json, BidRequest.class); + + // then + assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); + } + + @Test + public void decodeValueShouldPreserveOpenRtb25VideoProtocol() { + // given + final String json = """ + {"imp":[{"video":{"protocol":3,"protocols":[2,3]}}]} + """; + + // when + final BidRequest result = target.decodeValue(json, BidRequest.class); + + // then + assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); + } + + @Test + public void decodeValueShouldPreserveOpenRtb25ContentVideoQuality() { + // given + final String json = "{\"site\":{\"content\":{\"videoquality\":0,\"prodq\":1}}}"; + + // when + final BidRequest result = target.decodeValue(json, BidRequest.class); + + // then + assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); + } + + @Test + public void decodeValueShouldPreserveVideoPodId() { + // given + final String json = """ + {"imp":[{"video":{"podid":"pod-001"}}]} + """; + + // when + final BidRequest result = target.decodeValue(json, BidRequest.class); + + // then + assertThat(result.getImp().getFirst().getVideo().getPodid()).isEqualTo("pod-001"); + assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); + } + + @Test + public void decodeValueShouldPreserveAudioPodId() { + // given + final String json = """ + {"imp":[{"audio":{"podid":"pod-001"}}]} + """; + + // when + final BidRequest result = target.decodeValue(json, BidRequest.class); + + // then + assertThat(result.getImp().getFirst().getAudio().getPodid()).isEqualTo("pod-001"); + assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); + } + + @Test + public void decodeValueShouldAcceptNumericVideoPodId() { + // given + final String json = "{\"podid\":123}"; + + // when + final Video result = target.decodeValue(json, Video.class); + + // then + assertThat(result.getPodid()).isEqualTo("123"); + assertThat(target.encodeToString(result)).isEqualTo("{\"podid\":\"123\"}"); + } + + @Test + public void decodeValueShouldPreserveLeadingZerosInVideoPodId() { + // given + final String json = "{\"podid\":\"00123\"}"; + + // when + final Video result = target.decodeValue(json, Video.class); + + // then + assertThat(result.getPodid()).isEqualTo("00123"); + assertThat(target.encodeToString(result)).isEqualTo(json); + } + + @Test + public void decodeValueShouldPreserveLeadingZerosInAudioPodId() { + // given + final String json = "{\"podid\":\"00123\"}"; + + // when + final Audio result = target.decodeValue(json, Audio.class); + + // then + assertThat(result.getPodid()).isEqualTo("00123"); + assertThat(target.encodeToString(result)).isEqualTo(json); + } + + @Test + public void decodeValueShouldAcceptNumericAudioPodId() { + // given + final String json = "{\"podid\":123}"; + + // when + final Audio result = target.decodeValue(json, Audio.class); + + // then + assertThat(result.getPodid()).isEqualTo("123"); + assertThat(target.encodeToString(result)).isEqualTo("{\"podid\":\"123\"}"); + } + + @Test + public void decodeValueShouldPreserveSiteContentFields() { + // given + final String json = """ + {"site":{"content":{ + "gtax":9,"genres":["1","2"],"realtime":1,"firstbroadcast":0, + "data":[{"name":"content-provider","cids":["content-001","content-002"]}] + }}} + """; + + // when + final BidRequest result = target.decodeValue(json, BidRequest.class); + + // then + assertThat(mapper.valueToTree(result)) + .isEqualTo(target.decodeValue(json, JsonNode.class)); + assertThat(result.getSite().getContent().isEmpty()).isFalse(); + } + + @Test + public void decodeValueShouldPreserveAppContentFields() { + // given + final String json = """ + {"app":{"content":{ + "gtax":9,"genres":["1","2"],"realtime":0,"firstbroadcast":1, + "data":[{"name":"content-provider","cids":["content-001","content-002"]}] + }}} + """; + + // when + final BidRequest result = target.decodeValue(json, BidRequest.class); + + // then + assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); + } + + @Test + public void decodeValueShouldPreserveDoohContentFields() { + // given + final String json = """ + {"dooh":{"content":{ + "gtax":9,"genres":["1","2"],"realtime":1,"firstbroadcast":1, + "data":[{"name":"content-provider","cids":["content-001","content-002"]}] + }}} + """; + + // when + final BidRequest result = target.decodeValue(json, BidRequest.class); + + // then + assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); + } + + @Test + public void decodeValueShouldLeaveMissingContentFieldsUnset() { + // when + final Content result = target.decodeValue("{}", Content.class); + + // then + assertThat(result.getGtax()).isNull(); + assertThat(result.getGenres()).isNull(); + assertThat(result.getRealtime()).isNull(); + assertThat(result.getFirstbroadcast()).isNull(); + assertThat(result.isEmpty()).isTrue(); + assertThat(target.encodeToString(result)).isEqualTo("{}"); + } + + @Test + public void decodeValueShouldPreserveEmptyContentArrays() { + // given + final String json = "{\"genres\":[],\"data\":[{\"cids\":[]}]}"; + + // when + final Content result = target.decodeValue(json, Content.class); + + // then + assertThat(result.getGenres()).isEmpty(); + assertThat(result.getData().getFirst().getCids()).isEmpty(); + assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); + } +} From 1cfe6787f8bd96026dfd64ab9c781f275d0e2bfd Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts Date: Fri, 11 Sep 2026 12:43:18 +0200 Subject: [PATCH 2/2] Preserve legacy OpenRTB fields in protobuf requests --- docs/developers/openrtb-request-model.md | 30 --- .../request/ProtobufRequestUtils.java | 6 + .../model/request/auction/Content.groovy | 4 + .../model/request/auction/Data.groovy | 1 + .../functional/tests/OrtbConverterSpec.groovy | 39 ++++ .../server/json/OpenRtbRequestModelTest.java | 219 ------------------ .../request/ProtobufRequestUtilsTest.java | 55 +++++ 7 files changed, 105 insertions(+), 249 deletions(-) delete mode 100644 docs/developers/openrtb-request-model.md delete mode 100644 src/test/java/org/prebid/server/json/OpenRtbRequestModelTest.java diff --git a/docs/developers/openrtb-request-model.md b/docs/developers/openrtb-request-model.md deleted file mode 100644 index 9cd061ad4e4..00000000000 --- a/docs/developers/openrtb-request-model.md +++ /dev/null @@ -1,30 +0,0 @@ -# OpenRTB request model compatibility - -The request classes in `com.iab.openrtb.request` represent OpenRTB 2.6 and retain selected legacy fields for OpenRTB 2.5 interoperability. Optional fields remain unset when absent from the input; the model does not insert specification defaults. - -## OpenRTB 2.6 fields - -`Video.podid` and `Audio.podid` are strings, as defined in the IAB [Video](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#objectvideo) and [Audio](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#objectaudio) tables. This preserves identifiers such as `pod-001` and numeric-looking strings with leading zeros. Numeric JSON input is accepted by the existing Jackson coercion and serialized as a string. Java callers must supply a `String` to the builders and recompile against the updated model. - -The separate `/openrtb2/video` endpoint uses its own numeric pod IDs, which are not these OpenRTB impression fields. - -This matches the Go models in `prebid/openrtb` used by Prebid Server: [Video](https://github.com/prebid/openrtb/blob/v20.3.0/openrtb2/video.go) and [Audio](https://github.com/prebid/openrtb/blob/v20.3.0/openrtb2/audio.go). The corresponding Video type correction was merged in [prebid/openrtb#2](https://github.com/prebid/openrtb/pull/2). - -The [Content](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#objectcontent) model includes `gtax`, `genres`, `realtime`, and `firstbroadcast`. The [Data](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#objectdata) model includes `cids` for extended content identifiers. These fields survive JSON decoding and encoding in site, app, and DOOH content. - -In the current 2.6 specification, `content.livestream` describes scheduled versus on-demand delivery, while `content.realtime` describes whether the event is happening in real time. Earlier definitions of `livestream` described live content. The model preserves the supplied numeric value; adapters should interpret it according to the OpenRTB version they use. - -## Legacy OpenRTB 2.5 fields - -The following fields were already deprecated by OpenRTB 2.5 and removed in 2.6. They are retained so that a 2.5 request does not lose them when decoded and encoded through the shared model. - -| Field | Purpose in 2.5 | Replacement in 2.6 | -| --- | --- | --- | -| `banner.wmax`, `banner.hmax` | Maximum banner dimensions | `banner.format` | -| `banner.wmin`, `banner.hmin` | Minimum banner dimensions | `banner.format` | -| `video.protocol` | Single supported video protocol | `video.protocols` | -| `content.videoquality` | Video production quality | `content.prodq` | - -See the [IAB specification change log](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#appendixb). Retaining these fields does not make them OpenRTB 2.6 attributes. The shared model preserves supplied values independently of their replacements; callers producing strictly 2.6 requests should use the replacements and omit the legacy fields. - -`user.language` is not a standard OpenRTB request attribute and is not included in the model. diff --git a/src/main/java/org/prebid/server/protobuf/request/ProtobufRequestUtils.java b/src/main/java/org/prebid/server/protobuf/request/ProtobufRequestUtils.java index b8d0a9ddebe..ff6ba262211 100644 --- a/src/main/java/org/prebid/server/protobuf/request/ProtobufRequestUtils.java +++ b/src/main/java/org/prebid/server/protobuf/request/ProtobufRequestUtils.java @@ -263,6 +263,10 @@ public static ProtobufMapper ProtobufMapper ProtobufMapper genres String album String isrc Producer producer @@ -29,6 +31,8 @@ class Content { String keywords List kwarray Integer livestream + Integer realtime + Integer firstbroadcast Integer sourcerelationship Integer len String language diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Data.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Data.groovy index 894ba61e4cf..9731712864e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Data.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Data.groovy @@ -10,6 +10,7 @@ class Data { String id String name + List cids List segment ExtData ext diff --git a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy index af7aa064ceb..1615d173965 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy @@ -4,6 +4,7 @@ import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.auction.Audio import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Content +import org.prebid.server.functional.model.request.auction.Data import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Dooh import org.prebid.server.functional.model.request.auction.DoohExt @@ -32,6 +33,7 @@ import spock.lang.Shared import static org.prebid.server.functional.model.request.auction.Content.Channel import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE class OrtbConverterSpec extends BaseSpec { @@ -671,6 +673,43 @@ class OrtbConverterSpec extends BaseSpec { assert bidderRequest.imp[0].ssai == ssaiRandomNumber } + def "PBS should preserve content metadata for #distributionChannel when bidder supports ortb #ortbVersion"() { + given: "Content with genre taxonomy, broadcast flags and extended content identifiers" + def content = Content.defaultContent.tap { + gtax = 1 + genres = ["genre-001", "genre-002"] + realtime = realtimeFlag + firstbroadcast = firstbroadcastFlag + data = [new Data(id: "provider-id", cids: ["content-001", "00042"])] + } + def bidRequest = BidRequest.getDefaultBidRequest(distributionChannel).tap { + it[distributionChannel.value].content = content + } + def service = ortbVersion == "2.5" ? prebidServerServiceWithElderOrtb : prebidServerServiceWithNewOrtb + + when: "Requesting a PBS auction" + service.sendAuctionRequest(bidRequest) + + then: "The bidder receives the content metadata unchanged" + def actualContent = bidder.getBidderRequest(bidRequest.id)[distributionChannel.value].content + verifyAll(actualContent) { + gtax == content.gtax + genres == content.genres + realtime == realtimeFlag + firstbroadcast == firstbroadcastFlag + data == content.data + } + + where: + distributionChannel | ortbVersion | realtimeFlag | firstbroadcastFlag + SITE | "2.5" | 0 | 1 + SITE | "2.6" | 1 | 0 + APP | "2.5" | 1 | 0 + APP | "2.6" | 0 | 1 + DOOH | "2.5" | 0 | 1 + DOOH | "2.6" | 1 | 0 + } + def "PBS shouldn't remove site.content.{channel, network} when bidder doesn't support ortb 2.6"() { given: "Default bid request with site.content.{network, channel}" def defaultChannel = Channel.defaultChannel diff --git a/src/test/java/org/prebid/server/json/OpenRtbRequestModelTest.java b/src/test/java/org/prebid/server/json/OpenRtbRequestModelTest.java deleted file mode 100644 index 4c2984c43ee..00000000000 --- a/src/test/java/org/prebid/server/json/OpenRtbRequestModelTest.java +++ /dev/null @@ -1,219 +0,0 @@ -package org.prebid.server.json; - -import com.fasterxml.jackson.databind.JsonNode; -import com.iab.openrtb.request.Audio; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Content; -import com.iab.openrtb.request.Video; -import org.junit.jupiter.api.Test; -import org.prebid.server.VertxTest; - -import static org.assertj.core.api.Assertions.assertThat; - -public class OpenRtbRequestModelTest extends VertxTest { - - private final JacksonMapper target = jacksonMapper; - - @Test - public void decodeValueShouldPreserveOpenRtb25BannerSizeBounds() { - // given - final String json = """ - {"imp":[{"banner":{"wmax":970,"hmax":250,"wmin":300,"hmin":50}}]} - """; - - // when - final BidRequest result = target.decodeValue(json, BidRequest.class); - - // then - assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); - } - - @Test - public void decodeValueShouldPreserveOpenRtb25VideoProtocol() { - // given - final String json = """ - {"imp":[{"video":{"protocol":3,"protocols":[2,3]}}]} - """; - - // when - final BidRequest result = target.decodeValue(json, BidRequest.class); - - // then - assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); - } - - @Test - public void decodeValueShouldPreserveOpenRtb25ContentVideoQuality() { - // given - final String json = "{\"site\":{\"content\":{\"videoquality\":0,\"prodq\":1}}}"; - - // when - final BidRequest result = target.decodeValue(json, BidRequest.class); - - // then - assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); - } - - @Test - public void decodeValueShouldPreserveVideoPodId() { - // given - final String json = """ - {"imp":[{"video":{"podid":"pod-001"}}]} - """; - - // when - final BidRequest result = target.decodeValue(json, BidRequest.class); - - // then - assertThat(result.getImp().getFirst().getVideo().getPodid()).isEqualTo("pod-001"); - assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); - } - - @Test - public void decodeValueShouldPreserveAudioPodId() { - // given - final String json = """ - {"imp":[{"audio":{"podid":"pod-001"}}]} - """; - - // when - final BidRequest result = target.decodeValue(json, BidRequest.class); - - // then - assertThat(result.getImp().getFirst().getAudio().getPodid()).isEqualTo("pod-001"); - assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); - } - - @Test - public void decodeValueShouldAcceptNumericVideoPodId() { - // given - final String json = "{\"podid\":123}"; - - // when - final Video result = target.decodeValue(json, Video.class); - - // then - assertThat(result.getPodid()).isEqualTo("123"); - assertThat(target.encodeToString(result)).isEqualTo("{\"podid\":\"123\"}"); - } - - @Test - public void decodeValueShouldPreserveLeadingZerosInVideoPodId() { - // given - final String json = "{\"podid\":\"00123\"}"; - - // when - final Video result = target.decodeValue(json, Video.class); - - // then - assertThat(result.getPodid()).isEqualTo("00123"); - assertThat(target.encodeToString(result)).isEqualTo(json); - } - - @Test - public void decodeValueShouldPreserveLeadingZerosInAudioPodId() { - // given - final String json = "{\"podid\":\"00123\"}"; - - // when - final Audio result = target.decodeValue(json, Audio.class); - - // then - assertThat(result.getPodid()).isEqualTo("00123"); - assertThat(target.encodeToString(result)).isEqualTo(json); - } - - @Test - public void decodeValueShouldAcceptNumericAudioPodId() { - // given - final String json = "{\"podid\":123}"; - - // when - final Audio result = target.decodeValue(json, Audio.class); - - // then - assertThat(result.getPodid()).isEqualTo("123"); - assertThat(target.encodeToString(result)).isEqualTo("{\"podid\":\"123\"}"); - } - - @Test - public void decodeValueShouldPreserveSiteContentFields() { - // given - final String json = """ - {"site":{"content":{ - "gtax":9,"genres":["1","2"],"realtime":1,"firstbroadcast":0, - "data":[{"name":"content-provider","cids":["content-001","content-002"]}] - }}} - """; - - // when - final BidRequest result = target.decodeValue(json, BidRequest.class); - - // then - assertThat(mapper.valueToTree(result)) - .isEqualTo(target.decodeValue(json, JsonNode.class)); - assertThat(result.getSite().getContent().isEmpty()).isFalse(); - } - - @Test - public void decodeValueShouldPreserveAppContentFields() { - // given - final String json = """ - {"app":{"content":{ - "gtax":9,"genres":["1","2"],"realtime":0,"firstbroadcast":1, - "data":[{"name":"content-provider","cids":["content-001","content-002"]}] - }}} - """; - - // when - final BidRequest result = target.decodeValue(json, BidRequest.class); - - // then - assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); - } - - @Test - public void decodeValueShouldPreserveDoohContentFields() { - // given - final String json = """ - {"dooh":{"content":{ - "gtax":9,"genres":["1","2"],"realtime":1,"firstbroadcast":1, - "data":[{"name":"content-provider","cids":["content-001","content-002"]}] - }}} - """; - - // when - final BidRequest result = target.decodeValue(json, BidRequest.class); - - // then - assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); - } - - @Test - public void decodeValueShouldLeaveMissingContentFieldsUnset() { - // when - final Content result = target.decodeValue("{}", Content.class); - - // then - assertThat(result.getGtax()).isNull(); - assertThat(result.getGenres()).isNull(); - assertThat(result.getRealtime()).isNull(); - assertThat(result.getFirstbroadcast()).isNull(); - assertThat(result.isEmpty()).isTrue(); - assertThat(target.encodeToString(result)).isEqualTo("{}"); - } - - @Test - public void decodeValueShouldPreserveEmptyContentArrays() { - // given - final String json = "{\"genres\":[],\"data\":[{\"cids\":[]}]}"; - - // when - final Content result = target.decodeValue(json, Content.class); - - // then - assertThat(result.getGenres()).isEmpty(); - assertThat(result.getData().getFirst().getCids()).isEmpty(); - assertThat(mapper.valueToTree(result)).isEqualTo(target.decodeValue(json, JsonNode.class)); - } -} diff --git a/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java b/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java index 29e092f73c8..8901b7d918f 100644 --- a/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java +++ b/src/test/java/org/prebid/server/protobuf/request/ProtobufRequestUtilsTest.java @@ -361,6 +361,21 @@ public void bannerMapperShouldReturnValidMapper() { assertThat(result).isEqualTo(expectedResult); } + @Test + public void bannerMapperShouldLeaveMissingLegacyDimensionsUnset() { + // given + final Banner banner = givenBanner().toBuilder().wmax(null).hmax(null).wmin(null).hmin(null).build(); + final ProtobufMapper mapper = + ProtobufRequestUtils.bannerMapper(formatMapper, givenJsonExtensionMapper(OpenRtbTest.banner)); + + // when + final OpenRtb.BidRequest.Imp.Banner result = mapper.map(banner); + + // then + assertThat(result).isEqualTo(givenProtobufBanner().toBuilder() + .clearWmax().clearHmax().clearWmin().clearHmin().build()); + } + @Test public void nativeImageMapperShouldReturnValidMapper() { // when @@ -410,6 +425,20 @@ public void contentMapperShouldReturnValidMapper() { assertThat(result).isEqualTo(expectedResult); } + @Test + public void contentMapperShouldLeaveMissingVideoqualityUnset() { + // given + final Content content = givenContent().toBuilder().videoquality(null).build(); + final ProtobufMapper mapper = ProtobufRequestUtils.contentMapper( + producerMapper, dataMapper, givenJsonExtensionMapper(OpenRtbTest.content)); + + // when + final OpenRtb.BidRequest.Content result = mapper.map(content); + + // then + assertThat(result).isEqualTo(givenProtobufContent().toBuilder().clearVideoquality().build()); + } + @Test public void deviceMapperShouldReturnValidMapper() { // given @@ -811,6 +840,20 @@ public void videoMapperShouldReturnValidMapper() { assertThat(result).isEqualTo(expectedResult); } + @Test + public void videoMapperShouldLeaveMissingProtocolUnset() { + // given + final Video video = givenVideo().toBuilder().protocol(null).build(); + final ProtobufMapper mapper = + ProtobufRequestUtils.videoMapper(bannerMapper, givenJsonExtensionMapper(OpenRtbTest.video)); + + // when + final OpenRtb.BidRequest.Imp.Video result = mapper.map(video); + + // then + assertThat(result).isEqualTo(givenProtobufVideo().toBuilder().clearProtocol().build()); + } + @Test public void userMapperShouldReturnValidMapper() { // given @@ -1158,6 +1201,7 @@ private static Video givenVideo() { .minduration(1) .maxduration(2) .startdelay(3) + .protocol(2) .protocols(singletonList(4)) .w(5) .h(6) @@ -1189,6 +1233,7 @@ private static OpenRtb.BidRequest.Imp.Video givenProtobufVideo() { .setMinduration(1) .setMaxduration(2) .setStartdelay(3) + .setProtocol(2) .addProtocols(4) .setW(5) .setH(6) @@ -1343,6 +1388,10 @@ private static Banner givenBanner() { .format(singletonList(givenFormat())) .w(1) .h(2) + .wmax(640) + .hmax(480) + .wmin(160) + .hmin(120) .btype(singletonList(3)) .battr(singletonList(4)) .pos(5) @@ -1360,6 +1409,10 @@ private static OpenRtb.BidRequest.Imp.Banner givenProtobufBanner() { .addAllFormat(singletonList(givenProtobufFormat())) .setW(1) .setH(2) + .setWmax(640) + .setHmax(480) + .setWmin(160) + .setHmin(120) .addBtype(3) .addBattr(4) .setPos(5) @@ -1620,6 +1673,7 @@ private static Content givenContent() { .url("url") .cat(singletonList("cat")) .prodq(2) + .videoquality(1) .context(3) .contentrating("contentrating") .userrating("userrating") @@ -1650,6 +1704,7 @@ private static OpenRtb.BidRequest.Content givenProtobufContent() { .setUrl("url") .addCat("cat") .setProdq(2) + .setVideoquality(1) .setContext(3) .setContentrating("contentrating") .setUserrating("userrating")