Repository: prebid/prebid-server
Pull request #2529 (2023-01-12 19:15:55)

Bid Response Validation Test Fix
As pointed out by @laurb9 in the Bid Response Validation PR https://github.com/prebid/prebid-server/pull/2450, on line 2302 in `exchange/exchange_test.go`, the validation flag that is used for the JSON tests, wasn't being set. So we were getting a false pass in the tests, without actually asserting what's within the validation flag code block.

The reason it wasn't being set is because I changed how I wanted my validation flag to work, and implemented this change elsewhere in this test file, but forgot to change this line. To fix this, I updated the line so that the proper validation object is being utilized.

This validation object is set based on the info that's present in the `bid_response` json files specified at `host_bid_validations` within the json file.


Patch:
diff --git a/hooks/hookexecution/errors.go b/hooks/hookexecution/errors.go
index 62c6fb71..b1cf912c 100644
--- a/hooks/hookexecution/errors.go
+++ b/hooks/hookexecution/errors.go
@@ -13,6 +13,10 @@ func (e TimeoutError) Error() string {
 	return "Hook execution timeout"
 }
 
+func NewFailure(format string, a ...any) FailureError {
+	return FailureError{Message: fmt.Sprintf(format, a...)}
+}
+
 // FailureError indicates expected error occurred during hook execution on the module-side.
 // A moduleFailed metric will be sent in such case.
 type FailureError struct {
diff --git a/hooks/hookexecution/execution.go b/hooks/hookexecution/execution.go
index 52790501..1d9b4a52 100644
--- a/hooks/hookexecution/execution.go
+++ b/hooks/hookexecution/execution.go
@@ -269,7 +269,7 @@ func handleHookMutations[P any](
 	metricEngine metrics.MetricsEngine,
 	labels metrics.ModuleLabels,
 ) P {
-	if hr.Result.ChangeSet == nil || len(hr.Result.ChangeSet.Mutations()) == 0 {
+	if len(hr.Result.ChangeSet.Mutations()) == 0 {
 		metricEngine.RecordModuleSuccessNooped(labels)
 		hookOutcome.Action = ActionNone
 		return payload
diff --git a/hooks/hookstage/invocation.go b/hooks/hookstage/invocation.go
index afea4a2f..7f465382 100644
--- a/hooks/hookstage/invocation.go
+++ b/hooks/hookstage/invocation.go
@@ -8,10 +8,10 @@ import (
 
 // HookResult represents the result of execution the concrete hook instance.
 type HookResult[T any] struct {
-	Reject        bool          // true value indicates rejection of the program execution at the specific stage
-	NbrCode       int           // hook must provide NbrCode if the field Reject set to true
-	Message       string        // holds arbitrary message added by hook
-	ChangeSet     *ChangeSet[T] // set of changes the module wants to apply to hook payload in case of successful execution
+	Reject        bool         // true value indicates rejection of the program execution at the specific stage
+	NbrCode       int          // hook must provide NbrCode if the field Reject set to true
+	Message       string       // holds arbitrary message added by hook
+	ChangeSet     ChangeSet[T] // set of changes the module wants to apply to hook payload in case of successful execution
 	Errors        []string
 	Warnings      []string
 	DebugMessages []string
diff --git a/modules/builder.go b/modules/builder.go
index 803ab8bf..ffb814e6 100644
--- a/modules/builder.go
+++ b/modules/builder.go
@@ -1,7 +1,15 @@
 package modules
 
+import (
+	prebidOrtb2blocking "github.com/prebid/prebid-server/modules/prebid/ortb2blocking"
+)
+
 // builders returns mapping between module name and its builder
 // vendor and module names are chosen based on the module directory name
 func builders() ModuleBuilders {
-	return ModuleBuilders{}
+	return ModuleBuilders{
+		"prebid": {
+			"ortb2blocking": prebidOrtb2blocking.Builder,
+		},
+	}
 }
diff --git a/modules/moduledeps/deps.go b/modules/moduledeps/deps.go
new file mode 100644
index 00000000..08b18349
--- /dev/null
+++ b/modules/moduledeps/deps.go
@@ -0,0 +1,9 @@
+package moduledeps
+
+import "net/http"
+
+// ModuleDeps provides dependencies that custom modules may need for hooks execution.
+// Additional dependencies can be added here if modules need something more.
+type ModuleDeps struct {
+	HTTPClient *http.Client
+}
diff --git a/modules/modules.go b/modules/modules.go
index e595e1e1..ac60ec58 100644
--- a/modules/modules.go
+++ b/modules/modules.go
@@ -3,10 +3,11 @@ package modules
 import (
 	"encoding/json"
 	"fmt"
-	"net/http"
 
+	"github.com/golang/glog"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/hooks"
+	"github.com/prebid/prebid-server/modules/moduledeps"
 )
 
 //go:generate go run ./generator/buildergen.go
@@ -23,14 +24,14 @@ type Builder interface {
 	// It returns hook repository created based on the implemented hook interfaces by modules
 	// and a map of modules to a list of stage names for which module provides hooks
 	// or an error encountered during module initialization.
-	Build(cfg config.Modules, client *http.Client) (hooks.HookRepository, map[string][]string, error)
+	Build(cfg config.Modules, client moduledeps.ModuleDeps) (hooks.HookRepository, map[string][]string, error)
 }
 
 type (
 	// ModuleBuilders mapping between module name and its builder: map[vendor]map[module]ModuleBuilderFn
 	ModuleBuilders map[string]map[string]ModuleBuilderFn
-	// ModuleBuilderFn returns an interface{} type that implements certain hook interfaces
-	ModuleBuilderFn func(cfg json.RawMessage, client *http.Client) (interface{}, error)
+	// ModuleBuilderFn returns an interface{} type that implements certain hook interfaces.
+	ModuleBuilderFn func(cfg json.RawMessage, deps moduledeps.ModuleDeps) (interface{}, error)
 )
 
 type builder struct {
@@ -44,21 +45,36 @@ type builder struct {
 //
 // Method returns a hooks.HookRepository and a map of modules to a list of stage names
 // for which module provides hooks or an error occurred during modules initialization.
-func (m *builder) Build(cfg config.Modules, client *http.Client) (hooks.HookRepository, map[string][]string, error) {
+func (m *builder) Build(
+	cfg config.Modules,
+	deps moduledeps.ModuleDeps,
+) (hooks.HookRepository, map[string][]string, error) {
 	modules := make(map[string]interface{})
 	for vendor, moduleBuilders := range m.builders {
 		for moduleName, builder := range moduleBuilders {
 			var err error
 			var conf json.RawMessage
+			var isEnabled bool
 
 			id := fmt.Sprintf("%s.%s", vendor, moduleName)
 			if data, ok := cfg[vendor][moduleName]; ok {
 				if conf, err = json.Marshal(data); err != nil {
 					return nil, nil, fmt.Errorf(`failed to marshal "%s" module config: %s`, id, err)
 				}
+
+				if values, ok := data.(map[string]interface{}); ok {
+					if value, ok := values["enabled"].(bool); ok {
+						isEnabled = value
+					}
+				}
+			}
+
+			if !isEnabled {
+				glog.Infof("Skip %s module, disabled.", id)
+				continue
 			}
 
-			module, err := builder(conf, client)
+			module, err := builder(conf, deps)
 			if err != nil {
 				return nil, nil, fmt.Errorf(`failed to init "%s" module: %s`, id, err)
 			}
diff --git a/modules/prebid/ortb2blocking/README.md b/modules/prebid/ortb2blocking/README.md
new file mode 100644
index 00000000..a2176ca5
--- /dev/null
+++ b/modules/prebid/ortb2blocking/README.md
@@ -0,0 +1,18 @@
+# Overview
+
+Prebid Server doesn't provide all services an SSP normally provides, including account configuration of blocked:
+
+- advertiser domains
+- advertiser categories
+- creative types
+- creative attributes
+- apps
+
+This module allows Prebid Server host companies to better support adapters that require blocking config.
+
+# Maintainer contacts
+
+Any suggestions or questions can be directed to [example@site.com]() e-mail.
+
+Or just open new [issue](https://github.com/prebid/prebid-server/issues/new)
+or [pull request](https://github.com/prebid/prebid-server/pulls) in this repository.
diff --git a/modules/prebid/ortb2blocking/config.go b/modules/prebid/ortb2blocking/config.go
new file mode 100644
index 00000000..a7c6caff
--- /dev/null
+++ b/modules/prebid/ortb2blocking/config.go
@@ -0,0 +1,134 @@
+package ortb2blocking
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/prebid/openrtb/v17/adcom1"
+)
+
+func newConfig(data json.RawMessage) (config, error) {
+	var cfg config
+	if err := json.Unmarshal(data, &cfg); err != nil {
+		return cfg, fmt.Errorf("failed to parse config: %s", err)
+	}
+	return cfg, nil
+}
+
+type config struct {
+	Attributes Attributes `json:"attributes"`
+}
+
+type Attributes struct {
+	Badv  Badv  `json:"badv"`
+	Bcat  Bcat  `json:"bcat"`
+	Bapp  Bapp  `json:"bapp"`
+	Btype Btype `json:"btype"`
+	Battr Battr `json:"battr"`
+}
+
+type Badv struct {
+	ActionOverrides        BadvActionOverride `json:"action_overrides"`
+	AllowedAdomainForDeals []string           `json:"allowed_adomain_for_deals"`
+	BlockedAdomain         []string           `json:"blocked_adomain"`
+	BlockUnknownAdomain    bool               `json:"block_unknown_adomain"`
+	EnforceBlocks          bool               `json:"enforce_blocks"`
+}
+
+type BadvActionOverride struct {
+	AllowedAdomainForDeals []ActionOverride `json:"allowed_adomain_for_deals"`
+	BlockedAdomain         []ActionOverride `json:"blocked_adomain"`
+	BlockUnknownAdomain    []ActionOverride `json:"block_unknown_adomain"`
+	EnforceBlocks          []ActionOverride `json:"enforce_blocks"`
+}
+
+type Bcat struct {
+	ActionOverrides       BcatActionOverride      `json:"action_overrides"`
+	AllowedAdvCatForDeals []string                `json:"allowed_adv_cat_for_deals"`
+	BlockedAdvCat         []string                `json:"blocked_adv_cat"`
+	BlockUnknownAdvCat    bool                    `json:"block_unknown_adv_cat"`
+	CategoryTaxonomy      adcom1.CategoryTaxonomy `json:"category_taxonomy"`
+	EnforceBlocks         bool                    `json:"enforce_blocks"`
+}
+
+type BcatActionOverride struct {
+	AllowedAdvCatForDeals []ActionOverride `json:"allowed_adv_cat_for_deals"`
+	BlockedAdvCat         []ActionOverride `json:"blocked_adv_cat"`
+	BlockUnknownAdvCat    []ActionOverride `json:"block_unknown_adv_cat"`
+	EnforceBlocks         []ActionOverride `json:"enforce_blocks"`
+}
+
+type Bapp struct {
+	ActionOverrides    BappActionOverride `json:"action_overrides"`
+	AllowedAppForDeals []string           `json:"allowed_app_for_deals"`
+	BlockedApp         []string           `json:"blocked_app"`
+	EnforceBlocks      bool               `json:"enforce_blocks"`
+}
+
+type BappActionOverride struct {
+	AllowedAppForDeals []ActionOverride `json:"allowed_app_for_deals"`
+	BlockedApp         []ActionOverride `json:"blocked_app"`
+	EnforceBlocks      []ActionOverride `json:"enforce_blocks"`
+}
+
+type Btype struct {
+	ActionOverrides   BtypeActionOverride `json:"action_overrides"`
+	BlockedBannerType []int               `json:"blocked_banner_type"`
+}
+
+type BtypeActionOverride struct {
+	BlockedBannerType []ActionOverride `json:"blocked_banner_type"`
+}
+
+type Battr struct {
+	ActionOverrides           BattrActionOverride `json:"action_overrides"`
+	AllowedBannerAttrForDeals []int               `json:"allowed_banner_attr_for_deals"`
+	BlockedBannerAttr         []int               `json:"blocked_banner_attr"`
+	EnforceBlocks             bool                `json:"enforce_blocks"`
+}
+
+type BattrActionOverride struct {
+	AllowedBannerAttrForDeals []ActionOverride `json:"allowed_banner_attr_for_deals"`
+	BlockedBannerAttr         []ActionOverride `json:"blocked_banner_attr"`
+	EnforceBlocks             []ActionOverride `json:"enforce_blocks"`
+}
+
+type ActionOverride struct {
+	Conditions Conditions `json:"conditions"`
+	Override   Override   `json:"override"`
+}
+
+type Conditions struct {
+	Bidders    []string `json:"bidders"`
+	MediaTypes []string `json:"media_types"`
+	DealIds    []string `json:"deal_ids"`
+}
+
+type Override struct {
+	IsActive bool
+	Ids      []int
+	Names    []string
+}
+
+func (o *Override) UnmarshalJSON(bytes []byte) error {
+	var overrideData interface{}
+	if err := json.Unmarshal(bytes, &overrideData); err != nil {
+		return err
+	}
+
+	switch overrideValue := overrideData.(type) {
+	case bool:
+		o.IsActive = overrideValue
+	case []interface{}:
+		for _, value := range overrideValue {
+			switch override := value.(type) {
+			case string:
+				o.Names = append(o.Names, override)
+			case float64:
+				o.Ids = append(o.Ids, int(override))
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/modules/prebid/ortb2blocking/module.go b/modules/prebid/ortb2blocking/module.go
new file mode 100644
index 00000000..f0ef80cc
--- /dev/null
+++ b/modules/prebid/ortb2blocking/module.go
@@ -0,0 +1,13 @@
+package ortb2blocking
+
+import (
+	"encoding/json"
+
+	"github.com/prebid/prebid-server/modules/moduledeps"
+)
+
+func Builder(_ json.RawMessage, _ moduledeps.ModuleDeps) (interface{}, error) {
+	return Module{}, nil
+}
+
+type Module struct{}
diff --git a/router/router.go b/router/router.go
index 81658728..b93473b4 100644
--- a/router/router.go
+++ b/router/router.go
@@ -25,6 +25,7 @@ import (
 	"github.com/prebid/prebid-server/metrics"
 	metricsConf "github.com/prebid/prebid-server/metrics/config"
 	"github.com/prebid/prebid-server/modules"
+	"github.com/prebid/prebid-server/modules/moduledeps"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbs"
 	pbc "github.com/prebid/prebid-server/prebid_cache_client"
@@ -168,7 +169,8 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 		syncerKeys = append(syncerKeys, k)
 	}
 
-	repo, moduleStageNames, err := modules.NewBuilder().Build(cfg.Hooks.Modules, generalHttpClient)
+	moduleDeps := moduledeps.ModuleDeps{HTTPClient: generalHttpClient}
+	repo, moduleStageNames, err := modules.NewBuilder().Build(cfg.Hooks.Modules, moduleDeps)
 	if err != nil {
 		glog.Fatalf("Failed to init hook modules: %v", err)
 	}


Test patch:
diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go
index cf803a34..2a59fc45 100644
--- a/endpoints/openrtb2/test_utils.go
+++ b/endpoints/openrtb2/test_utils.go
@@ -1555,7 +1555,7 @@ var entryPointHookUpdateWithErrors = hooks.HookWrapper[hookstage.Entrypoint]{
 			_ hookstage.ModuleInvocationContext,
 			payload hookstage.EntrypointPayload,
 		) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
-			ch := &hookstage.ChangeSet[hookstage.EntrypointPayload]{}
+			ch := hookstage.ChangeSet[hookstage.EntrypointPayload]{}
 			ch.AddMutation(func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
 				payload.Request.Header.Add("foo", "bar")
 				return payload, nil
@@ -1577,7 +1577,7 @@ var entryPointHookUpdateWithErrorsAndWarnings = hooks.HookWrapper[hookstage.Entr
 			_ hookstage.ModuleInvocationContext,
 			payload hookstage.EntrypointPayload,
 		) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
-			ch := &hookstage.ChangeSet[hookstage.EntrypointPayload]{}
+			ch := hookstage.ChangeSet[hookstage.EntrypointPayload]{}
 			ch.AddMutation(func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
 				params := payload.Request.URL.Query()
 				params.Add("foo", "baz")
@@ -1608,7 +1608,7 @@ var entryPointHookUpdate = hooks.HookWrapper[hookstage.Entrypoint]{
 				return result, nil
 			}
 
-			ch := &hookstage.ChangeSet[hookstage.EntrypointPayload]{}
+			ch := hookstage.ChangeSet[hookstage.EntrypointPayload]{}
 			ch.AddMutation(func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
 				body, err := jsonpatch.MergePatch(payload.Body, []byte(`{"tmax":50}`))
 				if err == nil {
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 88c56a1d..e53ba7b4 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -2299,7 +2299,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 
 	}
 
-	if spec.BidValidationEnforcement == config.ValidationEnforce {
+	if spec.HostConfigBidValidation.BannerCreativeMaxSize == config.ValidationEnforce || spec.HostConfigBidValidation.SecureMarkup == config.ValidationEnforce {
 		actualBidRespExt := &openrtb_ext.ExtBidResponse{}
 		expectedBidRespExt := &openrtb_ext.ExtBidResponse{}
 		if bid.Ext != nil {
@@ -2311,7 +2311,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 			assert.NoError(t, err, fmt.Sprintf("Error when unmarshalling: %s", err))
 		}
 
-		assert.Equal(t, expectedBidRespExt.Errors, actualBidRespExt.Errors, "Oh no")
+		assert.Equal(t, expectedBidRespExt.Errors, actualBidRespExt.Errors, "Expected errors from response ext do not match")
 	}
 }
 
@@ -4674,7 +4674,6 @@ type exchangeSpec struct {
 	RequestType                *metrics.RequestType   `json:"requestType,omitempty"`
 	PassthroughFlag            bool                   `json:"passthrough_flag,omitempty"`
 	HostSChainFlag             bool                   `json:"host_schain_flag,omitempty"`
-	BidValidationEnforcement   string                 `json:"bid_validation_flag,omitempty"`
 	HostConfigBidValidation    config.Validations     `json:"host_bid_validations"`
 	AccountConfigBidValidation config.Validations     `json:"account_bid_validations"`
 }
@@ -5085,7 +5084,7 @@ type mockUpdateBidRequestHook struct{}
 
 func (e mockUpdateBidRequestHook) HandleBidderRequestHook(_ context.Context, mctx hookstage.ModuleInvocationContext, _ hookstage.BidderRequestPayload) (hookstage.HookResult[hookstage.BidderRequestPayload], error) {
 	time.Sleep(50 * time.Millisecond)
-	c := &hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
+	c := hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
 	c.AddMutation(
 		func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) {
 			payload.BidRequest.Site.Name = "test"
diff --git a/hooks/hookexecution/mocks_test.go b/hooks/hookexecution/mocks_test.go
index ab206fd9..e8670ff8 100644
--- a/hooks/hookexecution/mocks_test.go
+++ b/hooks/hookexecution/mocks_test.go
@@ -12,7 +12,7 @@ import (
 type mockUpdateHeaderEntrypointHook struct{}
 
 func (e mockUpdateHeaderEntrypointHook) HandleEntrypointHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.EntrypointPayload) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
-	c := &hookstage.ChangeSet[hookstage.EntrypointPayload]{}
+	c := hookstage.ChangeSet[hookstage.EntrypointPayload]{}
 	c.AddMutation(func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
 		payload.Request.Header.Add("foo", "bar")
 		return payload, nil
@@ -24,7 +24,7 @@ func (e mockUpdateHeaderEntrypointHook) HandleEntrypointHook(_ context.Context,
 type mockUpdateQueryEntrypointHook struct{}
 
 func (e mockUpdateQueryEntrypointHook) HandleEntrypointHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.EntrypointPayload) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
-	c := &hookstage.ChangeSet[hookstage.EntrypointPayload]{}
+	c := hookstage.ChangeSet[hookstage.EntrypointPayload]{}
 	c.AddMutation(func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
 		params := payload.Request.URL.Query()
 		params.Add("foo", "baz")
@@ -38,7 +38,7 @@ func (e mockUpdateQueryEntrypointHook) HandleEntrypointHook(_ context.Context, _
 type mockUpdateBodyHook struct{}
 
 func (e mockUpdateBodyHook) HandleEntrypointHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.EntrypointPayload) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
-	c := &hookstage.ChangeSet[hookstage.EntrypointPayload]{}
+	c := hookstage.ChangeSet[hookstage.EntrypointPayload]{}
 	c.AddMutation(
 		func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
 			payload.Body = []byte(`{"name": "John", "last_name": "Doe", "foo": "bar"}`)
@@ -55,7 +55,7 @@ func (e mockUpdateBodyHook) HandleEntrypointHook(_ context.Context, _ hookstage.
 }
 
 func (e mockUpdateBodyHook) HandleRawAuctionHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.RawAuctionRequestPayload) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) {
-	c := &hookstage.ChangeSet[hookstage.RawAuctionRequestPayload]{}
+	c := hookstage.ChangeSet[hookstage.RawAuctionRequestPayload]{}
 	c.AddMutation(
 		func(payload hookstage.RawAuctionRequestPayload) (hookstage.RawAuctionRequestPayload, error) {
 			payload = []byte(`{"name": "John", "last_name": "Doe", "foo": "bar"}`)
@@ -105,7 +105,7 @@ type mockTimeoutHook struct{}
 
 func (e mockTimeoutHook) HandleEntrypointHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.EntrypointPayload) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
 	time.Sleep(20 * time.Millisecond)
-	c := &hookstage.ChangeSet[hookstage.EntrypointPayload]{}
+	c := hookstage.ChangeSet[hookstage.EntrypointPayload]{}
 	c.AddMutation(func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
 		params := payload.Request.URL.Query()
 		params.Add("bar", "foo")
@@ -118,7 +118,7 @@ func (e mockTimeoutHook) HandleEntrypointHook(_ context.Context, _ hookstage.Mod
 
 func (e mockTimeoutHook) HandleRawAuctionHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.RawAuctionRequestPayload) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) {
 	time.Sleep(20 * time.Millisecond)
-	c := &hookstage.ChangeSet[hookstage.RawAuctionRequestPayload]{}
+	c := hookstage.ChangeSet[hookstage.RawAuctionRequestPayload]{}
 	c.AddMutation(func(payload hookstage.RawAuctionRequestPayload) (hookstage.RawAuctionRequestPayload, error) {
 		payload = []byte(`{"last_name": "Doe", "foo": "bar", "address": "A st."}`)
 		return payload, nil
@@ -129,7 +129,7 @@ func (e mockTimeoutHook) HandleRawAuctionHook(_ context.Context, _ hookstage.Mod
 
 func (e mockTimeoutHook) HandleProcessedAuctionHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.ProcessedAuctionRequestPayload) (hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload], error) {
 	time.Sleep(20 * time.Millisecond)
-	c := &hookstage.ChangeSet[hookstage.ProcessedAuctionRequestPayload]{}
+	c := hookstage.ChangeSet[hookstage.ProcessedAuctionRequestPayload]{}
 	c.AddMutation(func(payload hookstage.ProcessedAuctionRequestPayload) (hookstage.ProcessedAuctionRequestPayload, error) {
 		payload.BidRequest.User.CustomData = "some-custom-data"
 		return payload, nil
@@ -140,7 +140,7 @@ func (e mockTimeoutHook) HandleProcessedAuctionHook(_ context.Context, _ hooksta
 
 func (e mockTimeoutHook) HandleBidderRequestHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.BidderRequestPayload) (hookstage.HookResult[hookstage.BidderRequestPayload], error) {
 	time.Sleep(20 * time.Millisecond)
-	c := &hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
+	c := hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
 	c.AddMutation(func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) {
 		payload.BidRequest.User.CustomData = "some-custom-data"
 		return payload, nil
@@ -151,7 +151,7 @@ func (e mockTimeoutHook) HandleBidderRequestHook(_ context.Context, _ hookstage.
 
 func (e mockTimeoutHook) HandleRawBidderResponseHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.RawBidderResponsePayload) (hookstage.HookResult[hookstage.RawBidderResponsePayload], error) {
 	time.Sleep(20 * time.Millisecond)
-	c := &hookstage.ChangeSet[hookstage.RawBidderResponsePayload]{}
+	c := hookstage.ChangeSet[hookstage.RawBidderResponsePayload]{}
 	c.AddMutation(func(payload hookstage.RawBidderResponsePayload) (hookstage.RawBidderResponsePayload, error) {
 		payload.Bids[0].BidMeta = &openrtb_ext.ExtBidPrebidMeta{AdapterCode: "new-code"}
 		return payload, nil
@@ -162,7 +162,7 @@ func (e mockTimeoutHook) HandleRawBidderResponseHook(_ context.Context, _ hookst
 
 func (e mockTimeoutHook) HandleAllProcessedBidResponsesHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.AllProcessedBidResponsesPayload) (hookstage.HookResult[hookstage.AllProcessedBidResponsesPayload], error) {
 	time.Sleep(20 * time.Millisecond)
-	c := &hookstage.ChangeSet[hookstage.AllProcessedBidResponsesPayload]{}
+	c := hookstage.ChangeSet[hookstage.AllProcessedBidResponsesPayload]{}
 	c.AddMutation(func(payload hookstage.AllProcessedBidResponsesPayload) (hookstage.AllProcessedBidResponsesPayload, error) {
 		payload.Responses["some-bidder"].Bids[0].BidMeta = &openrtb_ext.ExtBidPrebidMeta{AdapterCode: "new-code"}
 		return payload, nil
@@ -173,7 +173,7 @@ func (e mockTimeoutHook) HandleAllProcessedBidResponsesHook(_ context.Context, _
 
 func (e mockTimeoutHook) HandleAuctionResponseHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.AuctionResponsePayload) (hookstage.HookResult[hookstage.AuctionResponsePayload], error) {
 	time.Sleep(2 * time.Millisecond)
-	c := &hookstage.ChangeSet[hookstage.AuctionResponsePayload]{}
+	c := hookstage.ChangeSet[hookstage.AuctionResponsePayload]{}
 	c.AddMutation(func(payload hookstage.AuctionResponsePayload) (hookstage.AuctionResponsePayload, error) {
 		payload.BidResponse.Ext = []byte("another-byte")
 		return payload, nil
@@ -264,7 +264,7 @@ func (h mockErrorHook) HandleAuctionResponseHook(_ context.Context, miCtx hookst
 type mockFailedMutationHook struct{}
 
 func (h mockFailedMutationHook) HandleEntrypointHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.EntrypointPayload) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
-	changeSet := &hookstage.ChangeSet[hookstage.EntrypointPayload]{}
+	changeSet := hookstage.ChangeSet[hookstage.EntrypointPayload]{}
 	changeSet.AddMutation(func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
 		return payload, errors.New("key not found")
 	}, hookstage.MutationUpdate, "header", "foo")
@@ -273,7 +273,7 @@ func (h mockFailedMutationHook) HandleEntrypointHook(_ context.Context, _ hookst
 }
 
 func (h mockFailedMutationHook) HandleRawAuctionHook(_ context.Context, miCtx hookstage.ModuleInvocationContext, _ hookstage.RawAuctionRequestPayload) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) {
-	changeSet := &hookstage.ChangeSet[hookstage.RawAuctionRequestPayload]{}
+	changeSet := hookstage.ChangeSet[hookstage.RawAuctionRequestPayload]{}
 	changeSet.AddMutation(func(payload hookstage.RawAuctionRequestPayload) (hookstage.RawAuctionRequestPayload, error) {
 		return payload, errors.New("key not found")
 	}, hookstage.MutationUpdate, "header", "foo")
@@ -282,7 +282,7 @@ func (h mockFailedMutationHook) HandleRawAuctionHook(_ context.Context, miCtx ho
 }
 
 func (h mockFailedMutationHook) HandleBidderRequestHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.BidderRequestPayload) (hookstage.HookResult[hookstage.BidderRequestPayload], error) {
-	changeSet := &hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
+	changeSet := hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
 	changeSet.AddMutation(func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) {
 		return payload, errors.New("key not found")
 	}, hookstage.MutationUpdate, "header", "foo")
@@ -291,7 +291,7 @@ func (h mockFailedMutationHook) HandleBidderRequestHook(_ context.Context, _ hoo
 }
 
 func (h mockFailedMutationHook) HandleAllProcessedBidResponsesHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.AllProcessedBidResponsesPayload) (hookstage.HookResult[hookstage.AllProcessedBidResponsesPayload], error) {
-	changeSet := &hookstage.ChangeSet[hookstage.AllProcessedBidResponsesPayload]{}
+	changeSet := hookstage.ChangeSet[hookstage.AllProcessedBidResponsesPayload]{}
 	changeSet.AddMutation(func(payload hookstage.AllProcessedBidResponsesPayload) (hookstage.AllProcessedBidResponsesPayload, error) {
 		return payload, errors.New("key not found")
 	}, hookstage.MutationUpdate, "some-bidder", "bids[0]", "deal_priority")
@@ -302,7 +302,7 @@ func (h mockFailedMutationHook) HandleAllProcessedBidResponsesHook(_ context.Con
 type mockUpdateBidRequestHook struct{}
 
 func (e mockUpdateBidRequestHook) HandleProcessedAuctionHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.ProcessedAuctionRequestPayload) (hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload], error) {
-	c := &hookstage.ChangeSet[hookstage.ProcessedAuctionRequestPayload]{}
+	c := hookstage.ChangeSet[hookstage.ProcessedAuctionRequestPayload]{}
 	c.AddMutation(
 		func(payload hookstage.ProcessedAuctionRequestPayload) (hookstage.ProcessedAuctionRequestPayload, error) {
 			payload.BidRequest.User.Yob = 2000
@@ -319,7 +319,7 @@ func (e mockUpdateBidRequestHook) HandleProcessedAuctionHook(_ context.Context,
 }
 
 func (e mockUpdateBidRequestHook) HandleBidderRequestHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.BidderRequestPayload) (hookstage.HookResult[hookstage.BidderRequestPayload], error) {
-	c := &hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
+	c := hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
 	c.AddMutation(
 		func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) {
 			payload.BidRequest.User.Yob = 2000
@@ -338,7 +338,7 @@ func (e mockUpdateBidRequestHook) HandleBidderRequestHook(_ context.Context, _ h
 type mockUpdateBidderResponseHook struct{}
 
 func (e mockUpdateBidderResponseHook) HandleRawBidderResponseHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.RawBidderResponsePayload) (hookstage.HookResult[hookstage.RawBidderResponsePayload], error) {
-	c := &hookstage.ChangeSet[hookstage.RawBidderResponsePayload]{}
+	c := hookstage.ChangeSet[hookstage.RawBidderResponsePayload]{}
 	c.AddMutation(
 		func(payload hookstage.RawBidderResponsePayload) (hookstage.RawBidderResponsePayload, error) {
 			payload.Bids[0].DealPriority = 10
@@ -352,7 +352,7 @@ func (e mockUpdateBidderResponseHook) HandleRawBidderResponseHook(_ context.Cont
 type mockUpdateBiddersResponsesHook struct{}
 
 func (e mockUpdateBiddersResponsesHook) HandleAllProcessedBidResponsesHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.AllProcessedBidResponsesPayload) (hookstage.HookResult[hookstage.AllProcessedBidResponsesPayload], error) {
-	c := &hookstage.ChangeSet[hookstage.AllProcessedBidResponsesPayload]{}
+	c := hookstage.ChangeSet[hookstage.AllProcessedBidResponsesPayload]{}
 	c.AddMutation(
 		func(payload hookstage.AllProcessedBidResponsesPayload) (hookstage.AllProcessedBidResponsesPayload, error) {
 			payload.Responses["some-bidder"].Bids[0].DealPriority = 10
@@ -366,7 +366,7 @@ func (e mockUpdateBiddersResponsesHook) HandleAllProcessedBidResponsesHook(_ con
 type mockUpdateBidResponseHook struct{}
 
 func (e mockUpdateBidResponseHook) HandleAuctionResponseHook(_ context.Context, _ hookstage.ModuleInvocationContext, _ hookstage.AuctionResponsePayload) (hookstage.HookResult[hookstage.AuctionResponsePayload], error) {
-	c := &hookstage.ChangeSet[hookstage.AuctionResponsePayload]{}
+	c := hookstage.ChangeSet[hookstage.AuctionResponsePayload]{}
 	c.AddMutation(
 		func(payload hookstage.AuctionResponsePayload) (hookstage.AuctionResponsePayload, error) {
 			payload.BidResponse.CustomData = "new-custom-data"
diff --git a/modules/modules_test.go b/modules/modules_test.go
index d7c150a6..1c70ce4b 100644
--- a/modules/modules_test.go
+++ b/modules/modules_test.go
@@ -12,76 +12,80 @@ import (
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/hooks"
 	"github.com/prebid/prebid-server/hooks/hookstage"
+	"github.com/prebid/prebid-server/modules/moduledeps"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestModuleBuilderBuild(t *testing.T) {
 	vendor := "acme"
 	moduleName := "foobar"
+	defaultModulesConfig := map[string]map[string]interface{}{vendor: {moduleName: map[string]interface{}{"enabled": true}}}
+	defaultHookRepository, err := hooks.NewHookRepository(map[string]interface{}{vendor + "." + moduleName: module{}})
+	if err != nil {
+		t.Fatalf("Failed to init default hook repository: %s", err)
+	}
+	emptyHookRepository, err := hooks.NewHookRepository(map[string]interface{}{})
+	if err != nil {
+		t.Fatalf("Failed to init empty hook repository: %s", err)
+	}
 
 	testCases := map[string]struct {
-		isHookFound          bool
-		expectedModStageColl map[string][]string
-		expectedHook         interface{}
-		givenModule          interface{}
-		givenConfig          config.Modules
-		expectedErr          error
-		givenHookBuilderErr  error
-		givenGetHookFn       func(repo hooks.HookRepository, module string) (interface{}, bool)
+		givenModule           interface{}
+		givenConfig           config.Modules
+		givenHookBuilderErr   error
+		expectedHookRepo      hooks.HookRepository
+		expectedModulesStages map[string][]string
+		expectedErr           error
 	}{
-		"Can build with entrypoint hook without config": {
-			isHookFound:          true,
-			expectedModStageColl: map[string][]string{vendor + "_" + moduleName: {hooks.StageEntrypoint.String(), hooks.StageAuctionResponse.String()}},
-			expectedHook:         module{},
-			givenModule:          module{},
-			givenGetHookFn: func(repo hooks.HookRepository, module string) (interface{}, bool) {
-				return repo.GetEntrypointHook(module)
-			},
+		"Can build module with config": {
+			givenModule:           module{},
+			givenConfig:           defaultModulesConfig,
+			expectedModulesStages: map[string][]string{vendor + "_" + moduleName: {hooks.StageEntrypoint.String(), hooks.StageAuctionResponse.String()}},
+			expectedHookRepo:      defaultHookRepository,
+			expectedErr:           nil,
 		},
-		"Can build with entrypoint hook with config": {
-			isHookFound:          true,
-			expectedModStageColl: map[string][]string{vendor + "_" + moduleName: {hooks.StageEntrypoint.String(), hooks.StageAuctionResponse.String()}},
-			expectedHook:         module{},
-			givenModule:          module{},
-			givenConfig:          map[string]map[string]interface{}{vendor: {moduleName: map[string]bool{"enabled": true}}},
-			givenGetHookFn: func(repo hooks.HookRepository, module string) (interface{}, bool) {
-				return repo.GetEntrypointHook(module)
-			},
+		"Module is not added to hook repository if it's disabled": {
+			givenModule:           module{},
+			givenConfig:           map[string]map[string]interface{}{vendor: {moduleName: map[string]interface{}{"enabled": false, "attr": "val"}}},
+			expectedModulesStages: map[string][]string{},
+			expectedHookRepo:      emptyHookRepository,
+			expectedErr:           nil,
 		},
-		"Can build with auction response hook": {
-			isHookFound:          true,
-			expectedModStageColl: map[string][]string{vendor + "_" + moduleName: {hooks.StageEntrypoint.String(), hooks.StageAuctionResponse.String()}},
-			expectedHook:         module{},
-			givenModule:          module{},
-			givenConfig:          map[string]map[string]interface{}{"vendor": {"module": map[string]bool{"enabled": true}}},
-			givenGetHookFn: func(repo hooks.HookRepository, module string) (interface{}, bool) {
-				return repo.GetAuctionResponseHook(module)
-			},
+		"Module considered disabled if status property not defined in module config": {
+			givenModule:           module{},
+			givenConfig:           map[string]map[string]interface{}{vendor: {moduleName: map[string]interface{}{"foo": "bar"}}},
+			expectedHookRepo:      emptyHookRepository,
+			expectedModulesStages: map[string][]string{},
+			expectedErr:           nil,
 		},
-		"Fails to find not registered hook": {
-			isHookFound:          false,
-			expectedModStageColl: map[string][]string{vendor + "_" + moduleName: {hooks.StageEntrypoint.String(), hooks.StageAuctionResponse.String()}},
-			expectedHook:         nil,
-			givenModule:          module{},
-			givenConfig:          map[string]map[string]interface{}{vendor: {"module": map[string]bool{"enabled": true}}},
-			givenGetHookFn: func(repo hooks.HookRepository, module string) (interface{}, bool) {
-				return repo.GetAllProcessedBidResponsesHook(module) // ask for hook not implemented in module
-			},
+		"Module considered disabled if its config not provided and as a result skipped from execution": {
+			givenModule:           module{},
+			givenConfig:           nil,
+			expectedHookRepo:      emptyHookRepository,
+			expectedModulesStages: map[string][]string{},
+			expectedErr:           nil,
 		},
 		"Fails if module does not implement any hook interface": {
-			expectedHook: struct{}{},
-			expectedErr:  fmt.Errorf(`hook "%s.%s" does not implement any supported hook interface`, vendor, moduleName),
+			givenModule:           struct{}{},
+			givenConfig:           defaultModulesConfig,
+			expectedHookRepo:      nil,
+			expectedModulesStages: nil,
+			expectedErr:           fmt.Errorf(`hook "%s.%s" does not implement any supported hook interface`, vendor, moduleName),
 		},
 		"Fails if module builder function returns error": {
-			givenModule:         module{},
-			givenConfig:         map[string]map[string]interface{}{vendor: {moduleName: map[string]string{"media_type": "video"}}},
-			givenHookBuilderErr: errors.New("failed to build module"),
-			expectedErr:         fmt.Errorf(`failed to init "%s.%s" module: %s`, vendor, moduleName, "failed to build module"),
+			givenModule:           module{},
+			givenConfig:           defaultModulesConfig,
+			givenHookBuilderErr:   errors.New("failed to build module"),
+			expectedHookRepo:      nil,
+			expectedModulesStages: nil,
+			expectedErr:           fmt.Errorf(`failed to init "%s.%s" module: %s`, vendor, moduleName, "failed to build module"),
 		},
 		"Fails if config marshaling returns error": {
-			givenModule: module{},
-			givenConfig: map[string]map[string]interface{}{vendor: {moduleName: math.Inf(1)}},
-			expectedErr: fmt.Errorf(`failed to marshal "%s.%s" module config: json: unsupported value: +Inf`, vendor, moduleName),
+			givenModule:           module{},
+			givenConfig:           map[string]map[string]interface{}{vendor: {moduleName: math.Inf(1)}},
+			expectedHookRepo:      nil,
+			expectedModulesStages: nil,
+			expectedErr:           fmt.Errorf(`failed to marshal "%s.%s" module config: json: unsupported value: +Inf`, vendor, moduleName),
 		},
 	}
 
@@ -90,21 +94,17 @@ func TestModuleBuilderBuild(t *testing.T) {
 			builder := &builder{
 				builders: ModuleBuilders{
 					vendor: {
-						moduleName: func(cfg json.RawMessage, client *http.Client) (interface{}, error) {
+						moduleName: func(cfg json.RawMessage, deps moduledeps.ModuleDeps) (interface{}, error) {
 							return test.givenModule, test.givenHookBuilderErr
 						},
 					},
 				},
 			}
 
-			repo, coll, err := builder.Build(test.givenConfig, http.DefaultClient)
+			repo, modulesStages, err := builder.Build(test.givenConfig, moduledeps.ModuleDeps{HTTPClient: http.DefaultClient})
 			assert.Equal(t, test.expectedErr, err)
-			if test.expectedErr == nil {
-				hook, found := test.givenGetHookFn(repo, fmt.Sprintf("%s.%s", vendor, moduleName))
-				assert.Equal(t, test.isHookFound, found)
-				assert.IsType(t, test.expectedHook, hook)
-				assert.Equal(t, test.expectedModStageColl, coll)
-			}
+			assert.Equal(t, test.expectedModulesStages, modulesStages)
+			assert.Equal(t, test.expectedHookRepo, repo)
 		})
 	}
 }
diff --git a/modules/prebid/ortb2blocking/config_test.go b/modules/prebid/ortb2blocking/config_test.go
new file mode 100644
index 00000000..18a51ed5
--- /dev/null
+++ b/modules/prebid/ortb2blocking/config_test.go
@@ -0,0 +1,339 @@
+package ortb2blocking
+
+import (
+	"testing"
+
+	"github.com/prebid/openrtb/v17/adcom1"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+var fullConfig = []byte(`
+{
+  "attributes": {
+    "badv": {
+      "enforce_blocks": true,
+      "block_unknown_adomain": true,
+      "blocked_adomain": [
+        "a.com",
+        "b.com",
+        "c.com"
+      ],
+      "allowed_adomain_for_deals": [
+        "z.com",
+        "x.com"
+      ],
+      "action_overrides": {
+        "blocked_adomain": [
+          {
+            "conditions": {
+              "bidders": [
+                "bidderA",
+                "bidderB"
+              ],
+              "media_types": [
+                "video"
+              ]
+            },
+            "override": [
+              "a.com",
+              "b.com"
+            ]
+          },
+          {
+            "conditions": {
+              "bidders": [
+                "bidderB"
+              ]
+            },
+            "override": [
+              "a.com",
+              "b.com",
+              "c.com",
+              "d.com",
+              "e.com"
+            ]
+          }
+        ],
+        "block_unknown_adomain": [
+          {
+            "conditions": {
+              "bidders": [
+                "bidderA"
+              ],
+              "media_types": [
+                "video"
+              ]
+            },
+            "override": true
+          }
+        ],
+        "allowed_adomain_for_deals": [
+          {
+            "conditions": {
+              "deal_ids": [
+                "12345678"
+              ]
+            },
+            "override": [
+              "a.com"
+            ]
+          }
+        ]
+      }
+    },
+    "bcat": {
+      "enforce_blocks": false,
+      "block_unknown_adv_cat": false,
+      "category_taxonomy": 6,
+      "blocked_adv_cat": [
+        "IAB-1",
+        "IAB-2"
+      ],
+      "allowed_adv_cat_for_deals": [
+        "IAB-1"
+      ],
+      "action_overrides": {
+        "blocked_adv_cat": [
+          {
+            "conditions": {
+              "media_types": [
+                "video"
+              ]
+            },
+            "override": [
+              "IAB-1",
+              "IAB-2",
+              "IAB-3",
+              "IAB-4"
+            ]
+          }
+        ],
+        "enforce_blocks": [
+          {
+            "conditions": {
+              "bidders": [
+                "bidderA"
+              ]
+            },
+            "override": true
+          }
+        ],
+        "block_unknown_adv_cat": [
+          {
+            "conditions": {
+              "media_types": [
+                "video"
+              ]
+            },
+            "override": true
+          }
+        ],
+        "allowed_adv_cat_for_deals": [
+          {
+            "conditions": {
+              "deal_ids": [
+                "1111111"
+              ]
+            },
+            "override": [
+              "IAB-1"
+            ]
+          }
+        ]
+      }
+    },
+    "bapp": {
+      "enforce_blocks": false,
+      "blocked_app": [
+        "app1",
+        "app2"
+      ],
+      "action_overrides": {
+        "blocked_app": [
+          {
+            "conditions": {
+              "bidders": [
+                "bidderA"
+              ]
+            },
+            "override": [
+              "app3"
+            ]
+          }
+        ]
+      }
+    },
+    "btype": {
+      "blocked_banner_type": [
+        3,
+        4
+      ],
+      "action_overrides": {
+        "blocked_banner_type": [
+          {
+            "conditions": {
+              "bidders": [
+                "bidderA"
+              ]
+            },
+            "override": [
+              3,
+              4,
+              5
+            ]
+          }
+        ]
+      }
+    },
+    "battr": {
+      "enforce_blocks": false,
+      "blocked_banner_attr": [
+        1,
+        8,
+        9,
+        10
+      ],
+      "action_overrides": {
+        "enforce_blocks": [
+          {
+            "conditions": {
+              "bidders": [
+                "bidderA"
+              ]
+            },
+            "override": true
+          }
+        ]
+      }
+    }
+  }
+}
+`)
+
+func TestNewConfig(t *testing.T) {
+	c, err := newConfig(fullConfig)
+	require.NoError(t, err)
+
+	// badv
+	assert.True(t, c.Attributes.Badv.EnforceBlocks, "attributes.badv.enforce_blocks")
+	assert.True(t, c.Attributes.Badv.BlockUnknownAdomain, "attributes.badv.block_unknown_adomain")
+	assert.Equal(t, []string{"a.com", "b.com", "c.com"}, c.Attributes.Badv.BlockedAdomain, "attributes.badv.blocked_adomain")
+	assert.Equal(t, []string{"z.com", "x.com"}, c.Attributes.Badv.AllowedAdomainForDeals, "attributes.badv.allowed_adomain_for_deals")
+
+	assert.Empty(t, c.Attributes.Badv.ActionOverrides.EnforceBlocks, "attributes.badv.action_overrides[0].enforce_blocks")
+
+	assert.Equal(t, []string{"bidderA", "bidderB"}, c.Attributes.Badv.ActionOverrides.BlockedAdomain[0].Conditions.Bidders, "attributes.badv.action_overrides[0].blocked_adomain[0].conditions.bidders")
+	assert.Equal(t, []string{"video"}, c.Attributes.Badv.ActionOverrides.BlockedAdomain[0].Conditions.MediaTypes, "attributes.badv.action_overrides[0].blocked_adomain[0].conditions.media_types")
+	assert.Empty(t, c.Attributes.Badv.ActionOverrides.BlockedAdomain[0].Conditions.DealIds, "attributes.badv.action_overrides[0].blocked_adomain[0].conditions.deal_ids")
+	assert.False(t, c.Attributes.Badv.ActionOverrides.BlockedAdomain[0].Override.IsActive, "attributes.badv.action_overrides[0].blocked_adomain[0].override")
+	assert.Empty(t, c.Attributes.Badv.ActionOverrides.BlockedAdomain[0].Override.Ids, "attributes.badv.action_overrides[0].blocked_adomain[0].override")
+	assert.Equal(t, []string{"a.com", "b.com"}, c.Attributes.Badv.ActionOverrides.BlockedAdomain[0].Override.Names, "attributes.badv.action_overrides[0].blocked_adomain[0].override")
+
+	assert.Equal(t, []string{"bidderB"}, c.Attributes.Badv.ActionOverrides.BlockedAdomain[1].Conditions.Bidders, "attributes.badv.action_overrides[0].blocked_adomain[1].conditions.bidders")
+	assert.Empty(t, c.Attributes.Badv.ActionOverrides.BlockedAdomain[1].Conditions.MediaTypes, "attributes.badv.action_overrides[0].blocked_adomain[1].conditions.media_types")
+	assert.Empty(t, c.Attributes.Badv.ActionOverrides.BlockedAdomain[1].Conditions.DealIds, "attributes.badv.action_overrides[0].blocked_adomain[1].conditions.deal_ids")
+	assert.False(t, c.Attributes.Badv.ActionOverrides.BlockedAdomain[1].Override.IsActive, "attributes.badv.action_overrides[0].blocked_adomain[1].override")
+	assert.Empty(t, c.Attributes.Badv.ActionOverrides.BlockedAdomain[1].Override.Ids, "attributes.badv.action_overrides[0].blocked_adomain[1].override")
+	assert.Equal(t, []string{"a.com", "b.com", "c.com", "d.com", "e.com"}, c.Attributes.Badv.ActionOverrides.BlockedAdomain[1].Override.Names, "attributes.badv.action_overrides[0].blocked_adomain[1].override")
+
+	assert.Equal(t, []string{"bidderA"}, c.Attributes.Badv.ActionOverrides.BlockUnknownAdomain[0].Conditions.Bidders, "attributes.badv.action_overrides[0].block_unknown_adomain[0].conditions.bidders")
+	assert.Equal(t, []string{"video"}, c.Attributes.Badv.ActionOverrides.BlockUnknownAdomain[0].Conditions.MediaTypes, "attributes.badv.action_overrides[0].block_unknown_adomain[0].conditions.media_types")
+	assert.True(t, c.Attributes.Badv.ActionOverrides.BlockUnknownAdomain[0].Override.IsActive, "attributes.badv.action_overrides[0].block_unknown_adomain[0].override")
+	assert.Empty(t, c.Attributes.Badv.ActionOverrides.BlockUnknownAdomain[0].Override.Ids, "attributes.badv.action_overrides[0].block_unknown_adomain[0].override")
+	assert.Empty(t, c.Attributes.Badv.ActionOverrides.BlockUnknownAdomain[0].Override.Names, "attributes.badv.action_overrides[0].block_unknown_adomain[0].override")
+
+	assert.Equal(t, []string{"12345678"}, c.Attributes.Badv.ActionOverrides.AllowedAdomainForDeals[0].Conditions.DealIds, "attributes.badv.action_overrides[0].allowed_adomain_for_deals[0].conditions.deal_ids")
+	assert.False(t, c.Attributes.Badv.ActionOverrides.AllowedAdomainForDeals[0].Override.IsActive, "attributes.badv.action_overrides[0].allowed_adomain_for_deals[0].override")
+	assert.Empty(t, c.Attributes.Badv.ActionOverrides.AllowedAdomainForDeals[0].Override.Ids, "attributes.badv.action_overrides[0].allowed_adomain_for_deals[0].override")
+	assert.Equal(t, []string{"a.com"}, c.Attributes.Badv.ActionOverrides.AllowedAdomainForDeals[0].Override.Names, "attributes.badv.action_overrides[0].allowed_adomain_for_deals[0].override")
+
+	// bcat
+	assert.False(t, c.Attributes.Bcat.EnforceBlocks, "attributes.bcat.enforce_blocks")
+	assert.False(t, c.Attributes.Bcat.BlockUnknownAdvCat, "attributes.bcat.block_unknown_adv_cat")
+	assert.Equal(t, adcom1.CategoryTaxonomy(6), c.Attributes.Bcat.CategoryTaxonomy, "attributes.bcat.category_taxonomy")
+	assert.Equal(t, []string{"IAB-1", "IAB-2"}, c.Attributes.Bcat.BlockedAdvCat, "attributes.bcat.blocked_adv_cat")
+	assert.Equal(t, []string{"IAB-1"}, c.Attributes.Bcat.AllowedAdvCatForDeals, "attributes.bcat.allowed_adv_cat_for_deals")
+
+	assert.Equal(t, []string{"video"}, c.Attributes.Bcat.ActionOverrides.BlockedAdvCat[0].Conditions.MediaTypes, "attributes.bcat.action_overrides[0].blocked_adv_cat[0].conditions.media_types")
+	assert.Empty(t, c.Attributes.Bcat.ActionOverrides.BlockedAdvCat[0].Conditions.Bidders, "attributes.bcat.action_overrides[0].blocked_adv_cat[0].conditions.bidders")
+	assert.Empty(t, c.Attributes.Bcat.ActionOverrides.BlockedAdvCat[0].Conditions.DealIds, "attributes.bcat.action_overrides[0].blocked_adv_cat[0].conditions.deal_ids")
+	assert.False(t, c.Attributes.Bcat.ActionOverrides.BlockedAdvCat[0].Override.IsActive, "attributes.bcat.action_overrides[0].blocked_adv_cat[0].override")
+	assert.Empty(t, c.Attributes.Bcat.ActionOverrides.BlockedAdvCat[0].Override.Ids, "attributes.bcat.action_overrides[0].blocked_adv_cat[0].override")
+	assert.Equal(t, []string{"IAB-1", "IAB-2", "IAB-3", "IAB-4"}, c.Attributes.Bcat.ActionOverrides.BlockedAdvCat[0].Override.Names, "attributes.bcat.action_overrides[0].blocked_adv_cat[0].override")
+
+	assert.Equal(t, []string{"bidderA"}, c.Attributes.Bcat.ActionOverrides.EnforceBlocks[0].Conditions.Bidders, "attributes.bcat.action_overrides[0].enforce_blocks[0].conditions.bidders")
+	assert.True(t, c.Attributes.Bcat.ActionOverrides.EnforceBlocks[0].Override.IsActive, "attributes.bcat.action_overrides[0].enforce_blocks[0].override")
+	assert.Empty(t, c.Attributes.Bcat.ActionOverrides.EnforceBlocks[0].Override.Ids, "attributes.bcat.action_overrides[0].enforce_blocks[0].override")
+	assert.Empty(t, c.Attributes.Bcat.ActionOverrides.EnforceBlocks[0].Override.Names, "attributes.bcat.action_overrides[0].enforce_blocks[0].override")
+
+	assert.Equal(t, []string{"video"}, c.Attributes.Bcat.ActionOverrides.BlockUnknownAdvCat[0].Conditions.MediaTypes, "attributes.bcat.action_overrides[0].block_unknown_adv_cat[0].conditions.media_types")
+	assert.True(t, c.Attributes.Bcat.ActionOverrides.BlockUnknownAdvCat[0].Override.IsActive, "attributes.bcat.action_overrides[0].block_unknown_adv_cat[0].override")
+	assert.Empty(t, c.Attributes.Bcat.ActionOverrides.BlockUnknownAdvCat[0].Override.Ids, "attributes.bcat.action_overrides[0].block_unknown_adv_cat[0].override")
+	assert.Empty(t, c.Attributes.Bcat.ActionOverrides.BlockUnknownAdvCat[0].Override.Names, "attributes.bcat.action_overrides[0].block_unknown_adv_cat[0].override")
+
+	assert.Equal(t, []string{"1111111"}, c.Attributes.Bcat.ActionOverrides.AllowedAdvCatForDeals[0].Conditions.DealIds, "attributes.bcat.action_overrides[0].allowed_adv_cat_for_deals[0].conditions.deal_ids")
+	assert.False(t, c.Attributes.Bcat.ActionOverrides.AllowedAdvCatForDeals[0].Override.IsActive, "attributes.bcat.action_overrides[0].allowed_adv_cat_for_deals[0].override")
+	assert.Empty(t, c.Attributes.Bcat.ActionOverrides.AllowedAdvCatForDeals[0].Override.Ids, "attributes.bcat.action_overrides[0].allowed_adv_cat_for_deals[0].override")
+	assert.Equal(t, []string{"IAB-1"}, c.Attributes.Bcat.ActionOverrides.AllowedAdvCatForDeals[0].Override.Names, "attributes.bcat.action_overrides[0].allowed_adv_cat_for_deals[0].override")
+
+	// bapp
+	assert.False(t, c.Attributes.Bapp.EnforceBlocks, "attributes.bapp.enforce_blocks")
+	assert.Equal(t, []string{"app1", "app2"}, c.Attributes.Bapp.BlockedApp, "attributes.bapp.blocked_app")
+	assert.Empty(t, c.Attributes.Bapp.AllowedAppForDeals, "attributes.bapp.allowed_app_for_deals")
+
+	assert.Empty(t, c.Attributes.Bapp.ActionOverrides.AllowedAppForDeals, "attributes.bapp.action_overrides[0].allowed_app_for_deals")
+	assert.Empty(t, c.Attributes.Bapp.ActionOverrides.EnforceBlocks, "attributes.bapp.action_overrides[0].enforce_blocks")
+
+	assert.Equal(t, []string{"bidderA"}, c.Attributes.Bapp.ActionOverrides.BlockedApp[0].Conditions.Bidders, "attributes.bapp.action_overrides[0].blocked_app[0].conditions.bidders")
+	assert.False(t, c.Attributes.Bapp.ActionOverrides.BlockedApp[0].Override.IsActive, "attributes.bapp.action_overrides[0].blocked_app[0].override")
+	assert.Empty(t, c.Attributes.Bapp.ActionOverrides.BlockedApp[0].Override.Ids, "attributes.bapp.action_overrides[0].blocked_app[0].override")
+	assert.Equal(t, []string{"app3"}, c.Attributes.Bapp.ActionOverrides.BlockedApp[0].Override.Names, "attributes.bapp.action_overrides[0].blocked_app[0].override")
+
+	// btype
+	assert.Equal(t, []int{3, 4}, c.Attributes.Btype.BlockedBannerType, "attributes.btype.blocked_banner_type")
+
+	assert.Equal(t, []string{"bidderA"}, c.Attributes.Btype.ActionOverrides.BlockedBannerType[0].Conditions.Bidders, "attributes.btype.action_overrides[0].blocked_banner_type[0].conditions.bidders")
+	assert.Equal(t, []int{3, 4, 5}, c.Attributes.Btype.ActionOverrides.BlockedBannerType[0].Override.Ids, "attributes.btype.action_overrides[0].blocked_banner_type[0].override")
+	assert.Empty(t, c.Attributes.Btype.ActionOverrides.BlockedBannerType[0].Override.Names, "attributes.btype.action_overrides[0].blocked_banner_type[0].override")
+	assert.False(t, c.Attributes.Btype.ActionOverrides.BlockedBannerType[0].Override.IsActive, "attributes.btype.action_overrides[0].blocked_banner_type[0].override")
+
+	// battr
+	assert.Empty(t, c.Attributes.Battr.AllowedBannerAttrForDeals, "attributes.battr.allowed_banner_attr_for_deals")
+	assert.False(t, c.Attributes.Battr.EnforceBlocks, "attributes.battr.enforce_blocks")
+	assert.Equal(t, []int{1, 8, 9, 10}, c.Attributes.Battr.BlockedBannerAttr, "attributes.battr.blocked_banner_attr")
+
+	assert.Empty(t, c.Attributes.Battr.ActionOverrides.AllowedBannerAttrForDeals, "attributes.battr.action_overrides[0].allowed_banner_attr_for_deals")
+	assert.Empty(t, c.Attributes.Battr.ActionOverrides.BlockedBannerAttr, "attributes.battr.action_overrides[0].blocked_banner_attr")
+
+	assert.Equal(t, []string{"bidderA"}, c.Attributes.Battr.ActionOverrides.EnforceBlocks[0].Conditions.Bidders, "attributes.battr.action_overrides[0].enforce_blocks[0].conditions.bidders")
+	assert.True(t, c.Attributes.Battr.ActionOverrides.EnforceBlocks[0].Override.IsActive, "attributes.battr.action_overrides[0].enforce_blocks[0].override")
+	assert.Empty(t, c.Attributes.Battr.ActionOverrides.EnforceBlocks[0].Override.Ids, "attributes.battr.action_overrides[0].enforce_blocks[0].override")
+	assert.Empty(t, c.Attributes.Battr.ActionOverrides.EnforceBlocks[0].Override.Names, "attributes.battr.action_overrides[0].enforce_blocks[0].override")
+}
+
+func TestOverride_UnmarshalJSON(t *testing.T) {
+	// error on invalid JSON
+	override := Override{}
+	assert.Error(t, override.UnmarshalJSON([]byte("...")), "Error expected on invalid JSON.")
+
+	// expect IsActive to be initialized correctly
+	override = Override{}
+	assert.NoError(t, override.UnmarshalJSON([]byte("true")), "Failed to unmarshal bool override.")
+	assert.Equal(t, Override{IsActive: true}, override, "Override.IsActive expected to be true.")
+
+	// expect IDs to be initialized correctly
+	override = Override{}
+	assert.NoError(t, override.UnmarshalJSON([]byte("[1, 2, 3]")), "Failed to unmarshal override with IDs.")
+	assert.Equal(t, Override{Ids: []int{1, 2, 3}}, override, "Invalid override.IDs.")
+
+	// expect Names to be initialized correctly
+	override = Override{}
+	assert.NoError(t, override.UnmarshalJSON([]byte(`["one", "two"]`)), "Failed to unmarshal override with Names.")
+	assert.Equal(t, Override{Names: []string{"one", "two"}}, override, "Invalid override.Names.")
+
+	// expect empty override on ignored JSON
+	override = Override{}
+	assert.NoError(t, override.UnmarshalJSON([]byte(`"string"`)), "Failed to unmarshal override with ignored value.")
+	assert.Equal(t, Override{}, override, "Empty override expected.")
+}