From 101fc87ea8b7ab235709eb32265f9a2dfa825eaa Mon Sep 17 00:00:00 2001 From: Postindustria Date: Wed, 25 Feb 2026 04:31:00 +0200 Subject: [PATCH] ORTB Blocking Module: battr media type strictness (#4551) --- modules/prebid/ortb2blocking/config.go | 8 + .../ortb2blocking/hook_bidderrequest.go | 184 ++++- .../ortb2blocking/hook_bidderrequest_test.go | 738 ++++++++++++++++++ modules/prebid/ortb2blocking/module_test.go | 8 +- 4 files changed, 901 insertions(+), 37 deletions(-) create mode 100644 modules/prebid/ortb2blocking/hook_bidderrequest_test.go diff --git a/modules/prebid/ortb2blocking/config.go b/modules/prebid/ortb2blocking/config.go index 1990b0a56..f41180155 100644 --- a/modules/prebid/ortb2blocking/config.go +++ b/modules/prebid/ortb2blocking/config.go @@ -84,13 +84,21 @@ type BtypeActionOverride struct { type Battr struct { ActionOverrides BattrActionOverride `json:"action_overrides"` AllowedBannerAttrForDeals []int `json:"allowed_banner_attr_for_deals"` + AllowedVideoAttrForDeals []int `json:"allowed_video_attr_for_deals"` + AllowedAudioAttrForDeals []int `json:"allowed_audio_attr_for_deals"` BlockedBannerAttr []int `json:"blocked_banner_attr"` + BlockedVideoAttr []int `json:"blocked_video_attr"` + BlockedAudioAttr []int `json:"blocked_audio_attr"` EnforceBlocks bool `json:"enforce_blocks"` } type BattrActionOverride struct { AllowedBannerAttrForDeals []ActionOverride `json:"allowed_banner_attr_for_deals"` + AllowedVideoAttrForDeals []ActionOverride `json:"allowed_video_attr_for_deals"` + AllowedAudioAttrForDeals []ActionOverride `json:"allowed_audio_attr_for_deals"` BlockedBannerAttr []ActionOverride `json:"blocked_banner_attr"` + BlockedVideoAttr []ActionOverride `json:"blocked_video_attr"` + BlockedAudioAttr []ActionOverride `json:"blocked_audio_attr"` EnforceBlocks []ActionOverride `json:"enforce_blocks"` } diff --git a/modules/prebid/ortb2blocking/hook_bidderrequest.go b/modules/prebid/ortb2blocking/hook_bidderrequest.go index 4b3cb87c8..cd18de72d 100644 --- a/modules/prebid/ortb2blocking/hook_bidderrequest.go +++ b/modules/prebid/ortb2blocking/hook_bidderrequest.go @@ -147,13 +147,22 @@ func updateBType( return imp.Banner != nil && len(imp.Banner.BType) > 0 } - attributes.bType, messages, err = findImpressionOverrides(payload, actionOverrides, btype, checkAttrExistence) + overrides, messages, err := findImpressionOverrides(payload, actionOverrides, btype, checkAttrExistence) result.Warnings = mergeStrings(result.Warnings, messages...) if err != nil { return fmt.Errorf("failed to get override for imp.*.banner.btype: %s", err) - } else if len(attributes.bType) > 0 { - mutation := bTypeMutation(attributes.bType) - changeSet.AddMutation(mutation, hookstage.MutationUpdate, "bidrequest", "imp", "banner", "btype") + } + + // Filter to only apply to impressions with Banner objects + if len(overrides) > 0 { + filteredOverrides := filterByMediaType(payload, overrides, func(imp openrtb2.Imp) bool { + return imp.Banner != nil + }) + if len(filteredOverrides) > 0 { + mutation := createBTypeMutation(filteredOverrides) + changeSet.AddMutation(mutation, hookstage.MutationUpdate, "bidrequest", "imp", "banner", "btype") + } + attributes.bType = overrides } return nil @@ -167,21 +176,91 @@ func updateBAttr( changeSet *hookstage.ChangeSet[hookstage.BidderRequestPayload], ) (err error) { var messages []string - battr := cfg.Attributes.Battr.BlockedBannerAttr - actionOverrides := cfg.Attributes.Battr.ActionOverrides.BlockedBannerAttr - checkAttrExistence := func(imp openrtb2.Imp) bool { + + bannerBattr := cfg.Attributes.Battr.BlockedBannerAttr + bannerActionOverrides := cfg.Attributes.Battr.ActionOverrides.BlockedBannerAttr + bannerCheckAttrExistence := func(imp openrtb2.Imp) bool { return imp.Banner != nil && len(imp.Banner.BAttr) > 0 } - attributes.bAttr, messages, err = findImpressionOverrides(payload, actionOverrides, battr, checkAttrExistence) - result.Warnings = mergeStrings(result.Warnings, messages...) + bannerOverrides, bannerMessages, err := findImpressionOverrides(payload, bannerActionOverrides, bannerBattr, bannerCheckAttrExistence) + messages = append(messages, bannerMessages...) if err != nil { return fmt.Errorf("failed to get override for imp.*.banner.battr: %s", err) - } else if len(attributes.bAttr) > 0 { - mutation := bAttrMutation(attributes.bAttr) - changeSet.AddMutation(mutation, hookstage.MutationUpdate, "bidrequest", "imp", "banner", "battr") } + // Apply banner battr only to impressions that have Banner objects + if len(bannerOverrides) > 0 { + filteredBannerOverrides := filterByMediaType(payload, bannerOverrides, func(imp openrtb2.Imp) bool { + return imp.Banner != nil + }) + if len(filteredBannerOverrides) > 0 { + mutation := createBAttrMutation(filteredBannerOverrides, "banner") + changeSet.AddMutation(mutation, hookstage.MutationUpdate, "bidrequest", "imp", "banner", "battr") + } + } + + // Video battr + videoBattr := cfg.Attributes.Battr.BlockedVideoAttr + videoActionOverrides := cfg.Attributes.Battr.ActionOverrides.BlockedVideoAttr + videoCheckAttrExistence := func(imp openrtb2.Imp) bool { + return imp.Video != nil && len(imp.Video.BAttr) > 0 + } + + videoOverrides, videoMessages, err := findImpressionOverrides(payload, videoActionOverrides, videoBattr, videoCheckAttrExistence) + messages = append(messages, videoMessages...) + if err != nil { + return fmt.Errorf("failed to get override for imp.*.video.battr: %s", err) + } + + // Apply video battr only to impressions that have Video objects + if len(videoOverrides) > 0 { + filteredVideoOverrides := filterByMediaType(payload, videoOverrides, func(imp openrtb2.Imp) bool { + return imp.Video != nil + }) + if len(filteredVideoOverrides) > 0 { + mutation := createBAttrMutation(filteredVideoOverrides, "video") + changeSet.AddMutation(mutation, hookstage.MutationUpdate, "bidrequest", "imp", "video", "battr") + } + } + + // Audio battr + audioBattr := cfg.Attributes.Battr.BlockedAudioAttr + audioActionOverrides := cfg.Attributes.Battr.ActionOverrides.BlockedAudioAttr + audioCheckAttrExistence := func(imp openrtb2.Imp) bool { + return imp.Audio != nil && len(imp.Audio.BAttr) > 0 + } + + audioOverrides, audioMessages, err := findImpressionOverrides(payload, audioActionOverrides, audioBattr, audioCheckAttrExistence) + messages = append(messages, audioMessages...) + if err != nil { + return fmt.Errorf("failed to get override for imp.*.audio.battr: %s", err) + } + + // Apply audio battr only to impressions that have Audio objects + if len(audioOverrides) > 0 { + filteredAudioOverrides := filterByMediaType(payload, audioOverrides, func(imp openrtb2.Imp) bool { + return imp.Audio != nil + }) + if len(filteredAudioOverrides) > 0 { + mutation := createBAttrMutation(filteredAudioOverrides, "audio") + changeSet.AddMutation(mutation, hookstage.MutationUpdate, "bidrequest", "imp", "audio", "battr") + } + } + + // Store all attributes and merge messages + attributes.bAttr = make(map[string][]int) + for k, v := range bannerOverrides { + attributes.bAttr[k] = v + } + for k, v := range videoOverrides { + attributes.bAttr[k] = v + } + for k, v := range audioOverrides { + attributes.bAttr[k] = v + } + + result.Warnings = mergeStrings(result.Warnings, messages...) return nil } @@ -199,26 +278,6 @@ func updateCatTax( changeSet.BidderRequest().CatTax().Update(attributes.catTax) } -func bTypeMutation(bTypeByImp map[string][]int) hookstage.MutationFunc[hookstage.BidderRequestPayload] { - return mutationForImp(bTypeByImp, func(imp openrtb2.Imp, btype []int) openrtb2.Imp { - imp.Banner.BType = make([]openrtb2.BannerAdType, len(btype)) - for i := range btype { - imp.Banner.BType[i] = openrtb2.BannerAdType(btype[i]) - } - return imp - }) -} - -func bAttrMutation(bAttrByImp map[string][]int) hookstage.MutationFunc[hookstage.BidderRequestPayload] { - return mutationForImp(bAttrByImp, func(imp openrtb2.Imp, battr []int) openrtb2.Imp { - imp.Banner.BAttr = make([]adcom1.CreativeAttribute, len(battr)) - for i := range battr { - imp.Banner.BAttr[i] = adcom1.CreativeAttribute(battr[i]) - } - return imp - }) -} - type impUpdateFunc func(imp openrtb2.Imp, values []int) openrtb2.Imp func mutationForImp( @@ -422,3 +481,64 @@ func validateCondition(conditions Conditions) error { } return nil } + +// filterByMediaType filters overrides to only include impressions with a specific media type +func filterByMediaType( + payload hookstage.BidderRequestPayload, + overrides map[string][]int, + mediaTypeExists func(imp openrtb2.Imp) bool, +) map[string][]int { + filtered := make(map[string][]int) + + for _, imp := range payload.Request.Imp { + if values, exists := overrides[imp.ID]; exists && mediaTypeExists(imp) { + filtered[imp.ID] = values + } + } + + return filtered +} + +// createBAttrMutation creates a mutation function for a specific media type +func createBAttrMutation(bAttrByImp map[string][]int, mediaType string) hookstage.MutationFunc[hookstage.BidderRequestPayload] { + return func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) { + for i, imp := range payload.Request.Imp { + if values, ok := bAttrByImp[imp.ID]; ok && len(values) > 0 { + switch mediaType { + case "banner": + imp.Banner.BAttr = make([]adcom1.CreativeAttribute, len(values)) + for j, attr := range values { + imp.Banner.BAttr[j] = adcom1.CreativeAttribute(attr) + } + case "video": + imp.Video.BAttr = make([]adcom1.CreativeAttribute, len(values)) + for j, attr := range values { + imp.Video.BAttr[j] = adcom1.CreativeAttribute(attr) + } + case "audio": + imp.Audio.BAttr = make([]adcom1.CreativeAttribute, len(values)) + for j, attr := range values { + imp.Audio.BAttr[j] = adcom1.CreativeAttribute(attr) + } + } + payload.Request.Imp[i] = imp + } + } + return payload, nil + } +} + +func createBTypeMutation(bTypeByImp map[string][]int) hookstage.MutationFunc[hookstage.BidderRequestPayload] { + return func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) { + for i, imp := range payload.Request.Imp { + if values, ok := bTypeByImp[imp.ID]; ok && len(values) > 0 { + imp.Banner.BType = make([]openrtb2.BannerAdType, len(values)) + for j, btype := range values { + imp.Banner.BType[j] = openrtb2.BannerAdType(btype) + } + payload.Request.Imp[i] = imp + } + } + return payload, nil + } +} diff --git a/modules/prebid/ortb2blocking/hook_bidderrequest_test.go b/modules/prebid/ortb2blocking/hook_bidderrequest_test.go new file mode 100644 index 000000000..5730bd500 --- /dev/null +++ b/modules/prebid/ortb2blocking/hook_bidderrequest_test.go @@ -0,0 +1,738 @@ +package ortb2blocking + +import ( + "testing" + + "github.com/prebid/openrtb/v20/adcom1" + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v3/hooks/hookstage" + "github.com/prebid/prebid-server/v3/openrtb_ext" + "github.com/prebid/prebid-server/v3/util/ptrutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdateBAttr_DoesNotCreateMediaTypeObjects(t *testing.T) { + cfg := config{ + Attributes: Attributes{ + Battr: Battr{ + BlockedBannerAttr: []int{1, 2, 3}, + BlockedVideoAttr: []int{4, 5, 6}, + BlockedAudioAttr: []int{7, 8, 9}, + }, + }, + } + + var sizeW int64 = 640 + var sizeH int64 = 480 + + payload := hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp-video-only", + Video: &openrtb2.Video{W: &sizeW, H: &sizeH}, + // Banner = nil, Audio = nil + }, + { + ID: "imp-audio-only", + Audio: &openrtb2.Audio{}, + // Banner = nil, Video = nil + }, + }, + }, + }, + Bidder: "test-bidder", + } + + var blockingAttrs blockingAttributes + var result hookstage.HookResult[hookstage.BidderRequestPayload] + var changeSet hookstage.ChangeSet[hookstage.BidderRequestPayload] + + err := updateBAttr(cfg, payload, &blockingAttrs, &result, &changeSet) + require.NoError(t, err) + + // Check that mutations were created only for existing media types + mutations := changeSet.Mutations() + + // Check the number of mutations - there should be only one for video and one for audio + videoMutationsCount := 0 + audioMutationsCount := 0 + bannerMutationsCount := 0 + + for _, mutation := range mutations { + if len(mutation.Key()) >= 3 { + mediaType := mutation.Key()[2] // the third path element must be a media type + switch mediaType { + case "video": + videoMutationsCount++ + case "audio": + audioMutationsCount++ + case "banner": + bannerMutationsCount++ + } + } + } + + assert.Equal(t, 1, videoMutationsCount, "Should have exactly one video mutation") + assert.Equal(t, 1, audioMutationsCount, "Should have exactly one audio mutation") + assert.Equal(t, 0, bannerMutationsCount, "Should have no banner mutations") + + // Apply mutations manually using functions from the code + for _, mutation := range mutations { + updatedPayload, err := mutation.Apply(payload) + require.NoError(t, err) + payload = updatedPayload + } + + // Check that Banner/Audio objects were NOT created + for _, imp := range payload.Request.BidRequest.Imp { + switch imp.ID { + case "imp-video-only": + assert.NotNil(t, imp.Video, "Video should exist") + assert.Nil(t, imp.Banner, "Banner should NOT be created") + assert.Nil(t, imp.Audio, "Audio should NOT be created") + // Check that battr was added to video + if imp.Video != nil { + assert.Len(t, imp.Video.BAttr, 3, "Video should have battr applied") + } + case "imp-audio-only": + assert.NotNil(t, imp.Audio, "Audio should exist") + assert.Nil(t, imp.Banner, "Banner should NOT be created") + assert.Nil(t, imp.Video, "Video should NOT be created") + // Check that battr was added to audio + if imp.Audio != nil { + assert.Len(t, imp.Audio.BAttr, 3, "Audio should have battr applied") + } + } + } +} + +func TestUpdateBAttr_AppliesOnlyToExistingMediaTypes(t *testing.T) { + cfg := config{ + Attributes: Attributes{ + Battr: Battr{ + BlockedBannerAttr: []int{1, 2}, + BlockedVideoAttr: []int{3, 4}, + BlockedAudioAttr: []int{5, 6}, + }, + }, + } + + payload := hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp-banner-only", + Banner: &openrtb2.Banner{W: ptrutil.ToPtr[int64](300), H: ptrutil.ToPtr[int64](250)}, + }, + { + ID: "imp-mixed", + Banner: &openrtb2.Banner{W: ptrutil.ToPtr[int64](728), H: ptrutil.ToPtr[int64](90)}, + Video: &openrtb2.Video{W: ptrutil.ToPtr[int64](640), H: ptrutil.ToPtr[int64](480)}, + }, + }, + }, + }, + Bidder: "test-bidder", + } + + var blockingAttrs blockingAttributes + var result hookstage.HookResult[hookstage.BidderRequestPayload] + var changeSet hookstage.ChangeSet[hookstage.BidderRequestPayload] + + err := updateBAttr(cfg, payload, &blockingAttrs, &result, &changeSet) + require.NoError(t, err) + + // We apply mutations + mutations := changeSet.Mutations() + for _, mutation := range mutations { + updatedPayload, err := mutation.Apply(payload) + require.NoError(t, err) + payload = updatedPayload + } + + for _, imp := range payload.Request.BidRequest.Imp { + switch imp.ID { + case "imp-banner-only": + assert.NotNil(t, imp.Banner, "Banner should exist") + assert.Nil(t, imp.Video, "Video should NOT be created") + assert.Nil(t, imp.Audio, "Audio should NOT be created") + if imp.Banner != nil { + assert.Len(t, imp.Banner.BAttr, 2, "Banner should have battr applied") + } + case "imp-mixed": + assert.NotNil(t, imp.Banner, "Banner should exist") + assert.NotNil(t, imp.Video, "Video should exist") + assert.Nil(t, imp.Audio, "Audio should NOT be created") + if imp.Banner != nil { + assert.Len(t, imp.Banner.BAttr, 2, "Banner should have battr applied") + } + if imp.Video != nil { + assert.Len(t, imp.Video.BAttr, 2, "Video should have battr applied") + } + } + } +} + +func TestFilterByMediaType(t *testing.T) { + payload := hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "has-banner", + Banner: &openrtb2.Banner{}, + }, + { + ID: "no-banner", + // Banner = nil + }, + }, + }, + }, + } + + overrides := map[string][]int{ + "has-banner": {1, 2, 3}, + "no-banner": {4, 5, 6}, + } + + filtered := filterByMediaType(payload, overrides, func(imp openrtb2.Imp) bool { + return imp.Banner != nil + }) + + // Only the impression with the Banner should remain. + assert.Len(t, filtered, 1) + assert.Contains(t, filtered, "has-banner") + assert.NotContains(t, filtered, "no-banner") + assert.Equal(t, []int{1, 2, 3}, filtered["has-banner"]) +} + +func TestCreateBAttrMutation(t *testing.T) { + bAttrByImp := map[string][]int{ + "imp1": {1, 2}, + "imp2": {3, 4}, + } + + payload := hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{}, + }, + { + ID: "imp2", + Banner: &openrtb2.Banner{}, + }, + }, + }, + }, + } + + mutation := createBAttrMutation(bAttrByImp, "banner") + updatedPayload, err := mutation(payload) + require.NoError(t, err) + + // Check that battr was applied + for _, imp := range updatedPayload.Request.BidRequest.Imp { + switch imp.ID { + case "imp1": + require.NotNil(t, imp.Banner) + assert.Len(t, imp.Banner.BAttr, 2) + assert.Equal(t, adcom1.CreativeAttribute(1), imp.Banner.BAttr[0]) + assert.Equal(t, adcom1.CreativeAttribute(2), imp.Banner.BAttr[1]) + case "imp2": + require.NotNil(t, imp.Banner) + assert.Len(t, imp.Banner.BAttr, 2) + assert.Equal(t, adcom1.CreativeAttribute(3), imp.Banner.BAttr[0]) + assert.Equal(t, adcom1.CreativeAttribute(4), imp.Banner.BAttr[1]) + } + } +} + +func TestCreateBTypeMutation(t *testing.T) { + bTypeByImp := map[string][]int{ + "imp1": {1, 2}, + "imp2": {3, 4}, + } + + payload := hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{}, + }, + { + ID: "imp2", + Banner: &openrtb2.Banner{}, + }, + }, + }, + }, + } + + mutation := createBTypeMutation(bTypeByImp) + updatedPayload, err := mutation(payload) + require.NoError(t, err) + + // Check that btype was applied + for _, imp := range updatedPayload.Request.BidRequest.Imp { + switch imp.ID { + case "imp1": + require.NotNil(t, imp.Banner) + assert.Len(t, imp.Banner.BType, 2) + assert.Equal(t, openrtb2.BannerAdType(1), imp.Banner.BType[0]) + assert.Equal(t, openrtb2.BannerAdType(2), imp.Banner.BType[1]) + case "imp2": + require.NotNil(t, imp.Banner) + assert.Len(t, imp.Banner.BType, 2) + assert.Equal(t, openrtb2.BannerAdType(3), imp.Banner.BType[0]) + assert.Equal(t, openrtb2.BannerAdType(4), imp.Banner.BType[1]) + } + } +} + +func TestUpdateBType_DoesNotCreateBannerObjects(t *testing.T) { + cfg := config{ + Attributes: Attributes{ + Btype: Btype{ + BlockedBannerType: []int{1, 2, 3}, + }, + }, + } + + payload := hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp-video-only", + Video: &openrtb2.Video{W: ptrutil.ToPtr[int64](640), H: ptrutil.ToPtr[int64](480)}, + }, + { + ID: "imp-with-banner", + Banner: &openrtb2.Banner{}, + }, + }, + }, + }, + Bidder: "test-bidder", + } + + var blockingAttrs blockingAttributes + var result hookstage.HookResult[hookstage.BidderRequestPayload] + var changeSet hookstage.ChangeSet[hookstage.BidderRequestPayload] + + err := updateBType(cfg, payload, &blockingAttrs, &result, &changeSet) + require.NoError(t, err) + + // Apply mutations + mutations := changeSet.Mutations() + for _, mutation := range mutations { + updatedPayload, err := mutation.Apply(payload) + require.NoError(t, err) + payload = updatedPayload + } + + // Check that Banner was NOT created for video-only impression + for _, imp := range payload.Request.BidRequest.Imp { + switch imp.ID { + case "imp-video-only": + assert.NotNil(t, imp.Video, "Video should exist") + assert.Nil(t, imp.Banner, "Banner should NOT be created") + case "imp-with-banner": + assert.NotNil(t, imp.Banner, "Banner should exist") + assert.Len(t, imp.Banner.BType, 3, "Banner should have btype applied") + } + } +} + +func TestCreateBAttrMutation_EmptyValues(t *testing.T) { + bAttrByImp := map[string][]int{ + "imp1": {}, + "imp2": {1, 2}, + } + + payload := hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{}, + }, + { + ID: "imp2", + Banner: &openrtb2.Banner{}, + }, + }, + }, + }, + } + + mutation := createBAttrMutation(bAttrByImp, "banner") + updatedPayload, err := mutation(payload) + require.NoError(t, err) + + // Check that empty values are handled correctly + for _, imp := range updatedPayload.Request.BidRequest.Imp { + switch imp.ID { + case "imp1": + require.NotNil(t, imp.Banner) + // Empty array should result in empty BAttr + assert.Len(t, imp.Banner.BAttr, 0) + case "imp2": + require.NotNil(t, imp.Banner) + assert.Len(t, imp.Banner.BAttr, 2) + } + } +} + +func TestCreateBTypeMutation_EmptyValues(t *testing.T) { + bTypeByImp := map[string][]int{ + "imp1": {}, + "imp2": {1, 2}, + } + + payload := hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{}, + }, + { + ID: "imp2", + Banner: &openrtb2.Banner{}, + }, + }, + }, + }, + } + + mutation := createBTypeMutation(bTypeByImp) + updatedPayload, err := mutation(payload) + require.NoError(t, err) + + for _, imp := range updatedPayload.Request.BidRequest.Imp { + switch imp.ID { + case "imp1": + require.NotNil(t, imp.Banner) + assert.Len(t, imp.Banner.BType, 0) + case "imp2": + require.NotNil(t, imp.Banner) + assert.Len(t, imp.Banner.BType, 2) + } + } +} + +func TestMediaTypesFrom(t *testing.T) { + request := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{}, + Video: &openrtb2.Video{}, + }, + { + ID: "imp2", + Audio: &openrtb2.Audio{}, + }, + }, + } + + mediaTypes := mediaTypesFrom(request) + + assert.Len(t, mediaTypes, 3) + assert.Contains(t, mediaTypes, "banner") + assert.Contains(t, mediaTypes, "video") + assert.Contains(t, mediaTypes, "audio") + assert.NotContains(t, mediaTypes, "native") +} + +func TestMediaTypesFromImp(t *testing.T) { + tests := []struct { + name string + imp openrtb2.Imp + expected []string + }{ + { + name: "banner only", + imp: openrtb2.Imp{ + Banner: &openrtb2.Banner{}, + }, + expected: []string{"banner"}, + }, + { + name: "video only", + imp: openrtb2.Imp{ + Video: &openrtb2.Video{}, + }, + expected: []string{"video"}, + }, + { + name: "audio only", + imp: openrtb2.Imp{ + Audio: &openrtb2.Audio{}, + }, + expected: []string{"audio"}, + }, + { + name: "native only", + imp: openrtb2.Imp{ + Native: &openrtb2.Native{}, + }, + expected: []string{"native"}, + }, + { + name: "multiple types", + imp: openrtb2.Imp{ + Banner: &openrtb2.Banner{}, + Video: &openrtb2.Video{}, + Audio: &openrtb2.Audio{}, + }, + expected: []string{"banner", "video", "audio"}, + }, + { + name: "no media types", + imp: openrtb2.Imp{}, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := mediaTypesFromImp(tt.imp) + assert.Len(t, result, len(tt.expected)) + for _, mt := range tt.expected { + assert.Contains(t, result, mt) + } + }) + } +} + +func TestMediaTypes_String(t *testing.T) { + tests := []struct { + name string + mt mediaTypes + expected string + }{ + { + name: "multiple types sorted", + mt: mediaTypes{ + "video": struct{}{}, + "banner": struct{}{}, + "audio": struct{}{}, + }, + expected: "audio, banner, video", + }, + { + name: "empty", + mt: mediaTypes{}, + expected: "", + }, + { + name: "all types", + mt: mediaTypes{ + "audio": struct{}{}, + "banner": struct{}{}, + "native": struct{}{}, + "video": struct{}{}, + }, + expected: "audio, banner, native, video", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.mt.String() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestMediaTypes_Intersects(t *testing.T) { + mt := mediaTypes{ + "banner": struct{}{}, + "video": struct{}{}, + } + + tests := []struct { + name string + input []string + expected bool + }{ + { + name: "exact match", + input: []string{"banner"}, + expected: true, + }, + { + name: "case insensitive match", + input: []string{"BANNER"}, + expected: true, + }, + { + name: "multiple with match", + input: []string{"audio", "video"}, + expected: true, + }, + { + name: "no match", + input: []string{"audio", "native"}, + expected: false, + }, + { + name: "empty input", + input: []string{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := mt.intersects(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestValidateCondition(t *testing.T) { + tests := []struct { + name string + condition Conditions + wantErr bool + }{ + { + name: "valid - bidders only", + condition: Conditions{ + Bidders: []string{"bidder1"}, + }, + wantErr: false, + }, + { + name: "valid - media types only", + condition: Conditions{ + MediaTypes: []string{"banner"}, + }, + wantErr: false, + }, + { + name: "valid - both present", + condition: Conditions{ + Bidders: []string{"bidder1"}, + MediaTypes: []string{"banner"}, + }, + wantErr: false, + }, + { + name: "invalid - both absent", + condition: Conditions{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateCondition(tt.condition) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestFilterByMediaType_NoMatches(t *testing.T) { + payload := hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Video: &openrtb2.Video{}, + }, + }, + }, + }, + } + + overrides := map[string][]int{ + "imp1": {1, 2, 3}, + } + + // Filter for Banner, but impression has only Video + filtered := filterByMediaType(payload, overrides, func(imp openrtb2.Imp) bool { + return imp.Banner != nil + }) + + assert.Len(t, filtered, 0, "Should not include impressions without matching media type") +} + +func TestCreateBAttrMutation_VideoMediaType(t *testing.T) { + bAttrByImp := map[string][]int{ + "imp1": {1, 2}, + } + + payload := hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Video: &openrtb2.Video{}, + }, + }, + }, + }, + } + + mutation := createBAttrMutation(bAttrByImp, "video") + updatedPayload, err := mutation(payload) + require.NoError(t, err) + + imp := updatedPayload.Request.BidRequest.Imp[0] + require.NotNil(t, imp.Video) + assert.Len(t, imp.Video.BAttr, 2) + assert.Equal(t, adcom1.CreativeAttribute(1), imp.Video.BAttr[0]) + assert.Equal(t, adcom1.CreativeAttribute(2), imp.Video.BAttr[1]) +} + +func TestCreateBAttrMutation_AudioMediaType(t *testing.T) { + bAttrByImp := map[string][]int{ + "imp1": {7, 8, 9}, + } + + payload := hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Audio: &openrtb2.Audio{}, + }, + }, + }, + }, + } + + mutation := createBAttrMutation(bAttrByImp, "audio") + updatedPayload, err := mutation(payload) + require.NoError(t, err) + + imp := updatedPayload.Request.BidRequest.Imp[0] + require.NotNil(t, imp.Audio) + assert.Len(t, imp.Audio.BAttr, 3) + assert.Equal(t, adcom1.CreativeAttribute(7), imp.Audio.BAttr[0]) + assert.Equal(t, adcom1.CreativeAttribute(8), imp.Audio.BAttr[1]) + assert.Equal(t, adcom1.CreativeAttribute(9), imp.Audio.BAttr[2]) +} diff --git a/modules/prebid/ortb2blocking/module_test.go b/modules/prebid/ortb2blocking/module_test.go index 17008b2da..4159d73f3 100644 --- a/modules/prebid/ortb2blocking/module_test.go +++ b/modules/prebid/ortb2blocking/module_test.go @@ -307,8 +307,9 @@ func TestHandleBidderRequestHook(t *testing.T) { Video: &openrtb2.Video{}, }, { - ID: "ImpID2", - Audio: &openrtb2.Audio{}, + ID: "ImpID2", + Audio: &openrtb2.Audio{}, + Banner: &openrtb2.Banner{}, }, }, }, @@ -391,7 +392,6 @@ func TestHandleBidderRequestHook(t *testing.T) { ModuleContext: map[string]interface{}{ bidder: blockingAttributes{ bAdv: []string{bAdvA, bAdvB, bAdvC}, - bType: map[string][]int{}, bAttr: map[string][]int{}, }, }, @@ -440,7 +440,6 @@ func TestHandleBidderRequestHook(t *testing.T) { }, expectedHookResult: hookstage.HookResult[hookstage.BidderRequestPayload]{ ModuleContext: map[string]interface{}{bidder: blockingAttributes{ - bType: map[string][]int{}, bAttr: map[string][]int{}, }}, }, @@ -461,7 +460,6 @@ func TestHandleBidderRequestHook(t *testing.T) { }, expectedHookResult: hookstage.HookResult[hookstage.BidderRequestPayload]{ ModuleContext: map[string]interface{}{bidder: blockingAttributes{ - bType: map[string][]int{}, bAttr: map[string][]int{}, }}, },