github.com/prebid/prebid-server/v2@v2.18.0/hooks/plan_test.go (about)

     1  package hooks
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/prebid/prebid-server/v2/config"
     9  	"github.com/prebid/prebid-server/v2/hooks/hookstage"
    10  	"github.com/prebid/prebid-server/v2/util/jsonutil"
    11  	"github.com/stretchr/testify/assert"
    12  )
    13  
    14  func TestNewExecutionPlanBuilder(t *testing.T) {
    15  	enabledConfig := config.Hooks{Enabled: true}
    16  	testCases := map[string]struct {
    17  		givenConfig         config.Hooks
    18  		expectedPlanBuilder ExecutionPlanBuilder
    19  	}{
    20  		"Real plan builder returned when hooks enabled": {
    21  			givenConfig:         enabledConfig,
    22  			expectedPlanBuilder: PlanBuilder{hooks: enabledConfig},
    23  		},
    24  		"Empty plan builder returned when hooks disabled": {
    25  			givenConfig:         config.Hooks{Enabled: false},
    26  			expectedPlanBuilder: EmptyPlanBuilder{},
    27  		},
    28  	}
    29  
    30  	for name, test := range testCases {
    31  		t.Run(name, func(t *testing.T) {
    32  			gotPlanBuilder := NewExecutionPlanBuilder(test.givenConfig, nil)
    33  			assert.Equal(t, test.expectedPlanBuilder, gotPlanBuilder)
    34  		})
    35  	}
    36  }
    37  
    38  func TestPlanForEntrypointStage(t *testing.T) {
    39  	const group1 string = `{"timeout":  5, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "foo"}]}`
    40  	const group2 string = `{"timeout": 10, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "bar"}, {"module_code": "ortb2blocking", "hook_impl_code": "block_request"}]}`
    41  	const planData1 string = `{"endpoints": {"/openrtb2/auction": {"stages": {"entrypoint": {"groups": [` + group1 + `]}}}}}`
    42  	const planData2 string = `{"endpoints": {"/openrtb2/auction": {"stages": {"entrypoint": {"groups": [` + group2 + `,` + group1 + `]}}}, "/openrtb2/amp": {"stages": {"entrypoint": {"groups": [` + group1 + `]}}}}}`
    43  
    44  	testCases := map[string]struct {
    45  		givenEndpoint               string
    46  		givenHostPlanData           []byte
    47  		givenDefaultAccountPlanData []byte
    48  		givenHooks                  map[string]interface{}
    49  		expectedPlan                Plan[hookstage.Entrypoint]
    50  	}{
    51  		"Host and default-account execution plans successfully merged": {
    52  			givenEndpoint:               "/openrtb2/auction",
    53  			givenHostPlanData:           []byte(planData1),
    54  			givenDefaultAccountPlanData: []byte(planData2),
    55  			givenHooks: map[string]interface{}{
    56  				"foobar":        fakeEntrypointHook{},
    57  				"ortb2blocking": fakeEntrypointHook{},
    58  			},
    59  			expectedPlan: Plan[hookstage.Entrypoint]{
    60  				// first group from host-level plan
    61  				Group[hookstage.Entrypoint]{
    62  					Timeout: 5 * time.Millisecond,
    63  					Hooks: []HookWrapper[hookstage.Entrypoint]{
    64  						{Module: "foobar", Code: "foo", Hook: fakeEntrypointHook{}},
    65  					},
    66  				},
    67  				// then groups from the account-level plan
    68  				Group[hookstage.Entrypoint]{
    69  					Timeout: 10 * time.Millisecond,
    70  					Hooks: []HookWrapper[hookstage.Entrypoint]{
    71  						{Module: "foobar", Code: "bar", Hook: fakeEntrypointHook{}},
    72  						{Module: "ortb2blocking", Code: "block_request", Hook: fakeEntrypointHook{}},
    73  					},
    74  				},
    75  				Group[hookstage.Entrypoint]{
    76  					Timeout: 5 * time.Millisecond,
    77  					Hooks: []HookWrapper[hookstage.Entrypoint]{
    78  						{Module: "foobar", Code: "foo", Hook: fakeEntrypointHook{}},
    79  					},
    80  				},
    81  			},
    82  		},
    83  		"Works with empty default-account-execution_plan": {
    84  			givenEndpoint:               "/openrtb2/auction",
    85  			givenHostPlanData:           []byte(planData1),
    86  			givenDefaultAccountPlanData: []byte(`{}`),
    87  			givenHooks:                  map[string]interface{}{"foobar": fakeEntrypointHook{}},
    88  			expectedPlan: Plan[hookstage.Entrypoint]{
    89  				Group[hookstage.Entrypoint]{
    90  					Timeout: 5 * time.Millisecond,
    91  					Hooks: []HookWrapper[hookstage.Entrypoint]{
    92  						{Module: "foobar", Code: "foo", Hook: fakeEntrypointHook{}},
    93  					},
    94  				},
    95  			},
    96  		},
    97  		"Works with empty host-execution_plan": {
    98  			givenEndpoint:               "/openrtb2/auction",
    99  			givenHostPlanData:           []byte(`{}`),
   100  			givenDefaultAccountPlanData: []byte(planData1),
   101  			givenHooks:                  map[string]interface{}{"foobar": fakeEntrypointHook{}},
   102  			expectedPlan: Plan[hookstage.Entrypoint]{
   103  				Group[hookstage.Entrypoint]{
   104  					Timeout: 5 * time.Millisecond,
   105  					Hooks: []HookWrapper[hookstage.Entrypoint]{
   106  						{Module: "foobar", Code: "foo", Hook: fakeEntrypointHook{}},
   107  					},
   108  				},
   109  			},
   110  		},
   111  		"Empty plan if hooks config not defined": {
   112  			givenEndpoint:               "/openrtb2/auction",
   113  			givenHostPlanData:           []byte(`{}`),
   114  			givenDefaultAccountPlanData: []byte(`{}`),
   115  			givenHooks:                  map[string]interface{}{"foobar": fakeEntrypointHook{}},
   116  			expectedPlan:                Plan[hookstage.Entrypoint]{},
   117  		},
   118  		"Empty plan if hook repository empty": {
   119  			givenEndpoint:               "/openrtb2/auction",
   120  			givenHostPlanData:           []byte(planData1),
   121  			givenDefaultAccountPlanData: []byte(`{}`),
   122  			givenHooks:                  nil,
   123  			expectedPlan:                Plan[hookstage.Entrypoint]{},
   124  		},
   125  	}
   126  
   127  	for name, test := range testCases {
   128  		t.Run(name, func(t *testing.T) {
   129  			planBuilder, err := getPlanBuilder(test.givenHooks, test.givenHostPlanData, test.givenDefaultAccountPlanData)
   130  			if assert.NoError(t, err, "Failed to init hook execution plan builder") {
   131  				assert.Equal(t, test.expectedPlan, planBuilder.PlanForEntrypointStage(test.givenEndpoint))
   132  			}
   133  		})
   134  	}
   135  }
   136  
   137  func TestPlanForRawAuctionStage(t *testing.T) {
   138  	const group1 string = `{"timeout":  5, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "foo"}]}`
   139  	const group2 string = `{"timeout": 10, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "bar"}, {"module_code": "ortb2blocking", "hook_impl_code": "block_request"}]}`
   140  	const group3 string = `{"timeout": 15, "hook_sequence": [{"module_code": "prebid", "hook_impl_code": "baz"}]}`
   141  	const hostPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"raw_auction_request": {"groups": [` + group1 + `]}}}}}`
   142  	const defaultAccountPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"raw_auction_request": {"groups": [` + group2 + `,` + group1 + `]}}}, "/openrtb2/amp": {"stages": {"entrypoint": {"groups": [` + group1 + `]}}}}}`
   143  	const accountPlanData string = `{"execution_plan": {"endpoints": {"/openrtb2/auction": {"stages": {"raw_auction_request": {"groups": [` + group3 + `]}}}}}}`
   144  
   145  	hooks := map[string]interface{}{
   146  		"foobar":        fakeRawAuctionHook{},
   147  		"ortb2blocking": fakeRawAuctionHook{},
   148  		"prebid":        fakeRawAuctionHook{},
   149  	}
   150  
   151  	testCases := map[string]struct {
   152  		givenEndpoint               string
   153  		givenHostPlanData           []byte
   154  		givenDefaultAccountPlanData []byte
   155  		giveAccountPlanData         []byte
   156  		givenHooks                  map[string]interface{}
   157  		expectedPlan                Plan[hookstage.RawAuctionRequest]
   158  	}{
   159  		"Account-specific execution plan rewrites default-account execution plan": {
   160  			givenEndpoint:               "/openrtb2/auction",
   161  			givenHostPlanData:           []byte(hostPlanData),
   162  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   163  			giveAccountPlanData:         []byte(accountPlanData),
   164  			givenHooks:                  hooks,
   165  			expectedPlan: Plan[hookstage.RawAuctionRequest]{
   166  				// first group from host-level plan
   167  				Group[hookstage.RawAuctionRequest]{
   168  					Timeout: 5 * time.Millisecond,
   169  					Hooks: []HookWrapper[hookstage.RawAuctionRequest]{
   170  						{Module: "foobar", Code: "foo", Hook: fakeRawAuctionHook{}},
   171  					},
   172  				},
   173  				// then come groups from account-level plan (default-account-level plan ignored)
   174  				Group[hookstage.RawAuctionRequest]{
   175  					Timeout: 15 * time.Millisecond,
   176  					Hooks: []HookWrapper[hookstage.RawAuctionRequest]{
   177  						{Module: "prebid", Code: "baz", Hook: fakeRawAuctionHook{}},
   178  					},
   179  				},
   180  			},
   181  		},
   182  		"Works with only account-specific plan": {
   183  			givenEndpoint:               "/openrtb2/auction",
   184  			givenHostPlanData:           []byte(`{}`),
   185  			givenDefaultAccountPlanData: []byte(`{}`),
   186  			giveAccountPlanData:         []byte(accountPlanData),
   187  			givenHooks:                  hooks,
   188  			expectedPlan: Plan[hookstage.RawAuctionRequest]{
   189  				Group[hookstage.RawAuctionRequest]{
   190  					Timeout: 15 * time.Millisecond,
   191  					Hooks: []HookWrapper[hookstage.RawAuctionRequest]{
   192  						{Module: "prebid", Code: "baz", Hook: fakeRawAuctionHook{}},
   193  					},
   194  				},
   195  			},
   196  		},
   197  		"Works with empty account-specific execution plan": {
   198  			givenEndpoint:               "/openrtb2/auction",
   199  			givenHostPlanData:           []byte(hostPlanData),
   200  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   201  			giveAccountPlanData:         []byte(`{}`),
   202  			givenHooks:                  hooks,
   203  			expectedPlan: Plan[hookstage.RawAuctionRequest]{
   204  				Group[hookstage.RawAuctionRequest]{
   205  					Timeout: 5 * time.Millisecond,
   206  					Hooks: []HookWrapper[hookstage.RawAuctionRequest]{
   207  						{Module: "foobar", Code: "foo", Hook: fakeRawAuctionHook{}},
   208  					},
   209  				},
   210  				Group[hookstage.RawAuctionRequest]{
   211  					Timeout: 10 * time.Millisecond,
   212  					Hooks: []HookWrapper[hookstage.RawAuctionRequest]{
   213  						{Module: "foobar", Code: "bar", Hook: fakeRawAuctionHook{}},
   214  						{Module: "ortb2blocking", Code: "block_request", Hook: fakeRawAuctionHook{}},
   215  					},
   216  				},
   217  				Group[hookstage.RawAuctionRequest]{
   218  					Timeout: 5 * time.Millisecond,
   219  					Hooks: []HookWrapper[hookstage.RawAuctionRequest]{
   220  						{Module: "foobar", Code: "foo", Hook: fakeRawAuctionHook{}},
   221  					},
   222  				},
   223  			},
   224  		},
   225  	}
   226  
   227  	for name, test := range testCases {
   228  		t.Run(name, func(t *testing.T) {
   229  			account := new(config.Account)
   230  			if err := jsonutil.UnmarshalValid(test.giveAccountPlanData, &account.Hooks); err != nil {
   231  				t.Fatal(err)
   232  			}
   233  
   234  			planBuilder, err := getPlanBuilder(test.givenHooks, test.givenHostPlanData, test.givenDefaultAccountPlanData)
   235  			if assert.NoError(t, err, "Failed to init hook execution plan builder") {
   236  				plan := planBuilder.PlanForRawAuctionStage(test.givenEndpoint, account)
   237  				assert.Equal(t, test.expectedPlan, plan)
   238  			}
   239  		})
   240  	}
   241  }
   242  
   243  func TestPlanForProcessedAuctionStage(t *testing.T) {
   244  	const group1 string = `{"timeout":  5, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "foo"}]}`
   245  	const group2 string = `{"timeout": 10, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "bar"}, {"module_code": "ortb2blocking", "hook_impl_code": "block_request"}]}`
   246  	const group3 string = `{"timeout": 15, "hook_sequence": [{"module_code": "prebid", "hook_impl_code": "baz"}]}`
   247  	const hostPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"processed_auction_request": {"groups": [` + group1 + `]}}}}}`
   248  	const defaultAccountPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"processed_auction_request": {"groups": [` + group2 + `,` + group1 + `]}}}, "/openrtb2/amp": {"stages": {"entrypoint": {"groups": [` + group1 + `]}}}}}`
   249  	const accountPlanData string = `{"execution_plan": {"endpoints": {"/openrtb2/auction": {"stages": {"processed_auction_request": {"groups": [` + group3 + `]}}}}}}`
   250  
   251  	hooks := map[string]interface{}{
   252  		"foobar":        fakeProcessedAuctionHook{},
   253  		"ortb2blocking": fakeProcessedAuctionHook{},
   254  		"prebid":        fakeProcessedAuctionHook{},
   255  	}
   256  
   257  	testCases := map[string]struct {
   258  		givenEndpoint               string
   259  		givenHostPlanData           []byte
   260  		givenDefaultAccountPlanData []byte
   261  		giveAccountPlanData         []byte
   262  		givenHooks                  map[string]interface{}
   263  		expectedPlan                Plan[hookstage.ProcessedAuctionRequest]
   264  	}{
   265  		"Account-specific execution plan rewrites default-account execution plan": {
   266  			givenEndpoint:               "/openrtb2/auction",
   267  			givenHostPlanData:           []byte(hostPlanData),
   268  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   269  			giveAccountPlanData:         []byte(accountPlanData),
   270  			givenHooks:                  hooks,
   271  			expectedPlan: Plan[hookstage.ProcessedAuctionRequest]{
   272  				// first group from host-level plan
   273  				Group[hookstage.ProcessedAuctionRequest]{
   274  					Timeout: 5 * time.Millisecond,
   275  					Hooks: []HookWrapper[hookstage.ProcessedAuctionRequest]{
   276  						{Module: "foobar", Code: "foo", Hook: fakeProcessedAuctionHook{}},
   277  					},
   278  				},
   279  				// then come groups from account-level plan (default-account-level plan ignored)
   280  				Group[hookstage.ProcessedAuctionRequest]{
   281  					Timeout: 15 * time.Millisecond,
   282  					Hooks: []HookWrapper[hookstage.ProcessedAuctionRequest]{
   283  						{Module: "prebid", Code: "baz", Hook: fakeProcessedAuctionHook{}},
   284  					},
   285  				},
   286  			},
   287  		},
   288  		"Works with only account-specific plan": {
   289  			givenEndpoint:               "/openrtb2/auction",
   290  			givenHostPlanData:           []byte(`{}`),
   291  			givenDefaultAccountPlanData: []byte(`{}`),
   292  			giveAccountPlanData:         []byte(accountPlanData),
   293  			givenHooks:                  hooks,
   294  			expectedPlan: Plan[hookstage.ProcessedAuctionRequest]{
   295  				Group[hookstage.ProcessedAuctionRequest]{
   296  					Timeout: 15 * time.Millisecond,
   297  					Hooks: []HookWrapper[hookstage.ProcessedAuctionRequest]{
   298  						{Module: "prebid", Code: "baz", Hook: fakeProcessedAuctionHook{}},
   299  					},
   300  				},
   301  			},
   302  		},
   303  		"Works with empty account-specific execution plan": {
   304  			givenEndpoint:               "/openrtb2/auction",
   305  			givenHostPlanData:           []byte(hostPlanData),
   306  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   307  			giveAccountPlanData:         []byte(`{}`),
   308  			givenHooks:                  hooks,
   309  			expectedPlan: Plan[hookstage.ProcessedAuctionRequest]{
   310  				Group[hookstage.ProcessedAuctionRequest]{
   311  					Timeout: 5 * time.Millisecond,
   312  					Hooks: []HookWrapper[hookstage.ProcessedAuctionRequest]{
   313  						{Module: "foobar", Code: "foo", Hook: fakeProcessedAuctionHook{}},
   314  					},
   315  				},
   316  				Group[hookstage.ProcessedAuctionRequest]{
   317  					Timeout: 10 * time.Millisecond,
   318  					Hooks: []HookWrapper[hookstage.ProcessedAuctionRequest]{
   319  						{Module: "foobar", Code: "bar", Hook: fakeProcessedAuctionHook{}},
   320  						{Module: "ortb2blocking", Code: "block_request", Hook: fakeProcessedAuctionHook{}},
   321  					},
   322  				},
   323  				Group[hookstage.ProcessedAuctionRequest]{
   324  					Timeout: 5 * time.Millisecond,
   325  					Hooks: []HookWrapper[hookstage.ProcessedAuctionRequest]{
   326  						{Module: "foobar", Code: "foo", Hook: fakeProcessedAuctionHook{}},
   327  					},
   328  				},
   329  			},
   330  		},
   331  	}
   332  
   333  	for name, test := range testCases {
   334  		t.Run(name, func(t *testing.T) {
   335  			account := new(config.Account)
   336  			if err := jsonutil.UnmarshalValid(test.giveAccountPlanData, &account.Hooks); err != nil {
   337  				t.Fatal(err)
   338  			}
   339  
   340  			planBuilder, err := getPlanBuilder(test.givenHooks, test.givenHostPlanData, test.givenDefaultAccountPlanData)
   341  			if assert.NoError(t, err, "Failed to init hook execution plan builder") {
   342  				plan := planBuilder.PlanForProcessedAuctionStage(test.givenEndpoint, account)
   343  				assert.Equal(t, test.expectedPlan, plan)
   344  			}
   345  		})
   346  	}
   347  }
   348  
   349  func TestPlanForBidderRequestStage(t *testing.T) {
   350  	const group1 string = `{"timeout":  5, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "foo"}]}`
   351  	const group2 string = `{"timeout": 10, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "bar"}, {"module_code": "ortb2blocking", "hook_impl_code": "block_request"}]}`
   352  	const group3 string = `{"timeout": 15, "hook_sequence": [{"module_code": "prebid", "hook_impl_code": "baz"}]}`
   353  	const hostPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"bidder_request": {"groups": [` + group1 + `]}}}}}`
   354  	const defaultAccountPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"bidder_request": {"groups": [` + group2 + `,` + group1 + `]}}}, "/openrtb2/amp": {"stages": {"entrypoint": {"groups": [` + group1 + `]}}}}}`
   355  	const accountPlanData string = `{"execution_plan": {"endpoints": {"/openrtb2/auction": {"stages": {"bidder_request": {"groups": [` + group3 + `]}}}}}}`
   356  
   357  	hooks := map[string]interface{}{
   358  		"foobar":        fakeBidderRequestHook{},
   359  		"ortb2blocking": fakeBidderRequestHook{},
   360  		"prebid":        fakeBidderRequestHook{},
   361  	}
   362  
   363  	testCases := map[string]struct {
   364  		givenEndpoint               string
   365  		givenHostPlanData           []byte
   366  		givenDefaultAccountPlanData []byte
   367  		giveAccountPlanData         []byte
   368  		givenHooks                  map[string]interface{}
   369  		expectedPlan                Plan[hookstage.BidderRequest]
   370  	}{
   371  		"Account-specific execution plan rewrites default-account execution plan": {
   372  			givenEndpoint:               "/openrtb2/auction",
   373  			givenHostPlanData:           []byte(hostPlanData),
   374  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   375  			giveAccountPlanData:         []byte(accountPlanData),
   376  			givenHooks:                  hooks,
   377  			expectedPlan: Plan[hookstage.BidderRequest]{
   378  				// first group from host-level plan
   379  				Group[hookstage.BidderRequest]{
   380  					Timeout: 5 * time.Millisecond,
   381  					Hooks: []HookWrapper[hookstage.BidderRequest]{
   382  						{Module: "foobar", Code: "foo", Hook: fakeBidderRequestHook{}},
   383  					},
   384  				},
   385  				// then come groups from account-level plan (default-account-level plan ignored)
   386  				Group[hookstage.BidderRequest]{
   387  					Timeout: 15 * time.Millisecond,
   388  					Hooks: []HookWrapper[hookstage.BidderRequest]{
   389  						{Module: "prebid", Code: "baz", Hook: fakeBidderRequestHook{}},
   390  					},
   391  				},
   392  			},
   393  		},
   394  		"Works with only account-specific plan": {
   395  			givenEndpoint:               "/openrtb2/auction",
   396  			givenHostPlanData:           []byte(`{}`),
   397  			givenDefaultAccountPlanData: []byte(`{}`),
   398  			giveAccountPlanData:         []byte(accountPlanData),
   399  			givenHooks:                  hooks,
   400  			expectedPlan: Plan[hookstage.BidderRequest]{
   401  				Group[hookstage.BidderRequest]{
   402  					Timeout: 15 * time.Millisecond,
   403  					Hooks: []HookWrapper[hookstage.BidderRequest]{
   404  						{Module: "prebid", Code: "baz", Hook: fakeBidderRequestHook{}},
   405  					},
   406  				},
   407  			},
   408  		},
   409  		"Works with empty account-specific execution plan": {
   410  			givenEndpoint:               "/openrtb2/auction",
   411  			givenHostPlanData:           []byte(hostPlanData),
   412  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   413  			giveAccountPlanData:         []byte(`{}`),
   414  			givenHooks:                  hooks,
   415  			expectedPlan: Plan[hookstage.BidderRequest]{
   416  				Group[hookstage.BidderRequest]{
   417  					Timeout: 5 * time.Millisecond,
   418  					Hooks: []HookWrapper[hookstage.BidderRequest]{
   419  						{Module: "foobar", Code: "foo", Hook: fakeBidderRequestHook{}},
   420  					},
   421  				},
   422  				Group[hookstage.BidderRequest]{
   423  					Timeout: 10 * time.Millisecond,
   424  					Hooks: []HookWrapper[hookstage.BidderRequest]{
   425  						{Module: "foobar", Code: "bar", Hook: fakeBidderRequestHook{}},
   426  						{Module: "ortb2blocking", Code: "block_request", Hook: fakeBidderRequestHook{}},
   427  					},
   428  				},
   429  				Group[hookstage.BidderRequest]{
   430  					Timeout: 5 * time.Millisecond,
   431  					Hooks: []HookWrapper[hookstage.BidderRequest]{
   432  						{Module: "foobar", Code: "foo", Hook: fakeBidderRequestHook{}},
   433  					},
   434  				},
   435  			},
   436  		},
   437  	}
   438  
   439  	for name, test := range testCases {
   440  		t.Run(name, func(t *testing.T) {
   441  			account := new(config.Account)
   442  			if err := jsonutil.UnmarshalValid(test.giveAccountPlanData, &account.Hooks); err != nil {
   443  				t.Fatal(err)
   444  			}
   445  
   446  			planBuilder, err := getPlanBuilder(test.givenHooks, test.givenHostPlanData, test.givenDefaultAccountPlanData)
   447  			if assert.NoError(t, err, "Failed to init hook execution plan builder") {
   448  				plan := planBuilder.PlanForBidderRequestStage(test.givenEndpoint, account)
   449  				assert.Equal(t, test.expectedPlan, plan)
   450  			}
   451  		})
   452  	}
   453  }
   454  
   455  func TestPlanForRawBidderResponseStage(t *testing.T) {
   456  	const group1 string = `{"timeout":  5, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "foo"}]}`
   457  	const group2 string = `{"timeout": 10, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "bar"}, {"module_code": "ortb2blocking", "hook_impl_code": "block_request"}]}`
   458  	const group3 string = `{"timeout": 15, "hook_sequence": [{"module_code": "prebid", "hook_impl_code": "baz"}]}`
   459  	const hostPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"raw_bidder_response": {"groups": [` + group1 + `]}}}}}`
   460  	const defaultAccountPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"raw_bidder_response": {"groups": [` + group2 + `,` + group1 + `]}}}, "/openrtb2/amp": {"stages": {"entrypoint": {"groups": [` + group1 + `]}}}}}`
   461  	const accountPlanData string = `{"execution_plan": {"endpoints": {"/openrtb2/auction": {"stages": {"raw_bidder_response": {"groups": [` + group3 + `]}}}}}}`
   462  
   463  	hooks := map[string]interface{}{
   464  		"foobar":        fakeRawBidderResponseHook{},
   465  		"ortb2blocking": fakeRawBidderResponseHook{},
   466  		"prebid":        fakeRawBidderResponseHook{},
   467  	}
   468  
   469  	testCases := map[string]struct {
   470  		givenEndpoint               string
   471  		givenHostPlanData           []byte
   472  		givenDefaultAccountPlanData []byte
   473  		giveAccountPlanData         []byte
   474  		givenHooks                  map[string]interface{}
   475  		expectedPlan                Plan[hookstage.RawBidderResponse]
   476  	}{
   477  		"Account-specific execution plan rewrites default-account execution plan": {
   478  			givenEndpoint:               "/openrtb2/auction",
   479  			givenHostPlanData:           []byte(hostPlanData),
   480  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   481  			giveAccountPlanData:         []byte(accountPlanData),
   482  			givenHooks:                  hooks,
   483  			expectedPlan: Plan[hookstage.RawBidderResponse]{
   484  				// first group from host-level plan
   485  				Group[hookstage.RawBidderResponse]{
   486  					Timeout: 5 * time.Millisecond,
   487  					Hooks: []HookWrapper[hookstage.RawBidderResponse]{
   488  						{Module: "foobar", Code: "foo", Hook: fakeRawBidderResponseHook{}},
   489  					},
   490  				},
   491  				// then come groups from account-level plan (default-account-level plan ignored)
   492  				Group[hookstage.RawBidderResponse]{
   493  					Timeout: 15 * time.Millisecond,
   494  					Hooks: []HookWrapper[hookstage.RawBidderResponse]{
   495  						{Module: "prebid", Code: "baz", Hook: fakeRawBidderResponseHook{}},
   496  					},
   497  				},
   498  			},
   499  		},
   500  		"Works with only account-specific plan": {
   501  			givenEndpoint:               "/openrtb2/auction",
   502  			givenHostPlanData:           []byte(`{}`),
   503  			givenDefaultAccountPlanData: []byte(`{}`),
   504  			giveAccountPlanData:         []byte(accountPlanData),
   505  			givenHooks:                  hooks,
   506  			expectedPlan: Plan[hookstage.RawBidderResponse]{
   507  				Group[hookstage.RawBidderResponse]{
   508  					Timeout: 15 * time.Millisecond,
   509  					Hooks: []HookWrapper[hookstage.RawBidderResponse]{
   510  						{Module: "prebid", Code: "baz", Hook: fakeRawBidderResponseHook{}},
   511  					},
   512  				},
   513  			},
   514  		},
   515  		"Works with empty account-specific execution plan": {
   516  			givenEndpoint:               "/openrtb2/auction",
   517  			givenHostPlanData:           []byte(hostPlanData),
   518  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   519  			giveAccountPlanData:         []byte(`{}`),
   520  			givenHooks:                  hooks,
   521  			expectedPlan: Plan[hookstage.RawBidderResponse]{
   522  				Group[hookstage.RawBidderResponse]{
   523  					Timeout: 5 * time.Millisecond,
   524  					Hooks: []HookWrapper[hookstage.RawBidderResponse]{
   525  						{Module: "foobar", Code: "foo", Hook: fakeRawBidderResponseHook{}},
   526  					},
   527  				},
   528  				Group[hookstage.RawBidderResponse]{
   529  					Timeout: 10 * time.Millisecond,
   530  					Hooks: []HookWrapper[hookstage.RawBidderResponse]{
   531  						{Module: "foobar", Code: "bar", Hook: fakeRawBidderResponseHook{}},
   532  						{Module: "ortb2blocking", Code: "block_request", Hook: fakeRawBidderResponseHook{}},
   533  					},
   534  				},
   535  				Group[hookstage.RawBidderResponse]{
   536  					Timeout: 5 * time.Millisecond,
   537  					Hooks: []HookWrapper[hookstage.RawBidderResponse]{
   538  						{Module: "foobar", Code: "foo", Hook: fakeRawBidderResponseHook{}},
   539  					},
   540  				},
   541  			},
   542  		},
   543  	}
   544  
   545  	for name, test := range testCases {
   546  		t.Run(name, func(t *testing.T) {
   547  			account := new(config.Account)
   548  			if err := jsonutil.UnmarshalValid(test.giveAccountPlanData, &account.Hooks); err != nil {
   549  				t.Fatal(err)
   550  			}
   551  
   552  			planBuilder, err := getPlanBuilder(test.givenHooks, test.givenHostPlanData, test.givenDefaultAccountPlanData)
   553  			if assert.NoError(t, err, "Failed to init hook execution plan builder") {
   554  				plan := planBuilder.PlanForRawBidderResponseStage(test.givenEndpoint, account)
   555  				assert.Equal(t, test.expectedPlan, plan)
   556  			}
   557  		})
   558  	}
   559  }
   560  
   561  func TestPlanForAllProcessedBidResponsesStage(t *testing.T) {
   562  	const group1 string = `{"timeout":  5, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "foo"}]}`
   563  	const group2 string = `{"timeout": 10, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "bar"}, {"module_code": "ortb2blocking", "hook_impl_code": "block_request"}]}`
   564  	const group3 string = `{"timeout": 15, "hook_sequence": [{"module_code": "prebid", "hook_impl_code": "baz"}]}`
   565  	const hostPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"all_processed_bid_responses": {"groups": [` + group1 + `]}}}}}`
   566  	const defaultAccountPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"all_processed_bid_responses": {"groups": [` + group2 + `,` + group1 + `]}}}, "/openrtb2/amp": {"stages": {"entrypoint": {"groups": [` + group1 + `]}}}}}`
   567  	const accountPlanData string = `{"execution_plan": {"endpoints": {"/openrtb2/auction": {"stages": {"all_processed_bid_responses": {"groups": [` + group3 + `]}}}}}}`
   568  
   569  	hooks := map[string]interface{}{
   570  		"foobar":        fakeAllProcessedBidResponsesHook{},
   571  		"ortb2blocking": fakeAllProcessedBidResponsesHook{},
   572  		"prebid":        fakeAllProcessedBidResponsesHook{},
   573  	}
   574  
   575  	testCases := map[string]struct {
   576  		givenEndpoint               string
   577  		givenHostPlanData           []byte
   578  		givenDefaultAccountPlanData []byte
   579  		giveAccountPlanData         []byte
   580  		givenHooks                  map[string]interface{}
   581  		expectedPlan                Plan[hookstage.AllProcessedBidResponses]
   582  	}{
   583  		"Account-specific execution plan rewrites default-account execution plan": {
   584  			givenEndpoint:               "/openrtb2/auction",
   585  			givenHostPlanData:           []byte(hostPlanData),
   586  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   587  			giveAccountPlanData:         []byte(accountPlanData),
   588  			givenHooks:                  hooks,
   589  			expectedPlan: Plan[hookstage.AllProcessedBidResponses]{
   590  				// first group from host-level plan
   591  				Group[hookstage.AllProcessedBidResponses]{
   592  					Timeout: 5 * time.Millisecond,
   593  					Hooks: []HookWrapper[hookstage.AllProcessedBidResponses]{
   594  						{Module: "foobar", Code: "foo", Hook: fakeAllProcessedBidResponsesHook{}},
   595  					},
   596  				},
   597  				// then come groups from account-level plan (default-account-level plan ignored)
   598  				Group[hookstage.AllProcessedBidResponses]{
   599  					Timeout: 15 * time.Millisecond,
   600  					Hooks: []HookWrapper[hookstage.AllProcessedBidResponses]{
   601  						{Module: "prebid", Code: "baz", Hook: fakeAllProcessedBidResponsesHook{}},
   602  					},
   603  				},
   604  			},
   605  		},
   606  		"Works with only account-specific plan": {
   607  			givenEndpoint:               "/openrtb2/auction",
   608  			givenHostPlanData:           []byte(`{}`),
   609  			givenDefaultAccountPlanData: []byte(`{}`),
   610  			giveAccountPlanData:         []byte(accountPlanData),
   611  			givenHooks:                  hooks,
   612  			expectedPlan: Plan[hookstage.AllProcessedBidResponses]{
   613  				Group[hookstage.AllProcessedBidResponses]{
   614  					Timeout: 15 * time.Millisecond,
   615  					Hooks: []HookWrapper[hookstage.AllProcessedBidResponses]{
   616  						{Module: "prebid", Code: "baz", Hook: fakeAllProcessedBidResponsesHook{}},
   617  					},
   618  				},
   619  			},
   620  		},
   621  		"Works with empty account-specific execution plan": {
   622  			givenEndpoint:               "/openrtb2/auction",
   623  			givenHostPlanData:           []byte(hostPlanData),
   624  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   625  			giveAccountPlanData:         []byte(`{}`),
   626  			givenHooks:                  hooks,
   627  			expectedPlan: Plan[hookstage.AllProcessedBidResponses]{
   628  				Group[hookstage.AllProcessedBidResponses]{
   629  					Timeout: 5 * time.Millisecond,
   630  					Hooks: []HookWrapper[hookstage.AllProcessedBidResponses]{
   631  						{Module: "foobar", Code: "foo", Hook: fakeAllProcessedBidResponsesHook{}},
   632  					},
   633  				},
   634  				Group[hookstage.AllProcessedBidResponses]{
   635  					Timeout: 10 * time.Millisecond,
   636  					Hooks: []HookWrapper[hookstage.AllProcessedBidResponses]{
   637  						{Module: "foobar", Code: "bar", Hook: fakeAllProcessedBidResponsesHook{}},
   638  						{Module: "ortb2blocking", Code: "block_request", Hook: fakeAllProcessedBidResponsesHook{}},
   639  					},
   640  				},
   641  				Group[hookstage.AllProcessedBidResponses]{
   642  					Timeout: 5 * time.Millisecond,
   643  					Hooks: []HookWrapper[hookstage.AllProcessedBidResponses]{
   644  						{Module: "foobar", Code: "foo", Hook: fakeAllProcessedBidResponsesHook{}},
   645  					},
   646  				},
   647  			},
   648  		},
   649  	}
   650  
   651  	for name, test := range testCases {
   652  		t.Run(name, func(t *testing.T) {
   653  			account := new(config.Account)
   654  			if err := jsonutil.UnmarshalValid(test.giveAccountPlanData, &account.Hooks); err != nil {
   655  				t.Fatal(err)
   656  			}
   657  
   658  			planBuilder, err := getPlanBuilder(test.givenHooks, test.givenHostPlanData, test.givenDefaultAccountPlanData)
   659  			if assert.NoError(t, err, "Failed to init hook execution plan builder") {
   660  				plan := planBuilder.PlanForAllProcessedBidResponsesStage(test.givenEndpoint, account)
   661  				assert.Equal(t, test.expectedPlan, plan)
   662  			}
   663  		})
   664  	}
   665  }
   666  
   667  func TestPlanForAuctionResponseStage(t *testing.T) {
   668  	const group1 string = `{"timeout":  5, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "foo"}]}`
   669  	const group2 string = `{"timeout": 10, "hook_sequence": [{"module_code": "foobar", "hook_impl_code": "bar"}, {"module_code": "ortb2blocking", "hook_impl_code": "block_request"}]}`
   670  	const group3 string = `{"timeout": 15, "hook_sequence": [{"module_code": "prebid", "hook_impl_code": "baz"}]}`
   671  	const hostPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"auction_response": {"groups": [` + group1 + `]}}}}}`
   672  	const defaultAccountPlanData string = `{"endpoints": {"/openrtb2/auction": {"stages": {"auction_response": {"groups": [` + group2 + `,` + group1 + `]}}}, "/openrtb2/amp": {"stages": {"entrypoint": {"groups": [` + group1 + `]}}}}}`
   673  	const accountPlanData string = `{"execution_plan": {"endpoints": {"/openrtb2/auction": {"stages": {"auction_response": {"groups": [` + group3 + `]}}}}}}`
   674  
   675  	hooks := map[string]interface{}{
   676  		"foobar":        fakeAuctionResponseHook{},
   677  		"ortb2blocking": fakeAuctionResponseHook{},
   678  		"prebid":        fakeAuctionResponseHook{},
   679  	}
   680  
   681  	testCases := map[string]struct {
   682  		givenEndpoint               string
   683  		givenHostPlanData           []byte
   684  		givenDefaultAccountPlanData []byte
   685  		giveAccountPlanData         []byte
   686  		givenHooks                  map[string]interface{}
   687  		expectedPlan                Plan[hookstage.AuctionResponse]
   688  	}{
   689  		"Account-specific execution plan rewrites default-account execution plan": {
   690  			givenEndpoint:               "/openrtb2/auction",
   691  			givenHostPlanData:           []byte(hostPlanData),
   692  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   693  			giveAccountPlanData:         []byte(accountPlanData),
   694  			givenHooks:                  hooks,
   695  			expectedPlan: Plan[hookstage.AuctionResponse]{
   696  				// first group from host-level plan
   697  				Group[hookstage.AuctionResponse]{
   698  					Timeout: 5 * time.Millisecond,
   699  					Hooks: []HookWrapper[hookstage.AuctionResponse]{
   700  						{Module: "foobar", Code: "foo", Hook: fakeAuctionResponseHook{}},
   701  					},
   702  				},
   703  				// then come groups from account-level plan (default-account-level plan ignored)
   704  				Group[hookstage.AuctionResponse]{
   705  					Timeout: 15 * time.Millisecond,
   706  					Hooks: []HookWrapper[hookstage.AuctionResponse]{
   707  						{Module: "prebid", Code: "baz", Hook: fakeAuctionResponseHook{}},
   708  					},
   709  				},
   710  			},
   711  		},
   712  		"Works with only account-specific plan": {
   713  			givenEndpoint:               "/openrtb2/auction",
   714  			givenHostPlanData:           []byte(`{}`),
   715  			givenDefaultAccountPlanData: []byte(`{}`),
   716  			giveAccountPlanData:         []byte(accountPlanData),
   717  			givenHooks:                  hooks,
   718  			expectedPlan: Plan[hookstage.AuctionResponse]{
   719  				Group[hookstage.AuctionResponse]{
   720  					Timeout: 15 * time.Millisecond,
   721  					Hooks: []HookWrapper[hookstage.AuctionResponse]{
   722  						{Module: "prebid", Code: "baz", Hook: fakeAuctionResponseHook{}},
   723  					},
   724  				},
   725  			},
   726  		},
   727  		"Works with empty account-specific execution plan": {
   728  			givenEndpoint:               "/openrtb2/auction",
   729  			givenHostPlanData:           []byte(hostPlanData),
   730  			givenDefaultAccountPlanData: []byte(defaultAccountPlanData),
   731  			giveAccountPlanData:         []byte(`{}`),
   732  			givenHooks:                  hooks,
   733  			expectedPlan: Plan[hookstage.AuctionResponse]{
   734  				Group[hookstage.AuctionResponse]{
   735  					Timeout: 5 * time.Millisecond,
   736  					Hooks: []HookWrapper[hookstage.AuctionResponse]{
   737  						{Module: "foobar", Code: "foo", Hook: fakeAuctionResponseHook{}},
   738  					},
   739  				},
   740  				Group[hookstage.AuctionResponse]{
   741  					Timeout: 10 * time.Millisecond,
   742  					Hooks: []HookWrapper[hookstage.AuctionResponse]{
   743  						{Module: "foobar", Code: "bar", Hook: fakeAuctionResponseHook{}},
   744  						{Module: "ortb2blocking", Code: "block_request", Hook: fakeAuctionResponseHook{}},
   745  					},
   746  				},
   747  				Group[hookstage.AuctionResponse]{
   748  					Timeout: 5 * time.Millisecond,
   749  					Hooks: []HookWrapper[hookstage.AuctionResponse]{
   750  						{Module: "foobar", Code: "foo", Hook: fakeAuctionResponseHook{}},
   751  					},
   752  				},
   753  			},
   754  		},
   755  	}
   756  
   757  	for name, test := range testCases {
   758  		t.Run(name, func(t *testing.T) {
   759  			account := new(config.Account)
   760  			if err := jsonutil.UnmarshalValid(test.giveAccountPlanData, &account.Hooks); err != nil {
   761  				t.Fatal(err)
   762  			}
   763  
   764  			planBuilder, err := getPlanBuilder(test.givenHooks, test.givenHostPlanData, test.givenDefaultAccountPlanData)
   765  			if assert.NoError(t, err, "Failed to init hook execution plan builder") {
   766  				plan := planBuilder.PlanForAuctionResponseStage(test.givenEndpoint, account)
   767  				assert.Equal(t, test.expectedPlan, plan)
   768  			}
   769  		})
   770  	}
   771  }
   772  
   773  func getPlanBuilder(
   774  	moduleHooks map[string]interface{},
   775  	hostPlanData, accountPlanData []byte,
   776  ) (ExecutionPlanBuilder, error) {
   777  	var err error
   778  	var hooks config.Hooks
   779  	var hostPlan config.HookExecutionPlan
   780  	var defaultAccountPlan config.HookExecutionPlan
   781  
   782  	err = jsonutil.UnmarshalValid(hostPlanData, &hostPlan)
   783  	if err != nil {
   784  		return nil, err
   785  	}
   786  
   787  	err = jsonutil.UnmarshalValid(accountPlanData, &defaultAccountPlan)
   788  	if err != nil {
   789  		return nil, err
   790  	}
   791  
   792  	hooks.Enabled = true
   793  	hooks.HostExecutionPlan = hostPlan
   794  	hooks.DefaultAccountExecutionPlan = defaultAccountPlan
   795  
   796  	repo, err := NewHookRepository(moduleHooks)
   797  	if err != nil {
   798  		return nil, err
   799  	}
   800  
   801  	return NewExecutionPlanBuilder(hooks, repo), nil
   802  }
   803  
   804  type fakeEntrypointHook struct{}
   805  
   806  func (h fakeEntrypointHook) HandleEntrypointHook(
   807  	_ context.Context,
   808  	_ hookstage.ModuleInvocationContext,
   809  	_ hookstage.EntrypointPayload,
   810  ) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
   811  	return hookstage.HookResult[hookstage.EntrypointPayload]{}, nil
   812  }
   813  
   814  type fakeRawAuctionHook struct{}
   815  
   816  func (f fakeRawAuctionHook) HandleRawAuctionHook(
   817  	_ context.Context,
   818  	_ hookstage.ModuleInvocationContext,
   819  	_ hookstage.RawAuctionRequestPayload,
   820  ) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) {
   821  	return hookstage.HookResult[hookstage.RawAuctionRequestPayload]{}, nil
   822  }
   823  
   824  type fakeProcessedAuctionHook struct{}
   825  
   826  func (f fakeProcessedAuctionHook) HandleProcessedAuctionHook(
   827  	_ context.Context,
   828  	_ hookstage.ModuleInvocationContext,
   829  	_ hookstage.ProcessedAuctionRequestPayload,
   830  ) (hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload], error) {
   831  	return hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload]{}, nil
   832  }
   833  
   834  type fakeBidderRequestHook struct{}
   835  
   836  func (f fakeBidderRequestHook) HandleBidderRequestHook(
   837  	_ context.Context,
   838  	_ hookstage.ModuleInvocationContext,
   839  	_ hookstage.BidderRequestPayload,
   840  ) (hookstage.HookResult[hookstage.BidderRequestPayload], error) {
   841  	return hookstage.HookResult[hookstage.BidderRequestPayload]{}, nil
   842  }
   843  
   844  type fakeRawBidderResponseHook struct{}
   845  
   846  func (f fakeRawBidderResponseHook) HandleRawBidderResponseHook(
   847  	_ context.Context,
   848  	_ hookstage.ModuleInvocationContext,
   849  	_ hookstage.RawBidderResponsePayload,
   850  ) (hookstage.HookResult[hookstage.RawBidderResponsePayload], error) {
   851  	return hookstage.HookResult[hookstage.RawBidderResponsePayload]{}, nil
   852  }
   853  
   854  type fakeAllProcessedBidResponsesHook struct{}
   855  
   856  func (f fakeAllProcessedBidResponsesHook) HandleAllProcessedBidResponsesHook(
   857  	_ context.Context,
   858  	_ hookstage.ModuleInvocationContext,
   859  	_ hookstage.AllProcessedBidResponsesPayload,
   860  ) (hookstage.HookResult[hookstage.AllProcessedBidResponsesPayload], error) {
   861  	return hookstage.HookResult[hookstage.AllProcessedBidResponsesPayload]{}, nil
   862  }
   863  
   864  type fakeAuctionResponseHook struct{}
   865  
   866  func (f fakeAuctionResponseHook) HandleAuctionResponseHook(
   867  	_ context.Context,
   868  	_ hookstage.ModuleInvocationContext,
   869  	_ hookstage.AuctionResponsePayload,
   870  ) (hookstage.HookResult[hookstage.AuctionResponsePayload], error) {
   871  	return hookstage.HookResult[hookstage.AuctionResponsePayload]{}, nil
   872  }