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

     1  package hookexecution
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/prebid/openrtb/v20/openrtb2"
    12  	"github.com/prebid/prebid-server/v2/adapters"
    13  	"github.com/prebid/prebid-server/v2/config"
    14  	"github.com/prebid/prebid-server/v2/exchange/entities"
    15  	"github.com/prebid/prebid-server/v2/hooks"
    16  	"github.com/prebid/prebid-server/v2/hooks/hookanalytics"
    17  	"github.com/prebid/prebid-server/v2/hooks/hookstage"
    18  	"github.com/prebid/prebid-server/v2/metrics"
    19  	metricsConfig "github.com/prebid/prebid-server/v2/metrics/config"
    20  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    21  	"github.com/prebid/prebid-server/v2/privacy"
    22  	"github.com/prebid/prebid-server/v2/util/ptrutil"
    23  	"github.com/stretchr/testify/assert"
    24  	"github.com/stretchr/testify/mock"
    25  )
    26  
    27  func TestEmptyHookExecutor(t *testing.T) {
    28  	executor := EmptyHookExecutor{}
    29  
    30  	body := []byte(`{"foo": "bar"}`)
    31  	reader := bytes.NewReader(body)
    32  	req, err := http.NewRequest(http.MethodPost, "https://prebid.com/openrtb2/auction", reader)
    33  	assert.NoError(t, err, "Failed to create http request.")
    34  
    35  	bidderRequest := &openrtb2.BidRequest{ID: "some-id"}
    36  	expectedBidderRequest := &openrtb2.BidRequest{ID: "some-id"}
    37  
    38  	entrypointBody, entrypointRejectErr := executor.ExecuteEntrypointStage(req, body)
    39  	rawAuctionBody, rawAuctionRejectErr := executor.ExecuteRawAuctionStage(body)
    40  	processedAuctionRejectErr := executor.ExecuteProcessedAuctionStage(&openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}})
    41  	bidderRequestRejectErr := executor.ExecuteBidderRequestStage(&openrtb_ext.RequestWrapper{BidRequest: bidderRequest}, "bidder-name")
    42  	executor.ExecuteAuctionResponseStage(&openrtb2.BidResponse{})
    43  
    44  	outcomes := executor.GetOutcomes()
    45  	assert.Equal(t, EmptyHookExecutor{}, executor, "EmptyHookExecutor shouldn't be changed.")
    46  	assert.Empty(t, outcomes, "EmptyHookExecutor shouldn't return stage outcomes.")
    47  
    48  	assert.Nil(t, entrypointRejectErr, "EmptyHookExecutor shouldn't return reject error at entrypoint stage.")
    49  	assert.Equal(t, body, entrypointBody, "EmptyHookExecutor shouldn't change body at entrypoint stage.")
    50  
    51  	assert.Nil(t, rawAuctionRejectErr, "EmptyHookExecutor shouldn't return reject error at raw-auction stage.")
    52  	assert.Equal(t, body, rawAuctionBody, "EmptyHookExecutor shouldn't change body at raw-auction stage.")
    53  
    54  	assert.Nil(t, processedAuctionRejectErr, "EmptyHookExecutor shouldn't return reject error at processed-auction stage.")
    55  	assert.Nil(t, bidderRequestRejectErr, "EmptyHookExecutor shouldn't return reject error at bidder-request stage.")
    56  	assert.Equal(t, expectedBidderRequest, bidderRequest, "EmptyHookExecutor shouldn't change payload at bidder-request stage.")
    57  }
    58  
    59  func TestExecuteEntrypointStage(t *testing.T) {
    60  	const body string = `{"name": "John", "last_name": "Doe"}`
    61  	const urlString string = "https://prebid.com/openrtb2/auction"
    62  
    63  	foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}}
    64  
    65  	testCases := []struct {
    66  		description            string
    67  		givenBody              string
    68  		givenUrl               string
    69  		givenPlanBuilder       hooks.ExecutionPlanBuilder
    70  		expectedBody           string
    71  		expectedHeader         http.Header
    72  		expectedQuery          url.Values
    73  		expectedReject         *RejectError
    74  		expectedModuleContexts *moduleContexts
    75  		expectedStageOutcomes  []StageOutcome
    76  	}{
    77  		{
    78  			description:            "Payload not changed if hook execution plan empty",
    79  			givenBody:              body,
    80  			givenUrl:               urlString,
    81  			givenPlanBuilder:       hooks.EmptyPlanBuilder{},
    82  			expectedBody:           body,
    83  			expectedHeader:         http.Header{},
    84  			expectedQuery:          url.Values{},
    85  			expectedReject:         nil,
    86  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}},
    87  			expectedStageOutcomes:  []StageOutcome{},
    88  		},
    89  		{
    90  			description:            "Payload changed if hooks return mutations",
    91  			givenBody:              body,
    92  			givenUrl:               urlString,
    93  			givenPlanBuilder:       TestApplyHookMutationsBuilder{},
    94  			expectedBody:           `{"last_name": "Doe", "foo": "bar"}`,
    95  			expectedHeader:         http.Header{"Foo": []string{"bar"}},
    96  			expectedQuery:          url.Values{"foo": []string{"baz"}},
    97  			expectedReject:         nil,
    98  			expectedModuleContexts: foobarModuleCtx,
    99  			expectedStageOutcomes: []StageOutcome{
   100  				{
   101  					Entity: entityHttpRequest,
   102  					Stage:  hooks.StageEntrypoint.String(),
   103  					Groups: []GroupOutcome{
   104  						{
   105  							InvocationResults: []HookOutcome{
   106  								{
   107  									AnalyticsTags: hookanalytics.Analytics{},
   108  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
   109  									Status:        StatusSuccess,
   110  									Action:        ActionUpdate,
   111  									Message:       "",
   112  									DebugMessages: []string{fmt.Sprintf("Hook mutation successfully applied, affected key: header.foo, mutation type: %s", hookstage.MutationUpdate)},
   113  									Errors:        nil,
   114  									Warnings:      nil,
   115  								},
   116  								{
   117  									AnalyticsTags: hookanalytics.Analytics{},
   118  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foobaz"},
   119  									Status:        StatusExecutionFailure,
   120  									Action:        ActionUpdate,
   121  									Message:       "",
   122  									DebugMessages: nil,
   123  									Errors:        nil,
   124  									Warnings:      []string{"failed to apply hook mutation: key not found"},
   125  								},
   126  								{
   127  									AnalyticsTags: hookanalytics.Analytics{},
   128  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
   129  									Status:        StatusSuccess,
   130  									Action:        ActionUpdate,
   131  									Message:       "",
   132  									DebugMessages: []string{fmt.Sprintf("Hook mutation successfully applied, affected key: param.foo, mutation type: %s", hookstage.MutationUpdate)},
   133  									Errors:        nil,
   134  									Warnings:      nil,
   135  								},
   136  							},
   137  						},
   138  						{
   139  							InvocationResults: []HookOutcome{
   140  								{
   141  									AnalyticsTags: hookanalytics.Analytics{},
   142  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "baz"},
   143  									Status:        StatusSuccess,
   144  									Action:        ActionUpdate,
   145  									Message:       "",
   146  									DebugMessages: []string{
   147  										fmt.Sprintf("Hook mutation successfully applied, affected key: body.foo, mutation type: %s", hookstage.MutationUpdate),
   148  										fmt.Sprintf("Hook mutation successfully applied, affected key: body.name, mutation type: %s", hookstage.MutationDelete),
   149  									},
   150  									Errors:   nil,
   151  									Warnings: nil,
   152  								},
   153  								{
   154  									AnalyticsTags: hookanalytics.Analytics{},
   155  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
   156  									Status:        StatusFailure,
   157  									Action:        "",
   158  									Message:       "",
   159  									DebugMessages: nil,
   160  									Errors:        []string{"hook execution failed: attribute not found"},
   161  									Warnings:      nil,
   162  								},
   163  							},
   164  						},
   165  					},
   166  				},
   167  			},
   168  		},
   169  		{
   170  			description:            "Stage execution can be rejected - and later hooks rejected",
   171  			givenBody:              body,
   172  			givenUrl:               urlString,
   173  			givenPlanBuilder:       TestRejectPlanBuilder{},
   174  			expectedBody:           body,
   175  			expectedHeader:         http.Header{"Foo": []string{"bar"}},
   176  			expectedQuery:          url.Values{},
   177  			expectedReject:         &RejectError{0, HookID{ModuleCode: "foobar", HookImplCode: "bar"}, hooks.StageEntrypoint.String()},
   178  			expectedModuleContexts: foobarModuleCtx,
   179  			expectedStageOutcomes: []StageOutcome{
   180  				{
   181  					Entity: entityHttpRequest,
   182  					Stage:  hooks.StageEntrypoint.String(),
   183  					Groups: []GroupOutcome{
   184  						{
   185  							InvocationResults: []HookOutcome{
   186  								{
   187  									AnalyticsTags: hookanalytics.Analytics{},
   188  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
   189  									Status:        StatusSuccess,
   190  									Action:        ActionUpdate,
   191  									Message:       "",
   192  									DebugMessages: []string{
   193  										fmt.Sprintf("Hook mutation successfully applied, affected key: header.foo, mutation type: %s", hookstage.MutationUpdate),
   194  									},
   195  									Errors:   nil,
   196  									Warnings: nil,
   197  								},
   198  								{
   199  									AnalyticsTags: hookanalytics.Analytics{},
   200  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "baz"},
   201  									Status:        StatusExecutionFailure,
   202  									Action:        "",
   203  									Message:       "",
   204  									DebugMessages: nil,
   205  									Errors:        []string{"unexpected error"},
   206  									Warnings:      nil,
   207  								},
   208  							},
   209  						},
   210  						{
   211  							InvocationResults: []HookOutcome{
   212  								{
   213  									AnalyticsTags: hookanalytics.Analytics{},
   214  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
   215  									Status:        StatusSuccess,
   216  									Action:        ActionReject,
   217  									Message:       "",
   218  									DebugMessages: nil,
   219  									Errors: []string{
   220  										`Module foobar (hook: bar) rejected request with code 0 at entrypoint stage`,
   221  									},
   222  									Warnings: nil,
   223  								},
   224  							},
   225  						},
   226  					},
   227  				},
   228  			},
   229  		},
   230  		{
   231  			description:            "Request can be changed when a hook times out",
   232  			givenBody:              body,
   233  			givenUrl:               urlString,
   234  			givenPlanBuilder:       TestWithTimeoutPlanBuilder{},
   235  			expectedBody:           `{"foo":"bar", "last_name":"Doe"}`,
   236  			expectedHeader:         http.Header{"Foo": []string{"bar"}},
   237  			expectedQuery:          url.Values{},
   238  			expectedReject:         nil,
   239  			expectedModuleContexts: foobarModuleCtx,
   240  			expectedStageOutcomes: []StageOutcome{
   241  				{
   242  					Entity: entityHttpRequest,
   243  					Stage:  hooks.StageEntrypoint.String(),
   244  					Groups: []GroupOutcome{
   245  						{
   246  							InvocationResults: []HookOutcome{
   247  								{
   248  									AnalyticsTags: hookanalytics.Analytics{},
   249  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
   250  									Status:        StatusSuccess,
   251  									Action:        ActionUpdate,
   252  									Message:       "",
   253  									DebugMessages: []string{
   254  										fmt.Sprintf("Hook mutation successfully applied, affected key: header.foo, mutation type: %s", hookstage.MutationUpdate),
   255  									},
   256  									Errors:   nil,
   257  									Warnings: nil,
   258  								},
   259  								{
   260  									AnalyticsTags: hookanalytics.Analytics{},
   261  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
   262  									Status:        StatusTimeout,
   263  									Action:        "",
   264  									Message:       "",
   265  									DebugMessages: nil,
   266  									Errors:        []string{"Hook execution timeout"},
   267  									Warnings:      nil,
   268  								},
   269  							},
   270  						},
   271  						{
   272  							InvocationResults: []HookOutcome{
   273  								{
   274  									AnalyticsTags: hookanalytics.Analytics{},
   275  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "baz"},
   276  									Status:        StatusSuccess,
   277  									Action:        ActionUpdate,
   278  									Message:       "",
   279  									DebugMessages: []string{
   280  										fmt.Sprintf("Hook mutation successfully applied, affected key: body.foo, mutation type: %s", hookstage.MutationUpdate),
   281  										fmt.Sprintf("Hook mutation successfully applied, affected key: body.name, mutation type: %s", hookstage.MutationDelete),
   282  									},
   283  									Errors:   nil,
   284  									Warnings: nil,
   285  								},
   286  							},
   287  						},
   288  					},
   289  				},
   290  			},
   291  		},
   292  		{
   293  			description:      "Modules contexts are preserved and correct",
   294  			givenBody:        body,
   295  			givenUrl:         urlString,
   296  			givenPlanBuilder: TestWithModuleContextsPlanBuilder{},
   297  			expectedBody:     body,
   298  			expectedHeader:   http.Header{},
   299  			expectedQuery:    url.Values{},
   300  			expectedReject:   nil,
   301  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{
   302  				"module-1": {"entrypoint-ctx-1": "some-ctx-1", "entrypoint-ctx-3": "some-ctx-3"},
   303  				"module-2": {"entrypoint-ctx-2": "some-ctx-2"},
   304  			}},
   305  			expectedStageOutcomes: []StageOutcome{
   306  				{
   307  					Entity: entityHttpRequest,
   308  					Stage:  hooks.StageEntrypoint.String(),
   309  					Groups: []GroupOutcome{
   310  						{
   311  							InvocationResults: []HookOutcome{
   312  								{
   313  									AnalyticsTags: hookanalytics.Analytics{},
   314  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "foo"},
   315  									Status:        StatusSuccess,
   316  									Action:        ActionNone,
   317  									Message:       "",
   318  									DebugMessages: nil,
   319  									Errors:        nil,
   320  									Warnings:      nil,
   321  								},
   322  							},
   323  						},
   324  						{
   325  							InvocationResults: []HookOutcome{
   326  								{
   327  									AnalyticsTags: hookanalytics.Analytics{},
   328  									HookID:        HookID{ModuleCode: "module-2", HookImplCode: "bar"},
   329  									Status:        StatusSuccess,
   330  									Action:        ActionNone,
   331  									Message:       "",
   332  									DebugMessages: nil,
   333  									Errors:        nil,
   334  									Warnings:      nil,
   335  								},
   336  								{
   337  									AnalyticsTags: hookanalytics.Analytics{},
   338  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "baz"},
   339  									Status:        StatusSuccess,
   340  									Action:        ActionNone,
   341  									Message:       "",
   342  									DebugMessages: nil,
   343  									Errors:        nil,
   344  									Warnings:      nil,
   345  								},
   346  							},
   347  						},
   348  					},
   349  				},
   350  			},
   351  		},
   352  	}
   353  
   354  	for _, test := range testCases {
   355  		t.Run(test.description, func(t *testing.T) {
   356  			body := []byte(test.givenBody)
   357  			reader := bytes.NewReader(body)
   358  			req, err := http.NewRequest(http.MethodPost, test.givenUrl, reader)
   359  			assert.NoError(t, err)
   360  
   361  			exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{})
   362  			newBody, reject := exec.ExecuteEntrypointStage(req, body)
   363  
   364  			assert.Equal(t, test.expectedReject, reject, "Unexpected stage reject.")
   365  			assert.JSONEq(t, test.expectedBody, string(newBody), "Incorrect request body.")
   366  			assert.Equal(t, test.expectedHeader, req.Header, "Incorrect request header.")
   367  			assert.Equal(t, test.expectedQuery, req.URL.Query(), "Incorrect request query.")
   368  			assert.Equal(t, test.expectedModuleContexts, exec.moduleContexts, "Incorrect module contexts")
   369  
   370  			stageOutcomes := exec.GetOutcomes()
   371  			if len(test.expectedStageOutcomes) == 0 {
   372  				assert.Empty(t, stageOutcomes, "Incorrect stage outcomes.")
   373  			} else {
   374  				assertEqualStageOutcomes(t, test.expectedStageOutcomes[0], stageOutcomes[0])
   375  			}
   376  		})
   377  	}
   378  }
   379  
   380  func TestMetricsAreGatheredDuringHookExecution(t *testing.T) {
   381  	reader := bytes.NewReader(nil)
   382  	req, err := http.NewRequest(http.MethodPost, "https://prebid.com/openrtb2/auction", reader)
   383  	assert.NoError(t, err)
   384  
   385  	metricEngine := &metrics.MetricsEngineMock{}
   386  	builder := TestAllHookResultsBuilder{}
   387  	exec := NewHookExecutor(TestAllHookResultsBuilder{}, "/openrtb2/auction", metricEngine)
   388  	moduleName := "module.x-1"
   389  	moduleLabels := metrics.ModuleLabels{
   390  		Module: moduleReplacer.Replace(moduleName),
   391  		Stage:  "entrypoint",
   392  	}
   393  	rTime := func(dur time.Duration) bool { return dur.Nanoseconds() > 0 }
   394  	plan := builder.PlanForEntrypointStage("")
   395  	hooksCalledDuringStage := 0
   396  	for _, group := range plan {
   397  		for range group.Hooks {
   398  			hooksCalledDuringStage++
   399  		}
   400  	}
   401  	metricEngine.On("RecordModuleCalled", moduleLabels, mock.MatchedBy(rTime)).Times(hooksCalledDuringStage)
   402  	metricEngine.On("RecordModuleSuccessUpdated", moduleLabels).Once()
   403  	metricEngine.On("RecordModuleSuccessRejected", moduleLabels).Once()
   404  	metricEngine.On("RecordModuleTimeout", moduleLabels).Once()
   405  	metricEngine.On("RecordModuleExecutionError", moduleLabels).Twice()
   406  	metricEngine.On("RecordModuleFailed", moduleLabels).Once()
   407  	metricEngine.On("RecordModuleSuccessNooped", moduleLabels).Once()
   408  
   409  	_, _ = exec.ExecuteEntrypointStage(req, nil)
   410  
   411  	// Assert that all module metrics funcs were called with the parameters we expected
   412  	metricEngine.AssertExpectations(t)
   413  }
   414  
   415  func TestExecuteRawAuctionStage(t *testing.T) {
   416  	const body string = `{"name": "John", "last_name": "Doe"}`
   417  	const bodyUpdated string = `{"last_name": "Doe", "foo": "bar"}`
   418  	const urlString string = "https://prebid.com/openrtb2/auction"
   419  
   420  	foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}}
   421  
   422  	testCases := []struct {
   423  		description            string
   424  		givenBody              string
   425  		givenUrl               string
   426  		givenPlanBuilder       hooks.ExecutionPlanBuilder
   427  		expectedBody           string
   428  		expectedReject         *RejectError
   429  		expectedModuleContexts *moduleContexts
   430  		expectedStageOutcomes  []StageOutcome
   431  	}{
   432  		{
   433  			description:            "Payload not changed if hook execution plan empty",
   434  			givenBody:              body,
   435  			givenUrl:               urlString,
   436  			givenPlanBuilder:       hooks.EmptyPlanBuilder{},
   437  			expectedBody:           body,
   438  			expectedReject:         nil,
   439  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}},
   440  			expectedStageOutcomes:  []StageOutcome{},
   441  		},
   442  		{
   443  			description:            "Payload changed if hooks return mutations",
   444  			givenBody:              body,
   445  			givenUrl:               urlString,
   446  			givenPlanBuilder:       TestApplyHookMutationsBuilder{},
   447  			expectedBody:           bodyUpdated,
   448  			expectedReject:         nil,
   449  			expectedModuleContexts: foobarModuleCtx,
   450  			expectedStageOutcomes: []StageOutcome{
   451  				{
   452  					Entity: entityAuctionRequest,
   453  					Stage:  hooks.StageRawAuctionRequest.String(),
   454  					Groups: []GroupOutcome{
   455  						{
   456  							InvocationResults: []HookOutcome{
   457  								{
   458  									AnalyticsTags: hookanalytics.Analytics{},
   459  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
   460  									Status:        StatusSuccess,
   461  									Action:        ActionUpdate,
   462  									Message:       "",
   463  									DebugMessages: []string{
   464  										fmt.Sprintf("Hook mutation successfully applied, affected key: body.foo, mutation type: %s", hookstage.MutationUpdate),
   465  										fmt.Sprintf("Hook mutation successfully applied, affected key: body.name, mutation type: %s", hookstage.MutationDelete),
   466  									},
   467  									Errors:   nil,
   468  									Warnings: nil,
   469  								},
   470  								{
   471  									AnalyticsTags: hookanalytics.Analytics{},
   472  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
   473  									Status:        StatusExecutionFailure,
   474  									Action:        ActionUpdate,
   475  									Message:       "",
   476  									DebugMessages: nil,
   477  									Errors:        nil,
   478  									Warnings:      []string{"failed to apply hook mutation: key not found"},
   479  								},
   480  							},
   481  						},
   482  						{
   483  							InvocationResults: []HookOutcome{
   484  								{
   485  									AnalyticsTags: hookanalytics.Analytics{},
   486  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "baz"},
   487  									Status:        StatusFailure,
   488  									Action:        "",
   489  									Message:       "",
   490  									DebugMessages: nil,
   491  									Errors:        []string{"hook execution failed: attribute not found"},
   492  									Warnings:      nil,
   493  								},
   494  							},
   495  						},
   496  					},
   497  				},
   498  			},
   499  		},
   500  		{
   501  			description:            "Stage execution can be rejected - and later hooks rejected",
   502  			givenBody:              body,
   503  			givenUrl:               urlString,
   504  			givenPlanBuilder:       TestRejectPlanBuilder{},
   505  			expectedBody:           bodyUpdated,
   506  			expectedReject:         &RejectError{0, HookID{ModuleCode: "foobar", HookImplCode: "bar"}, hooks.StageRawAuctionRequest.String()},
   507  			expectedModuleContexts: foobarModuleCtx,
   508  			expectedStageOutcomes: []StageOutcome{
   509  				{
   510  					Entity: entityAuctionRequest,
   511  					Stage:  hooks.StageRawAuctionRequest.String(),
   512  					Groups: []GroupOutcome{
   513  						{
   514  							InvocationResults: []HookOutcome{
   515  								{
   516  									AnalyticsTags: hookanalytics.Analytics{},
   517  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
   518  									Status:        StatusSuccess,
   519  									Action:        ActionUpdate,
   520  									Message:       "",
   521  									DebugMessages: []string{
   522  										fmt.Sprintf("Hook mutation successfully applied, affected key: body.foo, mutation type: %s", hookstage.MutationUpdate),
   523  										fmt.Sprintf("Hook mutation successfully applied, affected key: body.name, mutation type: %s", hookstage.MutationDelete),
   524  									},
   525  									Errors:   nil,
   526  									Warnings: nil,
   527  								},
   528  								{
   529  									AnalyticsTags: hookanalytics.Analytics{},
   530  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "baz"},
   531  									Status:        StatusExecutionFailure,
   532  									Action:        "",
   533  									Message:       "",
   534  									DebugMessages: nil,
   535  									Errors:        []string{"unexpected error"},
   536  									Warnings:      nil,
   537  								},
   538  							},
   539  						},
   540  						{
   541  							InvocationResults: []HookOutcome{
   542  								{
   543  									AnalyticsTags: hookanalytics.Analytics{},
   544  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
   545  									Status:        StatusSuccess,
   546  									Action:        ActionReject,
   547  									Message:       "",
   548  									DebugMessages: nil,
   549  									Errors: []string{
   550  										`Module foobar (hook: bar) rejected request with code 0 at raw_auction_request stage`,
   551  									},
   552  									Warnings: nil,
   553  								},
   554  							},
   555  						},
   556  					},
   557  				},
   558  			},
   559  		},
   560  		{
   561  			description:            "Request can be changed when a hook times out",
   562  			givenBody:              body,
   563  			givenUrl:               urlString,
   564  			givenPlanBuilder:       TestWithTimeoutPlanBuilder{},
   565  			expectedBody:           bodyUpdated,
   566  			expectedReject:         nil,
   567  			expectedModuleContexts: foobarModuleCtx,
   568  			expectedStageOutcomes: []StageOutcome{
   569  				{
   570  					Entity: entityAuctionRequest,
   571  					Stage:  hooks.StageRawAuctionRequest.String(),
   572  					Groups: []GroupOutcome{
   573  						{
   574  							InvocationResults: []HookOutcome{
   575  								{
   576  									AnalyticsTags: hookanalytics.Analytics{},
   577  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
   578  									Status:        StatusSuccess,
   579  									Action:        ActionUpdate,
   580  									Message:       "",
   581  									DebugMessages: []string{
   582  										fmt.Sprintf("Hook mutation successfully applied, affected key: body.foo, mutation type: %s", hookstage.MutationUpdate),
   583  										fmt.Sprintf("Hook mutation successfully applied, affected key: body.name, mutation type: %s", hookstage.MutationDelete),
   584  									},
   585  									Errors:   nil,
   586  									Warnings: nil,
   587  								},
   588  							},
   589  						},
   590  						{
   591  							InvocationResults: []HookOutcome{
   592  								{
   593  									AnalyticsTags: hookanalytics.Analytics{},
   594  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
   595  									Status:        StatusTimeout,
   596  									Action:        "",
   597  									Message:       "",
   598  									DebugMessages: nil,
   599  									Errors:        []string{"Hook execution timeout"},
   600  									Warnings:      nil,
   601  								},
   602  							},
   603  						},
   604  					},
   605  				},
   606  			},
   607  		},
   608  		{
   609  			description:      "Modules contexts are preserved and correct",
   610  			givenBody:        body,
   611  			givenUrl:         urlString,
   612  			givenPlanBuilder: TestWithModuleContextsPlanBuilder{},
   613  			expectedBody:     body,
   614  			expectedReject:   nil,
   615  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{
   616  				"module-1": {"raw-auction-ctx-1": "some-ctx-1", "raw-auction-ctx-3": "some-ctx-3"},
   617  				"module-2": {"raw-auction-ctx-2": "some-ctx-2"},
   618  			}},
   619  			expectedStageOutcomes: []StageOutcome{
   620  				{
   621  					Entity: entityAuctionRequest,
   622  					Stage:  hooks.StageRawAuctionRequest.String(),
   623  					Groups: []GroupOutcome{
   624  						{
   625  							InvocationResults: []HookOutcome{
   626  								{
   627  									AnalyticsTags: hookanalytics.Analytics{},
   628  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "foo"},
   629  									Status:        StatusSuccess,
   630  									Action:        ActionNone,
   631  									Message:       "",
   632  									DebugMessages: nil,
   633  									Errors:        nil,
   634  									Warnings:      nil,
   635  								},
   636  								{
   637  									AnalyticsTags: hookanalytics.Analytics{},
   638  									HookID:        HookID{ModuleCode: "module-2", HookImplCode: "baz"},
   639  									Status:        StatusSuccess,
   640  									Action:        ActionNone,
   641  									Message:       "",
   642  									DebugMessages: nil,
   643  									Errors:        nil,
   644  									Warnings:      nil,
   645  								},
   646  							},
   647  						},
   648  						{
   649  							InvocationResults: []HookOutcome{
   650  								{
   651  									AnalyticsTags: hookanalytics.Analytics{},
   652  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "bar"},
   653  									Status:        StatusSuccess,
   654  									Action:        ActionNone,
   655  									Message:       "",
   656  									DebugMessages: nil,
   657  									Errors:        nil,
   658  									Warnings:      nil,
   659  								},
   660  							},
   661  						},
   662  					},
   663  				},
   664  			},
   665  		},
   666  	}
   667  
   668  	for _, test := range testCases {
   669  		t.Run(test.description, func(t *testing.T) {
   670  			exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{})
   671  
   672  			privacyConfig := getModuleActivities("foo", false, false)
   673  			ac := privacy.NewActivityControl(privacyConfig)
   674  			exec.SetActivityControl(ac)
   675  
   676  			newBody, reject := exec.ExecuteRawAuctionStage([]byte(test.givenBody))
   677  
   678  			assert.Equal(t, test.expectedReject, reject, "Unexpected stage reject.")
   679  			assert.JSONEq(t, test.expectedBody, string(newBody), "Incorrect request body.")
   680  			assert.Equal(t, test.expectedModuleContexts, exec.moduleContexts, "Incorrect module contexts")
   681  
   682  			stageOutcomes := exec.GetOutcomes()
   683  			if len(test.expectedStageOutcomes) == 0 {
   684  				assert.Empty(t, stageOutcomes, "Incorrect stage outcomes.")
   685  			} else {
   686  				assertEqualStageOutcomes(t, test.expectedStageOutcomes[0], stageOutcomes[0])
   687  			}
   688  		})
   689  	}
   690  }
   691  
   692  func TestExecuteProcessedAuctionStage(t *testing.T) {
   693  	foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}}
   694  	req := openrtb2.BidRequest{ID: "some-id", User: &openrtb2.User{ID: "user-id"}}
   695  	reqUpdated := openrtb2.BidRequest{ID: "some-id", User: &openrtb2.User{ID: "user-id", Yob: 2000, Consent: "true"}}
   696  
   697  	testCases := []struct {
   698  		description            string
   699  		givenPlanBuilder       hooks.ExecutionPlanBuilder
   700  		givenRequest           openrtb_ext.RequestWrapper
   701  		expectedRequest        openrtb2.BidRequest
   702  		expectedErr            error
   703  		expectedModuleContexts *moduleContexts
   704  		expectedStageOutcomes  []StageOutcome
   705  	}{
   706  		{
   707  			description:            "Request not changed if hook execution plan empty",
   708  			givenPlanBuilder:       hooks.EmptyPlanBuilder{},
   709  			givenRequest:           openrtb_ext.RequestWrapper{BidRequest: &req},
   710  			expectedRequest:        req,
   711  			expectedErr:            nil,
   712  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}},
   713  			expectedStageOutcomes:  []StageOutcome{},
   714  		},
   715  		{
   716  			description:            "Request changed if hooks return mutations",
   717  			givenPlanBuilder:       TestApplyHookMutationsBuilder{},
   718  			givenRequest:           openrtb_ext.RequestWrapper{BidRequest: &req},
   719  			expectedRequest:        reqUpdated,
   720  			expectedErr:            nil,
   721  			expectedModuleContexts: foobarModuleCtx,
   722  			expectedStageOutcomes: []StageOutcome{
   723  				{
   724  					Entity: entityAuctionRequest,
   725  					Stage:  hooks.StageProcessedAuctionRequest.String(),
   726  					Groups: []GroupOutcome{
   727  						{
   728  							InvocationResults: []HookOutcome{
   729  								{
   730  									AnalyticsTags: hookanalytics.Analytics{},
   731  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
   732  									Status:        StatusSuccess,
   733  									Action:        ActionUpdate,
   734  									Message:       "",
   735  									DebugMessages: []string{
   736  										fmt.Sprintf("Hook mutation successfully applied, affected key: bidRequest.user.yob, mutation type: %s", hookstage.MutationUpdate),
   737  										fmt.Sprintf("Hook mutation successfully applied, affected key: bidRequest.user.consent, mutation type: %s", hookstage.MutationUpdate),
   738  									},
   739  									Errors:   nil,
   740  									Warnings: nil,
   741  								},
   742  							},
   743  						},
   744  					},
   745  				},
   746  			},
   747  		},
   748  		{
   749  			description:            "Stage execution can be rejected - and later hooks rejected",
   750  			givenPlanBuilder:       TestRejectPlanBuilder{},
   751  			givenRequest:           openrtb_ext.RequestWrapper{BidRequest: &req},
   752  			expectedRequest:        req,
   753  			expectedErr:            &RejectError{0, HookID{ModuleCode: "foobar", HookImplCode: "foo"}, hooks.StageProcessedAuctionRequest.String()},
   754  			expectedModuleContexts: foobarModuleCtx,
   755  			expectedStageOutcomes: []StageOutcome{
   756  				{
   757  					Entity: entityAuctionRequest,
   758  					Stage:  hooks.StageProcessedAuctionRequest.String(),
   759  					Groups: []GroupOutcome{
   760  						{
   761  							InvocationResults: []HookOutcome{
   762  								{
   763  									AnalyticsTags: hookanalytics.Analytics{},
   764  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
   765  									Status:        StatusSuccess,
   766  									Action:        ActionReject,
   767  									Message:       "",
   768  									DebugMessages: nil,
   769  									Errors: []string{
   770  										`Module foobar (hook: foo) rejected request with code 0 at processed_auction_request stage`,
   771  									},
   772  									Warnings: nil,
   773  								},
   774  							},
   775  						},
   776  					},
   777  				},
   778  			},
   779  		},
   780  		{
   781  			description:            "Request can be changed when a hook times out",
   782  			givenPlanBuilder:       TestWithTimeoutPlanBuilder{},
   783  			givenRequest:           openrtb_ext.RequestWrapper{BidRequest: &req},
   784  			expectedRequest:        reqUpdated,
   785  			expectedErr:            nil,
   786  			expectedModuleContexts: foobarModuleCtx,
   787  			expectedStageOutcomes: []StageOutcome{
   788  				{
   789  					Entity: entityAuctionRequest,
   790  					Stage:  hooks.StageProcessedAuctionRequest.String(),
   791  					Groups: []GroupOutcome{
   792  						{
   793  							InvocationResults: []HookOutcome{
   794  								{
   795  									AnalyticsTags: hookanalytics.Analytics{},
   796  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
   797  									Status:        StatusTimeout,
   798  									Action:        "",
   799  									Message:       "",
   800  									DebugMessages: nil,
   801  									Errors:        []string{"Hook execution timeout"},
   802  									Warnings:      nil,
   803  								},
   804  							},
   805  						},
   806  						{
   807  							InvocationResults: []HookOutcome{
   808  								{
   809  									AnalyticsTags: hookanalytics.Analytics{},
   810  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
   811  									Status:        StatusSuccess,
   812  									Action:        ActionUpdate,
   813  									Message:       "",
   814  									DebugMessages: []string{
   815  										fmt.Sprintf("Hook mutation successfully applied, affected key: bidRequest.user.yob, mutation type: %s", hookstage.MutationUpdate),
   816  										fmt.Sprintf("Hook mutation successfully applied, affected key: bidRequest.user.consent, mutation type: %s", hookstage.MutationUpdate),
   817  									},
   818  									Errors:   nil,
   819  									Warnings: nil,
   820  								},
   821  							},
   822  						},
   823  					},
   824  				},
   825  			},
   826  		},
   827  		{
   828  			description:      "Modules contexts are preserved and correct",
   829  			givenPlanBuilder: TestWithModuleContextsPlanBuilder{},
   830  			givenRequest:     openrtb_ext.RequestWrapper{BidRequest: &req},
   831  			expectedRequest:  req,
   832  			expectedErr:      nil,
   833  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{
   834  				"module-1": {"processed-auction-ctx-1": "some-ctx-1", "processed-auction-ctx-3": "some-ctx-3"},
   835  				"module-2": {"processed-auction-ctx-2": "some-ctx-2"},
   836  			}},
   837  			expectedStageOutcomes: []StageOutcome{
   838  				{
   839  					Entity: entityAuctionRequest,
   840  					Stage:  hooks.StageProcessedAuctionRequest.String(),
   841  					Groups: []GroupOutcome{
   842  						{
   843  							InvocationResults: []HookOutcome{
   844  								{
   845  									AnalyticsTags: hookanalytics.Analytics{},
   846  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "foo"},
   847  									Status:        StatusSuccess,
   848  									Action:        ActionNone,
   849  									Message:       "",
   850  									DebugMessages: nil,
   851  									Errors:        nil,
   852  									Warnings:      nil,
   853  								},
   854  							},
   855  						},
   856  						{
   857  							InvocationResults: []HookOutcome{
   858  								{
   859  									AnalyticsTags: hookanalytics.Analytics{},
   860  									HookID:        HookID{ModuleCode: "module-2", HookImplCode: "bar"},
   861  									Status:        StatusSuccess,
   862  									Action:        ActionNone,
   863  									Message:       "",
   864  									DebugMessages: nil,
   865  									Errors:        nil,
   866  									Warnings:      nil,
   867  								},
   868  								{
   869  									AnalyticsTags: hookanalytics.Analytics{},
   870  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "baz"},
   871  									Status:        StatusSuccess,
   872  									Action:        ActionNone,
   873  									Message:       "",
   874  									DebugMessages: nil,
   875  									Errors:        nil,
   876  									Warnings:      nil,
   877  								},
   878  							},
   879  						},
   880  					},
   881  				},
   882  			},
   883  		},
   884  	}
   885  
   886  	for _, test := range testCases {
   887  		t.Run(test.description, func(ti *testing.T) {
   888  			exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{})
   889  
   890  			privacyConfig := getModuleActivities("foo", false, false)
   891  			ac := privacy.NewActivityControl(privacyConfig)
   892  			exec.SetActivityControl(ac)
   893  
   894  			err := exec.ExecuteProcessedAuctionStage(&test.givenRequest)
   895  
   896  			assert.Equal(ti, test.expectedErr, err, "Unexpected stage reject.")
   897  			assert.Equal(ti, test.expectedRequest, *test.givenRequest.BidRequest, "Incorrect request update.")
   898  			assert.Equal(ti, test.expectedModuleContexts, exec.moduleContexts, "Incorrect module contexts")
   899  
   900  			stageOutcomes := exec.GetOutcomes()
   901  			if len(test.expectedStageOutcomes) == 0 {
   902  				assert.Empty(ti, stageOutcomes, "Incorrect stage outcomes.")
   903  			} else {
   904  				assertEqualStageOutcomes(ti, test.expectedStageOutcomes[0], stageOutcomes[0])
   905  			}
   906  		})
   907  	}
   908  }
   909  
   910  func TestExecuteBidderRequestStage(t *testing.T) {
   911  	bidderName := "the-bidder"
   912  	foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}}
   913  
   914  	expectedBidderRequest := &openrtb2.BidRequest{ID: "some-id", User: &openrtb2.User{ID: "user-id"}}
   915  	expectedUpdatedBidderRequest := &openrtb2.BidRequest{
   916  		ID: "some-id",
   917  		User: &openrtb2.User{
   918  			ID:      "user-id",
   919  			Yob:     2000,
   920  			Consent: "true",
   921  		},
   922  	}
   923  
   924  	testCases := []struct {
   925  		description            string
   926  		givenBidderRequest     *openrtb2.BidRequest
   927  		givenPlanBuilder       hooks.ExecutionPlanBuilder
   928  		expectedBidderRequest  *openrtb2.BidRequest
   929  		expectedReject         *RejectError
   930  		expectedModuleContexts *moduleContexts
   931  		expectedStageOutcomes  []StageOutcome
   932  		privacyConfig          *config.AccountPrivacy
   933  	}{
   934  		{
   935  			description:            "Payload not changed if hook execution plan empty",
   936  			givenBidderRequest:     &openrtb2.BidRequest{ID: "some-id", User: &openrtb2.User{ID: "user-id"}},
   937  			givenPlanBuilder:       hooks.EmptyPlanBuilder{},
   938  			expectedBidderRequest:  expectedBidderRequest,
   939  			expectedReject:         nil,
   940  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}},
   941  			expectedStageOutcomes:  []StageOutcome{},
   942  		},
   943  		{
   944  			description:            "Payload changed if hooks return mutations",
   945  			givenBidderRequest:     &openrtb2.BidRequest{ID: "some-id", User: &openrtb2.User{ID: "user-id"}},
   946  			givenPlanBuilder:       TestApplyHookMutationsBuilder{},
   947  			expectedBidderRequest:  expectedUpdatedBidderRequest,
   948  			expectedReject:         nil,
   949  			expectedModuleContexts: foobarModuleCtx,
   950  			expectedStageOutcomes: []StageOutcome{
   951  				{
   952  					Entity: entity(bidderName),
   953  					Stage:  hooks.StageBidderRequest.String(),
   954  					Groups: []GroupOutcome{
   955  						{
   956  							InvocationResults: []HookOutcome{
   957  								{
   958  									AnalyticsTags: hookanalytics.Analytics{},
   959  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
   960  									Status:        StatusSuccess,
   961  									Action:        ActionUpdate,
   962  									Message:       "",
   963  									DebugMessages: []string{
   964  										fmt.Sprintf("Hook mutation successfully applied, affected key: bidRequest.user.yob, mutation type: %s", hookstage.MutationUpdate),
   965  										fmt.Sprintf("Hook mutation successfully applied, affected key: bidRequest.user.consent, mutation type: %s", hookstage.MutationUpdate),
   966  									},
   967  									Errors:   nil,
   968  									Warnings: nil,
   969  								},
   970  								{
   971  									AnalyticsTags: hookanalytics.Analytics{},
   972  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
   973  									Status:        StatusExecutionFailure,
   974  									Action:        ActionUpdate,
   975  									Message:       "",
   976  									DebugMessages: nil,
   977  									Errors:        nil,
   978  									Warnings:      []string{"failed to apply hook mutation: key not found"},
   979  								},
   980  							},
   981  						},
   982  						{
   983  							InvocationResults: []HookOutcome{
   984  								{
   985  									AnalyticsTags: hookanalytics.Analytics{},
   986  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "baz"},
   987  									Status:        StatusFailure,
   988  									Action:        "",
   989  									Message:       "",
   990  									DebugMessages: nil,
   991  									Errors:        []string{"hook execution failed: attribute not found"},
   992  									Warnings:      nil,
   993  								},
   994  							},
   995  						},
   996  					},
   997  				},
   998  			},
   999  		},
  1000  		{
  1001  			description:            "Stage execution can be rejected - and later hooks rejected",
  1002  			givenBidderRequest:     &openrtb2.BidRequest{ID: "some-id", User: &openrtb2.User{ID: "user-id"}},
  1003  			givenPlanBuilder:       TestRejectPlanBuilder{},
  1004  			expectedBidderRequest:  expectedBidderRequest,
  1005  			expectedReject:         &RejectError{0, HookID{ModuleCode: "foobar", HookImplCode: "foo"}, hooks.StageBidderRequest.String()},
  1006  			expectedModuleContexts: foobarModuleCtx,
  1007  			expectedStageOutcomes: []StageOutcome{
  1008  				{
  1009  					ExecutionTime: ExecutionTime{},
  1010  					Entity:        entity(bidderName),
  1011  					Stage:         hooks.StageBidderRequest.String(),
  1012  					Groups: []GroupOutcome{
  1013  						{
  1014  							ExecutionTime: ExecutionTime{},
  1015  							InvocationResults: []HookOutcome{
  1016  								{
  1017  									ExecutionTime: ExecutionTime{},
  1018  									AnalyticsTags: hookanalytics.Analytics{},
  1019  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "baz"},
  1020  									Status:        StatusExecutionFailure,
  1021  									Action:        "",
  1022  									Message:       "",
  1023  									DebugMessages: nil,
  1024  									Errors:        []string{"unexpected error"},
  1025  									Warnings:      nil,
  1026  								},
  1027  							},
  1028  						},
  1029  						{
  1030  							ExecutionTime: ExecutionTime{},
  1031  							InvocationResults: []HookOutcome{
  1032  								{
  1033  									ExecutionTime: ExecutionTime{},
  1034  									AnalyticsTags: hookanalytics.Analytics{},
  1035  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
  1036  									Status:        StatusSuccess,
  1037  									Action:        ActionReject,
  1038  									Message:       "",
  1039  									DebugMessages: nil,
  1040  									Errors: []string{
  1041  										`Module foobar (hook: foo) rejected request with code 0 at bidder_request stage`,
  1042  									},
  1043  									Warnings: nil,
  1044  								},
  1045  							},
  1046  						},
  1047  					},
  1048  				},
  1049  			},
  1050  		},
  1051  		{
  1052  			description:            "Stage execution can be timed out",
  1053  			givenBidderRequest:     &openrtb2.BidRequest{ID: "some-id", User: &openrtb2.User{ID: "user-id"}},
  1054  			givenPlanBuilder:       TestWithTimeoutPlanBuilder{},
  1055  			expectedBidderRequest:  expectedUpdatedBidderRequest,
  1056  			expectedReject:         nil,
  1057  			expectedModuleContexts: foobarModuleCtx,
  1058  			expectedStageOutcomes: []StageOutcome{
  1059  				{
  1060  					ExecutionTime: ExecutionTime{},
  1061  					Entity:        entity(bidderName),
  1062  					Stage:         hooks.StageBidderRequest.String(),
  1063  					Groups: []GroupOutcome{
  1064  						{
  1065  							ExecutionTime: ExecutionTime{},
  1066  							InvocationResults: []HookOutcome{
  1067  								{
  1068  									ExecutionTime: ExecutionTime{},
  1069  									AnalyticsTags: hookanalytics.Analytics{},
  1070  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
  1071  									Status:        StatusTimeout,
  1072  									Action:        "",
  1073  									Message:       "",
  1074  									DebugMessages: nil,
  1075  									Errors:        []string{"Hook execution timeout"},
  1076  									Warnings:      nil,
  1077  								},
  1078  							},
  1079  						},
  1080  						{
  1081  							ExecutionTime: ExecutionTime{},
  1082  							InvocationResults: []HookOutcome{
  1083  								{
  1084  									AnalyticsTags: hookanalytics.Analytics{},
  1085  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
  1086  									Status:        StatusSuccess,
  1087  									Action:        ActionUpdate,
  1088  									Message:       "",
  1089  									DebugMessages: []string{
  1090  										fmt.Sprintf("Hook mutation successfully applied, affected key: bidRequest.user.yob, mutation type: %s", hookstage.MutationUpdate),
  1091  										fmt.Sprintf("Hook mutation successfully applied, affected key: bidRequest.user.consent, mutation type: %s", hookstage.MutationUpdate),
  1092  									},
  1093  									Errors:   nil,
  1094  									Warnings: nil,
  1095  								},
  1096  							},
  1097  						},
  1098  					},
  1099  				},
  1100  			},
  1101  		},
  1102  		{
  1103  			description:           "Modules contexts are preserved and correct",
  1104  			givenBidderRequest:    &openrtb2.BidRequest{ID: "some-id", User: &openrtb2.User{ID: "user-id"}},
  1105  			givenPlanBuilder:      TestWithModuleContextsPlanBuilder{},
  1106  			expectedBidderRequest: expectedBidderRequest,
  1107  			expectedReject:        nil,
  1108  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{
  1109  				"module-1": {"bidder-request-ctx-1": "some-ctx-1"},
  1110  				"module-2": {"bidder-request-ctx-2": "some-ctx-2"},
  1111  			}},
  1112  			expectedStageOutcomes: []StageOutcome{
  1113  				{
  1114  					ExecutionTime: ExecutionTime{},
  1115  					Entity:        entity(bidderName),
  1116  					Stage:         hooks.StageBidderRequest.String(),
  1117  					Groups: []GroupOutcome{
  1118  						{
  1119  							ExecutionTime: ExecutionTime{},
  1120  							InvocationResults: []HookOutcome{
  1121  								{
  1122  									ExecutionTime: ExecutionTime{},
  1123  									AnalyticsTags: hookanalytics.Analytics{},
  1124  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "foo"},
  1125  									Status:        StatusSuccess,
  1126  									Action:        ActionNone,
  1127  									Message:       "",
  1128  									DebugMessages: nil,
  1129  									Errors:        nil,
  1130  									Warnings:      nil,
  1131  								},
  1132  							},
  1133  						},
  1134  						{
  1135  							ExecutionTime: ExecutionTime{},
  1136  							InvocationResults: []HookOutcome{
  1137  								{
  1138  									ExecutionTime: ExecutionTime{},
  1139  									AnalyticsTags: hookanalytics.Analytics{},
  1140  									HookID:        HookID{ModuleCode: "module-2", HookImplCode: "bar"},
  1141  									Status:        StatusSuccess,
  1142  									Action:        ActionNone,
  1143  									Message:       "",
  1144  									DebugMessages: nil,
  1145  									Errors:        nil,
  1146  									Warnings:      nil,
  1147  								},
  1148  							},
  1149  						},
  1150  					},
  1151  				},
  1152  			},
  1153  		},
  1154  	}
  1155  
  1156  	for _, test := range testCases {
  1157  		t.Run(test.description, func(t *testing.T) {
  1158  			exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{})
  1159  			privacyConfig := getModuleActivities("foo", false, false)
  1160  			ac := privacy.NewActivityControl(privacyConfig)
  1161  			exec.SetActivityControl(ac)
  1162  
  1163  			reject := exec.ExecuteBidderRequestStage(&openrtb_ext.RequestWrapper{BidRequest: test.givenBidderRequest}, bidderName)
  1164  
  1165  			assert.Equal(t, test.expectedReject, reject, "Unexpected stage reject.")
  1166  			assert.Equal(t, test.expectedBidderRequest, test.givenBidderRequest, "Incorrect bidder request.")
  1167  			assert.Equal(t, test.expectedModuleContexts, exec.moduleContexts, "Incorrect module contexts")
  1168  
  1169  			stageOutcomes := exec.GetOutcomes()
  1170  			if len(test.expectedStageOutcomes) == 0 {
  1171  				assert.Empty(t, stageOutcomes, "Incorrect stage outcomes.")
  1172  			} else {
  1173  				assertEqualStageOutcomes(t, test.expectedStageOutcomes[0], stageOutcomes[0])
  1174  			}
  1175  		})
  1176  	}
  1177  }
  1178  
  1179  func getModuleActivities(componentName string, allowTransmitUserFPD, allowTransmitPreciseGeo bool) *config.AccountPrivacy {
  1180  	return &config.AccountPrivacy{
  1181  		AllowActivities: &config.AllowActivities{
  1182  			TransmitUserFPD:    buildDefaultActivityConfig(componentName, allowTransmitUserFPD),
  1183  			TransmitPreciseGeo: buildDefaultActivityConfig(componentName, allowTransmitPreciseGeo),
  1184  		},
  1185  	}
  1186  }
  1187  
  1188  func getTransmitUFPDActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
  1189  	return &config.AccountPrivacy{
  1190  		AllowActivities: &config.AllowActivities{
  1191  			TransmitUserFPD: buildDefaultActivityConfig(componentName, allow),
  1192  		},
  1193  	}
  1194  }
  1195  
  1196  func getTransmitPreciseGeoActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
  1197  	return &config.AccountPrivacy{
  1198  		AllowActivities: &config.AllowActivities{
  1199  			TransmitPreciseGeo: buildDefaultActivityConfig(componentName, allow),
  1200  		},
  1201  	}
  1202  }
  1203  
  1204  func buildDefaultActivityConfig(componentName string, allow bool) config.Activity {
  1205  	return config.Activity{
  1206  		Default: ptrutil.ToPtr(true),
  1207  		Rules: []config.ActivityRule{
  1208  			{
  1209  				Allow: allow,
  1210  				Condition: config.ActivityCondition{
  1211  					ComponentName: []string{componentName},
  1212  					ComponentType: []string{"general"},
  1213  				},
  1214  			},
  1215  		},
  1216  	}
  1217  }
  1218  
  1219  func TestExecuteRawBidderResponseStage(t *testing.T) {
  1220  	foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}}
  1221  	resp := adapters.BidderResponse{Bids: []*adapters.TypedBid{{DealPriority: 1}}}
  1222  	expResp := adapters.BidderResponse{Bids: []*adapters.TypedBid{{DealPriority: 10}}}
  1223  	vEntity := entity("the-bidder")
  1224  
  1225  	testCases := []struct {
  1226  		description            string
  1227  		givenPlanBuilder       hooks.ExecutionPlanBuilder
  1228  		givenBidderResponse    adapters.BidderResponse
  1229  		expectedBidderResponse adapters.BidderResponse
  1230  		expectedReject         *RejectError
  1231  		expectedModuleContexts *moduleContexts
  1232  		expectedStageOutcomes  []StageOutcome
  1233  	}{
  1234  		{
  1235  			description:            "Payload not changed if hook execution plan empty",
  1236  			givenPlanBuilder:       hooks.EmptyPlanBuilder{},
  1237  			givenBidderResponse:    resp,
  1238  			expectedBidderResponse: resp,
  1239  			expectedReject:         nil,
  1240  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}},
  1241  			expectedStageOutcomes:  []StageOutcome{},
  1242  		},
  1243  		{
  1244  			description:            "Payload changed if hooks return mutations",
  1245  			givenPlanBuilder:       TestApplyHookMutationsBuilder{},
  1246  			givenBidderResponse:    resp,
  1247  			expectedBidderResponse: expResp,
  1248  			expectedReject:         nil,
  1249  			expectedModuleContexts: foobarModuleCtx,
  1250  			expectedStageOutcomes: []StageOutcome{
  1251  				{
  1252  					Entity: vEntity,
  1253  					Stage:  hooks.StageRawBidderResponse.String(),
  1254  					Groups: []GroupOutcome{
  1255  						{
  1256  							InvocationResults: []HookOutcome{
  1257  								{
  1258  									AnalyticsTags: hookanalytics.Analytics{},
  1259  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
  1260  									Status:        StatusSuccess,
  1261  									Action:        ActionUpdate,
  1262  									Message:       "",
  1263  									DebugMessages: []string{
  1264  										fmt.Sprintf("Hook mutation successfully applied, affected key: bidderResponse.bid.deal-priority, mutation type: %s", hookstage.MutationUpdate),
  1265  									},
  1266  									Errors:   nil,
  1267  									Warnings: nil,
  1268  								},
  1269  							},
  1270  						},
  1271  					},
  1272  				},
  1273  			},
  1274  		},
  1275  		{
  1276  			description:            "Stage execution can be rejected",
  1277  			givenPlanBuilder:       TestRejectPlanBuilder{},
  1278  			givenBidderResponse:    resp,
  1279  			expectedBidderResponse: resp,
  1280  			expectedReject:         &RejectError{0, HookID{ModuleCode: "foobar", HookImplCode: "foo"}, hooks.StageRawBidderResponse.String()},
  1281  			expectedModuleContexts: foobarModuleCtx,
  1282  			expectedStageOutcomes: []StageOutcome{
  1283  				{
  1284  					Entity: vEntity,
  1285  					Stage:  hooks.StageRawBidderResponse.String(),
  1286  					Groups: []GroupOutcome{
  1287  						{
  1288  							InvocationResults: []HookOutcome{
  1289  								{
  1290  									AnalyticsTags: hookanalytics.Analytics{},
  1291  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
  1292  									Status:        StatusSuccess,
  1293  									Action:        ActionReject,
  1294  									Message:       "",
  1295  									DebugMessages: nil,
  1296  									Errors: []string{
  1297  										`Module foobar (hook: foo) rejected request with code 0 at raw_bidder_response stage`,
  1298  									},
  1299  									Warnings: nil,
  1300  								},
  1301  							},
  1302  						},
  1303  					},
  1304  				},
  1305  			},
  1306  		},
  1307  		{
  1308  			description:            "Response can be changed when a hook times out",
  1309  			givenPlanBuilder:       TestWithTimeoutPlanBuilder{},
  1310  			givenBidderResponse:    resp,
  1311  			expectedBidderResponse: expResp,
  1312  			expectedReject:         nil,
  1313  			expectedModuleContexts: foobarModuleCtx,
  1314  			expectedStageOutcomes: []StageOutcome{
  1315  				{
  1316  					Entity: vEntity,
  1317  					Stage:  hooks.StageRawBidderResponse.String(),
  1318  					Groups: []GroupOutcome{
  1319  						{
  1320  							InvocationResults: []HookOutcome{
  1321  								{
  1322  									AnalyticsTags: hookanalytics.Analytics{},
  1323  									HookID:        HookID{"foobar", "foo"},
  1324  									Status:        StatusTimeout,
  1325  									Action:        "",
  1326  									Message:       "",
  1327  									DebugMessages: nil,
  1328  									Errors:        []string{"Hook execution timeout"},
  1329  									Warnings:      nil,
  1330  								},
  1331  							},
  1332  						},
  1333  						{
  1334  							InvocationResults: []HookOutcome{
  1335  								{
  1336  									AnalyticsTags: hookanalytics.Analytics{},
  1337  									HookID:        HookID{"foobar", "bar"},
  1338  									Status:        StatusSuccess,
  1339  									Action:        ActionUpdate,
  1340  									Message:       "",
  1341  									DebugMessages: []string{
  1342  										fmt.Sprintf("Hook mutation successfully applied, affected key: bidderResponse.bid.deal-priority, mutation type: %s", hookstage.MutationUpdate),
  1343  									},
  1344  									Errors:   nil,
  1345  									Warnings: nil,
  1346  								},
  1347  							},
  1348  						},
  1349  					},
  1350  				},
  1351  			},
  1352  		},
  1353  		{
  1354  			description:            "Modules contexts are preserved and correct",
  1355  			givenPlanBuilder:       TestWithModuleContextsPlanBuilder{},
  1356  			givenBidderResponse:    resp,
  1357  			expectedBidderResponse: expResp,
  1358  			expectedReject:         nil,
  1359  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{
  1360  				"module-1": {"raw-bidder-response-ctx-1": "some-ctx-1", "raw-bidder-response-ctx-3": "some-ctx-3"},
  1361  				"module-2": {"raw-bidder-response-ctx-2": "some-ctx-2"},
  1362  			}},
  1363  			expectedStageOutcomes: []StageOutcome{
  1364  				{
  1365  					Entity: vEntity,
  1366  					Stage:  hooks.StageRawBidderResponse.String(),
  1367  					Groups: []GroupOutcome{
  1368  						{
  1369  							InvocationResults: []HookOutcome{
  1370  								{
  1371  									AnalyticsTags: hookanalytics.Analytics{},
  1372  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "foo"},
  1373  									Status:        StatusSuccess,
  1374  									Action:        ActionNone,
  1375  									Message:       "",
  1376  									DebugMessages: nil,
  1377  									Errors:        nil,
  1378  									Warnings:      nil,
  1379  								},
  1380  								{
  1381  									AnalyticsTags: hookanalytics.Analytics{},
  1382  									HookID:        HookID{ModuleCode: "module-2", HookImplCode: "baz"},
  1383  									Status:        StatusSuccess,
  1384  									Action:        ActionNone,
  1385  									Message:       "",
  1386  									DebugMessages: nil,
  1387  									Errors:        nil,
  1388  									Warnings:      nil,
  1389  								},
  1390  							},
  1391  						},
  1392  						{
  1393  							InvocationResults: []HookOutcome{
  1394  								{
  1395  									AnalyticsTags: hookanalytics.Analytics{},
  1396  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "bar"},
  1397  									Status:        StatusSuccess,
  1398  									Action:        ActionNone,
  1399  									Message:       "",
  1400  									DebugMessages: nil,
  1401  									Errors:        nil,
  1402  									Warnings:      nil,
  1403  								},
  1404  							},
  1405  						},
  1406  					},
  1407  				},
  1408  			},
  1409  		},
  1410  	}
  1411  
  1412  	for _, test := range testCases {
  1413  		t.Run(test.description, func(ti *testing.T) {
  1414  			exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{})
  1415  
  1416  			privacyConfig := getModuleActivities("foo", false, false)
  1417  			ac := privacy.NewActivityControl(privacyConfig)
  1418  			exec.SetActivityControl(ac)
  1419  
  1420  			reject := exec.ExecuteRawBidderResponseStage(&test.givenBidderResponse, "the-bidder")
  1421  
  1422  			assert.Equal(ti, test.expectedReject, reject, "Unexpected stage reject.")
  1423  			assert.Equal(ti, test.expectedBidderResponse, test.givenBidderResponse, "Incorrect response update.")
  1424  			assert.Equal(ti, test.expectedModuleContexts, exec.moduleContexts, "Incorrect module contexts")
  1425  
  1426  			stageOutcomes := exec.GetOutcomes()
  1427  			if len(test.expectedStageOutcomes) == 0 {
  1428  				assert.Empty(ti, stageOutcomes, "Incorrect stage outcomes.")
  1429  			} else {
  1430  				assertEqualStageOutcomes(ti, test.expectedStageOutcomes[0], stageOutcomes[0])
  1431  			}
  1432  		})
  1433  	}
  1434  }
  1435  
  1436  func TestExecuteAllProcessedBidResponsesStage(t *testing.T) {
  1437  	foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}}
  1438  
  1439  	expectedAllProcBidResponses := map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
  1440  		"some-bidder": {Bids: []*entities.PbsOrtbBid{{DealPriority: 1}}},
  1441  	}
  1442  	expectedUpdatedAllProcBidResponses := map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
  1443  		"some-bidder": {Bids: []*entities.PbsOrtbBid{{DealPriority: 10}}},
  1444  	}
  1445  
  1446  	testCases := []struct {
  1447  		description             string
  1448  		givenBiddersResponse    map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
  1449  		givenPlanBuilder        hooks.ExecutionPlanBuilder
  1450  		expectedBiddersResponse map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
  1451  		expectedReject          *RejectError
  1452  		expectedModuleContexts  *moduleContexts
  1453  		expectedStageOutcomes   []StageOutcome
  1454  	}{
  1455  		{
  1456  			description: "Payload not changed if hook execution plan empty",
  1457  			givenBiddersResponse: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
  1458  				"some-bidder": {Bids: []*entities.PbsOrtbBid{{DealPriority: 1}}},
  1459  			},
  1460  			givenPlanBuilder:        hooks.EmptyPlanBuilder{},
  1461  			expectedBiddersResponse: expectedAllProcBidResponses,
  1462  			expectedReject:          nil,
  1463  			expectedModuleContexts:  &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}},
  1464  			expectedStageOutcomes:   []StageOutcome{},
  1465  		},
  1466  		{
  1467  			description: "Payload changed if hooks return mutations",
  1468  			givenBiddersResponse: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
  1469  				"some-bidder": {Bids: []*entities.PbsOrtbBid{{DealPriority: 1}}},
  1470  			},
  1471  			givenPlanBuilder:        TestApplyHookMutationsBuilder{},
  1472  			expectedBiddersResponse: expectedUpdatedAllProcBidResponses,
  1473  			expectedReject:          nil,
  1474  			expectedModuleContexts:  foobarModuleCtx,
  1475  			expectedStageOutcomes: []StageOutcome{
  1476  				{
  1477  					Entity: entityAllProcessedBidResponses,
  1478  					Stage:  hooks.StageAllProcessedBidResponses.String(),
  1479  					Groups: []GroupOutcome{
  1480  						{
  1481  							InvocationResults: []HookOutcome{
  1482  								{
  1483  									AnalyticsTags: hookanalytics.Analytics{},
  1484  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
  1485  									Status:        StatusSuccess,
  1486  									Action:        ActionUpdate,
  1487  									Message:       "",
  1488  									DebugMessages: []string{
  1489  										fmt.Sprintf("Hook mutation successfully applied, affected key: processedBidderResponse.bid.deal-priority, mutation type: %s", hookstage.MutationUpdate),
  1490  									},
  1491  									Errors:   nil,
  1492  									Warnings: nil,
  1493  								},
  1494  								{
  1495  									AnalyticsTags: hookanalytics.Analytics{},
  1496  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
  1497  									Status:        StatusExecutionFailure,
  1498  									Action:        ActionUpdate,
  1499  									Message:       "",
  1500  									DebugMessages: nil,
  1501  									Errors:        nil,
  1502  									Warnings:      []string{"failed to apply hook mutation: key not found"},
  1503  								},
  1504  							},
  1505  						},
  1506  						{
  1507  							InvocationResults: []HookOutcome{
  1508  								{
  1509  									AnalyticsTags: hookanalytics.Analytics{},
  1510  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "baz"},
  1511  									Status:        StatusFailure,
  1512  									Action:        "",
  1513  									Message:       "",
  1514  									DebugMessages: nil,
  1515  									Errors:        []string{"hook execution failed: attribute not found"},
  1516  									Warnings:      nil,
  1517  								},
  1518  							},
  1519  						},
  1520  					},
  1521  				},
  1522  			},
  1523  		},
  1524  		{
  1525  			description: "Stage execution can't be rejected - stage doesn't support rejection",
  1526  			givenBiddersResponse: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
  1527  				"some-bidder": {Bids: []*entities.PbsOrtbBid{{DealPriority: 1}}},
  1528  			},
  1529  			givenPlanBuilder:        TestRejectPlanBuilder{},
  1530  			expectedBiddersResponse: expectedUpdatedAllProcBidResponses,
  1531  			expectedReject:          &RejectError{0, HookID{ModuleCode: "foobar", HookImplCode: "foo"}, hooks.StageAllProcessedBidResponses.String()},
  1532  			expectedModuleContexts:  foobarModuleCtx,
  1533  			expectedStageOutcomes: []StageOutcome{
  1534  				{
  1535  					Entity: entityAllProcessedBidResponses,
  1536  					Stage:  hooks.StageAllProcessedBidResponses.String(),
  1537  					Groups: []GroupOutcome{
  1538  						{
  1539  							InvocationResults: []HookOutcome{
  1540  								{
  1541  									AnalyticsTags: hookanalytics.Analytics{},
  1542  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "baz"},
  1543  									Status:        StatusExecutionFailure,
  1544  									Action:        "",
  1545  									Message:       "",
  1546  									DebugMessages: nil,
  1547  									Errors:        []string{"unexpected error"},
  1548  									Warnings:      nil,
  1549  								},
  1550  							},
  1551  						},
  1552  						{
  1553  							InvocationResults: []HookOutcome{
  1554  								{
  1555  									AnalyticsTags: hookanalytics.Analytics{},
  1556  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
  1557  									Status:        StatusExecutionFailure,
  1558  									Action:        "",
  1559  									Message:       "",
  1560  									DebugMessages: nil,
  1561  									Errors: []string{
  1562  										fmt.Sprintf("Module (name: foobar, hook code: foo) tried to reject request on the %s stage that does not support rejection", hooks.StageAllProcessedBidResponses),
  1563  									},
  1564  									Warnings: nil,
  1565  								},
  1566  							},
  1567  						},
  1568  						{
  1569  							InvocationResults: []HookOutcome{
  1570  								{
  1571  									AnalyticsTags: hookanalytics.Analytics{},
  1572  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
  1573  									Status:        StatusSuccess,
  1574  									Action:        ActionUpdate,
  1575  									Message:       "",
  1576  									DebugMessages: []string{
  1577  										fmt.Sprintf("Hook mutation successfully applied, affected key: processedBidderResponse.bid.deal-priority, mutation type: %s", hookstage.MutationUpdate),
  1578  									},
  1579  									Errors:   nil,
  1580  									Warnings: nil,
  1581  								},
  1582  							},
  1583  						},
  1584  					},
  1585  				},
  1586  			},
  1587  		},
  1588  		{
  1589  			description: "Stage execution can be timed out",
  1590  			givenBiddersResponse: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
  1591  				"some-bidder": {Bids: []*entities.PbsOrtbBid{{DealPriority: 1}}},
  1592  			},
  1593  			givenPlanBuilder:        TestWithTimeoutPlanBuilder{},
  1594  			expectedBiddersResponse: expectedUpdatedAllProcBidResponses,
  1595  			expectedReject:          nil,
  1596  			expectedModuleContexts:  foobarModuleCtx,
  1597  			expectedStageOutcomes: []StageOutcome{
  1598  				{
  1599  					Entity: entityAllProcessedBidResponses,
  1600  					Stage:  hooks.StageAllProcessedBidResponses.String(),
  1601  					Groups: []GroupOutcome{
  1602  						{
  1603  							InvocationResults: []HookOutcome{
  1604  								{
  1605  									AnalyticsTags: hookanalytics.Analytics{},
  1606  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
  1607  									Status:        StatusTimeout,
  1608  									Action:        "",
  1609  									Message:       "",
  1610  									DebugMessages: nil,
  1611  									Errors:        []string{"Hook execution timeout"},
  1612  									Warnings:      nil,
  1613  								},
  1614  							},
  1615  						},
  1616  						{
  1617  							InvocationResults: []HookOutcome{
  1618  								{
  1619  									AnalyticsTags: hookanalytics.Analytics{},
  1620  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
  1621  									Status:        StatusSuccess,
  1622  									Action:        ActionUpdate,
  1623  									Message:       "",
  1624  									DebugMessages: []string{
  1625  										fmt.Sprintf("Hook mutation successfully applied, affected key: processedBidderResponse.bid.deal-priority, mutation type: %s", hookstage.MutationUpdate),
  1626  									},
  1627  									Errors:   nil,
  1628  									Warnings: nil,
  1629  								},
  1630  							},
  1631  						},
  1632  					},
  1633  				},
  1634  			},
  1635  		},
  1636  		{
  1637  			description: "Modules contexts are preserved and correct",
  1638  			givenBiddersResponse: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
  1639  				"some-bidder": {Bids: []*entities.PbsOrtbBid{{DealPriority: 1}}},
  1640  			},
  1641  			givenPlanBuilder:        TestWithModuleContextsPlanBuilder{},
  1642  			expectedBiddersResponse: expectedAllProcBidResponses,
  1643  			expectedReject:          nil,
  1644  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{
  1645  				"module-1": {"all-processed-bid-responses-ctx-1": "some-ctx-1"},
  1646  				"module-2": {"all-processed-bid-responses-ctx-2": "some-ctx-2"},
  1647  			}},
  1648  			expectedStageOutcomes: []StageOutcome{
  1649  				{
  1650  					Entity: entityAllProcessedBidResponses,
  1651  					Stage:  hooks.StageAllProcessedBidResponses.String(),
  1652  					Groups: []GroupOutcome{
  1653  						{
  1654  							InvocationResults: []HookOutcome{
  1655  								{
  1656  									AnalyticsTags: hookanalytics.Analytics{},
  1657  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "foo"},
  1658  									Status:        StatusSuccess,
  1659  									Action:        ActionNone,
  1660  									Message:       "",
  1661  									DebugMessages: nil,
  1662  									Errors:        nil,
  1663  									Warnings:      nil,
  1664  								},
  1665  							},
  1666  						},
  1667  						{
  1668  							InvocationResults: []HookOutcome{
  1669  								{
  1670  									AnalyticsTags: hookanalytics.Analytics{},
  1671  									HookID:        HookID{ModuleCode: "module-2", HookImplCode: "bar"},
  1672  									Status:        StatusSuccess,
  1673  									Action:        ActionNone,
  1674  									Message:       "",
  1675  									DebugMessages: nil,
  1676  									Errors:        nil,
  1677  									Warnings:      nil,
  1678  								},
  1679  							},
  1680  						},
  1681  					},
  1682  				},
  1683  			},
  1684  		},
  1685  	}
  1686  
  1687  	for _, test := range testCases {
  1688  		t.Run(test.description, func(t *testing.T) {
  1689  			exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{})
  1690  
  1691  			privacyConfig := getModuleActivities("foo", false, false)
  1692  			ac := privacy.NewActivityControl(privacyConfig)
  1693  			exec.SetActivityControl(ac)
  1694  
  1695  			exec.ExecuteAllProcessedBidResponsesStage(test.givenBiddersResponse)
  1696  
  1697  			assert.Equal(t, test.expectedBiddersResponse, test.givenBiddersResponse, "Incorrect bidders response.")
  1698  			assert.Equal(t, test.expectedModuleContexts, exec.moduleContexts, "Incorrect module contexts")
  1699  
  1700  			stageOutcomes := exec.GetOutcomes()
  1701  			if len(test.expectedStageOutcomes) == 0 {
  1702  				assert.Empty(t, stageOutcomes, "Incorrect stage outcomes.")
  1703  			} else {
  1704  				assertEqualStageOutcomes(t, test.expectedStageOutcomes[0], stageOutcomes[0])
  1705  			}
  1706  		})
  1707  	}
  1708  }
  1709  
  1710  func TestExecuteAuctionResponseStage(t *testing.T) {
  1711  	foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}}
  1712  	resp := &openrtb2.BidResponse{CustomData: "some-custom-data"}
  1713  	expResp := &openrtb2.BidResponse{CustomData: "new-custom-data"}
  1714  
  1715  	testCases := []struct {
  1716  		description            string
  1717  		givenPlanBuilder       hooks.ExecutionPlanBuilder
  1718  		givenResponse          *openrtb2.BidResponse
  1719  		expectedResponse       *openrtb2.BidResponse
  1720  		expectedReject         *RejectError
  1721  		expectedModuleContexts *moduleContexts
  1722  		expectedStageOutcomes  []StageOutcome
  1723  	}{
  1724  		{
  1725  			description:            "Payload not changed if hook execution plan empty",
  1726  			givenPlanBuilder:       hooks.EmptyPlanBuilder{},
  1727  			givenResponse:          resp,
  1728  			expectedResponse:       resp,
  1729  			expectedReject:         nil,
  1730  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}},
  1731  			expectedStageOutcomes:  []StageOutcome{},
  1732  		},
  1733  		{
  1734  			description:            "Payload changed if hooks return mutations",
  1735  			givenPlanBuilder:       TestApplyHookMutationsBuilder{},
  1736  			givenResponse:          resp,
  1737  			expectedResponse:       expResp,
  1738  			expectedReject:         nil,
  1739  			expectedModuleContexts: foobarModuleCtx,
  1740  			expectedStageOutcomes: []StageOutcome{
  1741  				{
  1742  					Entity: entityAuctionResponse,
  1743  					Stage:  hooks.StageAuctionResponse.String(),
  1744  					Groups: []GroupOutcome{
  1745  						{
  1746  							InvocationResults: []HookOutcome{
  1747  								{
  1748  									AnalyticsTags: hookanalytics.Analytics{},
  1749  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
  1750  									Status:        StatusSuccess,
  1751  									Action:        ActionUpdate,
  1752  									Message:       "",
  1753  									DebugMessages: []string{
  1754  										fmt.Sprintf("Hook mutation successfully applied, affected key: auctionResponse.bidResponse.custom-data, mutation type: %s", hookstage.MutationUpdate),
  1755  									},
  1756  									Errors:   nil,
  1757  									Warnings: nil,
  1758  								},
  1759  							},
  1760  						},
  1761  					},
  1762  				},
  1763  			},
  1764  		},
  1765  		{
  1766  			description:            "Stage execution can't be rejected - stage doesn't support rejection",
  1767  			givenPlanBuilder:       TestRejectPlanBuilder{},
  1768  			givenResponse:          resp,
  1769  			expectedResponse:       expResp,
  1770  			expectedReject:         &RejectError{0, HookID{ModuleCode: "foobar", HookImplCode: "foo"}, hooks.StageAuctionResponse.String()},
  1771  			expectedModuleContexts: foobarModuleCtx,
  1772  			expectedStageOutcomes: []StageOutcome{
  1773  				{
  1774  					Entity: entityAuctionResponse,
  1775  					Stage:  hooks.StageAuctionResponse.String(),
  1776  					Groups: []GroupOutcome{
  1777  						{
  1778  							InvocationResults: []HookOutcome{
  1779  								{
  1780  									AnalyticsTags: hookanalytics.Analytics{},
  1781  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "baz"},
  1782  									Status:        StatusExecutionFailure,
  1783  									Action:        "",
  1784  									Message:       "",
  1785  									DebugMessages: nil,
  1786  									Errors:        []string{"unexpected error"},
  1787  									Warnings:      nil,
  1788  								},
  1789  							},
  1790  						},
  1791  						{
  1792  							InvocationResults: []HookOutcome{
  1793  								{
  1794  									AnalyticsTags: hookanalytics.Analytics{},
  1795  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
  1796  									Status:        StatusExecutionFailure,
  1797  									Action:        "",
  1798  									Message:       "",
  1799  									DebugMessages: nil,
  1800  									Errors: []string{
  1801  										fmt.Sprintf("Module (name: foobar, hook code: foo) tried to reject request on the %s stage that does not support rejection", hooks.StageAuctionResponse),
  1802  									},
  1803  									Warnings: nil,
  1804  								},
  1805  							},
  1806  						},
  1807  						{
  1808  							InvocationResults: []HookOutcome{
  1809  								{
  1810  									AnalyticsTags: hookanalytics.Analytics{},
  1811  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
  1812  									Status:        StatusSuccess,
  1813  									Action:        ActionUpdate,
  1814  									Message:       "",
  1815  									DebugMessages: []string{
  1816  										fmt.Sprintf("Hook mutation successfully applied, affected key: auctionResponse.bidResponse.custom-data, mutation type: %s", hookstage.MutationUpdate),
  1817  									},
  1818  									Errors:   nil,
  1819  									Warnings: nil,
  1820  								},
  1821  							},
  1822  						},
  1823  					},
  1824  				},
  1825  			},
  1826  		},
  1827  		{
  1828  			description:            "Request can be changed when a hook times out",
  1829  			givenPlanBuilder:       TestWithTimeoutPlanBuilder{},
  1830  			givenResponse:          resp,
  1831  			expectedResponse:       expResp,
  1832  			expectedReject:         nil,
  1833  			expectedModuleContexts: foobarModuleCtx,
  1834  			expectedStageOutcomes: []StageOutcome{
  1835  				{
  1836  					Entity: entityAuctionResponse,
  1837  					Stage:  hooks.StageAuctionResponse.String(),
  1838  					Groups: []GroupOutcome{
  1839  						{
  1840  							InvocationResults: []HookOutcome{
  1841  								{
  1842  									AnalyticsTags: hookanalytics.Analytics{},
  1843  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "foo"},
  1844  									Status:        StatusTimeout,
  1845  									Action:        "",
  1846  									Message:       "",
  1847  									DebugMessages: nil,
  1848  									Errors:        []string{"Hook execution timeout"},
  1849  									Warnings:      nil,
  1850  								},
  1851  							},
  1852  						},
  1853  						{
  1854  							InvocationResults: []HookOutcome{
  1855  								{
  1856  									AnalyticsTags: hookanalytics.Analytics{},
  1857  									HookID:        HookID{ModuleCode: "foobar", HookImplCode: "bar"},
  1858  									Status:        StatusSuccess,
  1859  									Action:        ActionUpdate,
  1860  									Message:       "",
  1861  									DebugMessages: []string{
  1862  										fmt.Sprintf("Hook mutation successfully applied, affected key: auctionResponse.bidResponse.custom-data, mutation type: %s", hookstage.MutationUpdate),
  1863  									},
  1864  									Errors:   nil,
  1865  									Warnings: nil,
  1866  								},
  1867  							},
  1868  						},
  1869  					},
  1870  				},
  1871  			},
  1872  		},
  1873  		{
  1874  			description:      "Modules contexts are preserved and correct",
  1875  			givenPlanBuilder: TestWithModuleContextsPlanBuilder{},
  1876  			givenResponse:    resp,
  1877  			expectedResponse: resp,
  1878  			expectedReject:   nil,
  1879  			expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{
  1880  				"module-1": {"auction-response-ctx-1": "some-ctx-1", "auction-response-ctx-3": "some-ctx-3"},
  1881  				"module-2": {"auction-response-ctx-2": "some-ctx-2"},
  1882  			}},
  1883  			expectedStageOutcomes: []StageOutcome{
  1884  				{
  1885  					Entity: entityAuctionResponse,
  1886  					Stage:  hooks.StageAuctionResponse.String(),
  1887  					Groups: []GroupOutcome{
  1888  						{
  1889  							InvocationResults: []HookOutcome{
  1890  								{
  1891  									AnalyticsTags: hookanalytics.Analytics{},
  1892  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "foo"},
  1893  									Status:        StatusSuccess,
  1894  									Action:        ActionNone,
  1895  									Message:       "",
  1896  									DebugMessages: nil,
  1897  									Errors:        nil,
  1898  									Warnings:      nil,
  1899  								},
  1900  								{
  1901  									AnalyticsTags: hookanalytics.Analytics{},
  1902  									HookID:        HookID{ModuleCode: "module-2", HookImplCode: "baz"},
  1903  									Status:        StatusSuccess,
  1904  									Action:        ActionNone,
  1905  									Message:       "",
  1906  									DebugMessages: nil,
  1907  									Errors:        nil,
  1908  									Warnings:      nil,
  1909  								},
  1910  							},
  1911  						},
  1912  						{
  1913  							InvocationResults: []HookOutcome{
  1914  								{
  1915  									AnalyticsTags: hookanalytics.Analytics{},
  1916  									HookID:        HookID{ModuleCode: "module-1", HookImplCode: "bar"},
  1917  									Status:        StatusSuccess,
  1918  									Action:        ActionNone,
  1919  									Message:       "",
  1920  									DebugMessages: nil,
  1921  									Errors:        nil,
  1922  									Warnings:      nil,
  1923  								},
  1924  							},
  1925  						},
  1926  					},
  1927  				},
  1928  			},
  1929  		},
  1930  	}
  1931  
  1932  	for _, test := range testCases {
  1933  		t.Run(test.description, func(t *testing.T) {
  1934  			exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{})
  1935  
  1936  			privacyConfig := getModuleActivities("foo", false, false)
  1937  			ac := privacy.NewActivityControl(privacyConfig)
  1938  			exec.SetActivityControl(ac)
  1939  
  1940  			exec.ExecuteAuctionResponseStage(test.givenResponse)
  1941  
  1942  			assert.Equal(t, test.expectedResponse, test.givenResponse, "Incorrect response update.")
  1943  			assert.Equal(t, test.expectedModuleContexts, exec.moduleContexts, "Incorrect module contexts")
  1944  
  1945  			stageOutcomes := exec.GetOutcomes()
  1946  			if len(test.expectedStageOutcomes) == 0 {
  1947  				assert.Empty(t, stageOutcomes, "Incorrect stage outcomes.")
  1948  			} else {
  1949  				assertEqualStageOutcomes(t, test.expectedStageOutcomes[0], stageOutcomes[0])
  1950  			}
  1951  		})
  1952  	}
  1953  }
  1954  
  1955  func TestInterStageContextCommunication(t *testing.T) {
  1956  	body := []byte(`{"foo": "bar"}`)
  1957  	reader := bytes.NewReader(body)
  1958  	exec := NewHookExecutor(TestWithModuleContextsPlanBuilder{}, EndpointAuction, &metricsConfig.NilMetricsEngine{})
  1959  	req, err := http.NewRequest(http.MethodPost, "https://prebid.com/openrtb2/auction", reader)
  1960  	assert.NoError(t, err)
  1961  
  1962  	// test that context added at the entrypoint stage
  1963  	_, reject := exec.ExecuteEntrypointStage(req, body)
  1964  	assert.Nil(t, reject, "Unexpected reject from entrypoint stage.")
  1965  	assert.Equal(
  1966  		t,
  1967  		&moduleContexts{ctxs: map[string]hookstage.ModuleContext{
  1968  			"module-1": {
  1969  				"entrypoint-ctx-1": "some-ctx-1",
  1970  				"entrypoint-ctx-3": "some-ctx-3",
  1971  			},
  1972  			"module-2": {"entrypoint-ctx-2": "some-ctx-2"},
  1973  		}},
  1974  		exec.moduleContexts,
  1975  		"Wrong module contexts after executing entrypoint hook.",
  1976  	)
  1977  
  1978  	// test that context added at the raw-auction stage merged with existing module contexts
  1979  	_, reject = exec.ExecuteRawAuctionStage(body)
  1980  	assert.Nil(t, reject, "Unexpected reject from raw-auction stage.")
  1981  	assert.Equal(t, &moduleContexts{ctxs: map[string]hookstage.ModuleContext{
  1982  		"module-1": {
  1983  			"entrypoint-ctx-1":  "some-ctx-1",
  1984  			"entrypoint-ctx-3":  "some-ctx-3",
  1985  			"raw-auction-ctx-1": "some-ctx-1",
  1986  			"raw-auction-ctx-3": "some-ctx-3",
  1987  		},
  1988  		"module-2": {
  1989  			"entrypoint-ctx-2":  "some-ctx-2",
  1990  			"raw-auction-ctx-2": "some-ctx-2",
  1991  		},
  1992  	}}, exec.moduleContexts, "Wrong module contexts after executing raw-auction hook.")
  1993  
  1994  	// test that context added at the processed-auction stage merged with existing module contexts
  1995  	err = exec.ExecuteProcessedAuctionStage(&openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}})
  1996  	assert.Nil(t, err, "Unexpected reject from processed-auction stage.")
  1997  	assert.Equal(t, &moduleContexts{ctxs: map[string]hookstage.ModuleContext{
  1998  		"module-1": {
  1999  			"entrypoint-ctx-1":        "some-ctx-1",
  2000  			"entrypoint-ctx-3":        "some-ctx-3",
  2001  			"raw-auction-ctx-1":       "some-ctx-1",
  2002  			"raw-auction-ctx-3":       "some-ctx-3",
  2003  			"processed-auction-ctx-1": "some-ctx-1",
  2004  			"processed-auction-ctx-3": "some-ctx-3",
  2005  		},
  2006  		"module-2": {
  2007  			"entrypoint-ctx-2":        "some-ctx-2",
  2008  			"raw-auction-ctx-2":       "some-ctx-2",
  2009  			"processed-auction-ctx-2": "some-ctx-2",
  2010  		},
  2011  	}}, exec.moduleContexts, "Wrong module contexts after executing processed-auction hook.")
  2012  
  2013  	// test that context added at the raw bidder response stage merged with existing module contexts
  2014  	reject = exec.ExecuteRawBidderResponseStage(&adapters.BidderResponse{}, "some-bidder")
  2015  	assert.Nil(t, reject, "Unexpected reject from raw-bidder-response stage.")
  2016  	assert.Equal(t, &moduleContexts{ctxs: map[string]hookstage.ModuleContext{
  2017  		"module-1": {
  2018  			"entrypoint-ctx-1":          "some-ctx-1",
  2019  			"entrypoint-ctx-3":          "some-ctx-3",
  2020  			"raw-auction-ctx-1":         "some-ctx-1",
  2021  			"raw-auction-ctx-3":         "some-ctx-3",
  2022  			"processed-auction-ctx-1":   "some-ctx-1",
  2023  			"processed-auction-ctx-3":   "some-ctx-3",
  2024  			"raw-bidder-response-ctx-1": "some-ctx-1",
  2025  			"raw-bidder-response-ctx-3": "some-ctx-3",
  2026  		},
  2027  		"module-2": {
  2028  			"entrypoint-ctx-2":          "some-ctx-2",
  2029  			"raw-auction-ctx-2":         "some-ctx-2",
  2030  			"processed-auction-ctx-2":   "some-ctx-2",
  2031  			"raw-bidder-response-ctx-2": "some-ctx-2",
  2032  		},
  2033  	}}, exec.moduleContexts, "Wrong module contexts after executing raw-bidder-response hook.")
  2034  
  2035  	// test that context added at the auction-response stage merged with existing module contexts
  2036  	exec.ExecuteAuctionResponseStage(&openrtb2.BidResponse{})
  2037  	assert.Nil(t, reject, "Unexpected reject from raw-auction stage.")
  2038  	assert.Equal(t, &moduleContexts{ctxs: map[string]hookstage.ModuleContext{
  2039  		"module-1": {
  2040  			"entrypoint-ctx-1":          "some-ctx-1",
  2041  			"entrypoint-ctx-3":          "some-ctx-3",
  2042  			"raw-auction-ctx-1":         "some-ctx-1",
  2043  			"raw-auction-ctx-3":         "some-ctx-3",
  2044  			"processed-auction-ctx-1":   "some-ctx-1",
  2045  			"processed-auction-ctx-3":   "some-ctx-3",
  2046  			"raw-bidder-response-ctx-1": "some-ctx-1",
  2047  			"raw-bidder-response-ctx-3": "some-ctx-3",
  2048  			"auction-response-ctx-1":    "some-ctx-1",
  2049  			"auction-response-ctx-3":    "some-ctx-3",
  2050  		},
  2051  		"module-2": {
  2052  			"entrypoint-ctx-2":          "some-ctx-2",
  2053  			"raw-auction-ctx-2":         "some-ctx-2",
  2054  			"processed-auction-ctx-2":   "some-ctx-2",
  2055  			"raw-bidder-response-ctx-2": "some-ctx-2",
  2056  			"auction-response-ctx-2":    "some-ctx-2",
  2057  		},
  2058  	}}, exec.moduleContexts, "Wrong module contexts after executing auction-response hook.")
  2059  }
  2060  
  2061  type TestApplyHookMutationsBuilder struct {
  2062  	hooks.EmptyPlanBuilder
  2063  }
  2064  
  2065  func (e TestApplyHookMutationsBuilder) PlanForEntrypointStage(_ string) hooks.Plan[hookstage.Entrypoint] {
  2066  	return hooks.Plan[hookstage.Entrypoint]{
  2067  		hooks.Group[hookstage.Entrypoint]{
  2068  			Timeout: 10 * time.Millisecond,
  2069  			Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
  2070  				{Module: "foobar", Code: "foo", Hook: mockUpdateHeaderEntrypointHook{}},
  2071  				{Module: "foobar", Code: "foobaz", Hook: mockFailedMutationHook{}},
  2072  				{Module: "foobar", Code: "bar", Hook: mockUpdateQueryEntrypointHook{}},
  2073  			},
  2074  		},
  2075  		hooks.Group[hookstage.Entrypoint]{
  2076  			Timeout: 10 * time.Millisecond,
  2077  			Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
  2078  				{Module: "foobar", Code: "baz", Hook: mockUpdateBodyHook{}},
  2079  				{Module: "foobar", Code: "foo", Hook: mockFailureHook{}},
  2080  			},
  2081  		},
  2082  	}
  2083  }
  2084  
  2085  func (e TestApplyHookMutationsBuilder) PlanForRawAuctionStage(_ string, _ *config.Account) hooks.Plan[hookstage.RawAuctionRequest] {
  2086  	return hooks.Plan[hookstage.RawAuctionRequest]{
  2087  		hooks.Group[hookstage.RawAuctionRequest]{
  2088  			Timeout: 10 * time.Millisecond,
  2089  			Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{
  2090  				{Module: "foobar", Code: "foo", Hook: mockUpdateBodyHook{}},
  2091  				{Module: "foobar", Code: "bar", Hook: mockFailedMutationHook{}},
  2092  			},
  2093  		},
  2094  		hooks.Group[hookstage.RawAuctionRequest]{
  2095  			Timeout: 10 * time.Millisecond,
  2096  			Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{
  2097  				{Module: "foobar", Code: "baz", Hook: mockFailureHook{}},
  2098  			},
  2099  		},
  2100  	}
  2101  }
  2102  
  2103  func (e TestApplyHookMutationsBuilder) PlanForProcessedAuctionStage(_ string, _ *config.Account) hooks.Plan[hookstage.ProcessedAuctionRequest] {
  2104  	return hooks.Plan[hookstage.ProcessedAuctionRequest]{
  2105  		hooks.Group[hookstage.ProcessedAuctionRequest]{
  2106  			Timeout: 10 * time.Millisecond,
  2107  			Hooks: []hooks.HookWrapper[hookstage.ProcessedAuctionRequest]{
  2108  				{Module: "foobar", Code: "foo", Hook: mockUpdateBidRequestHook{}},
  2109  			},
  2110  		},
  2111  	}
  2112  }
  2113  
  2114  func (e TestApplyHookMutationsBuilder) PlanForBidderRequestStage(_ string, _ *config.Account) hooks.Plan[hookstage.BidderRequest] {
  2115  	return hooks.Plan[hookstage.BidderRequest]{
  2116  		hooks.Group[hookstage.BidderRequest]{
  2117  			Timeout: 10 * time.Millisecond,
  2118  			Hooks: []hooks.HookWrapper[hookstage.BidderRequest]{
  2119  				{Module: "foobar", Code: "foo", Hook: mockUpdateBidRequestHook{}},
  2120  				{Module: "foobar", Code: "bar", Hook: mockFailedMutationHook{}},
  2121  			},
  2122  		},
  2123  		hooks.Group[hookstage.BidderRequest]{
  2124  			Timeout: 10 * time.Millisecond,
  2125  			Hooks: []hooks.HookWrapper[hookstage.BidderRequest]{
  2126  				{Module: "foobar", Code: "baz", Hook: mockFailureHook{}},
  2127  			},
  2128  		},
  2129  	}
  2130  }
  2131  
  2132  func (e TestApplyHookMutationsBuilder) PlanForRawBidderResponseStage(_ string, _ *config.Account) hooks.Plan[hookstage.RawBidderResponse] {
  2133  	return hooks.Plan[hookstage.RawBidderResponse]{
  2134  		hooks.Group[hookstage.RawBidderResponse]{
  2135  			Timeout: 10 * time.Millisecond,
  2136  			Hooks: []hooks.HookWrapper[hookstage.RawBidderResponse]{
  2137  				{Module: "foobar", Code: "foo", Hook: mockUpdateBidderResponseHook{}},
  2138  			},
  2139  		},
  2140  	}
  2141  }
  2142  
  2143  func (e TestApplyHookMutationsBuilder) PlanForAllProcessedBidResponsesStage(_ string, _ *config.Account) hooks.Plan[hookstage.AllProcessedBidResponses] {
  2144  	return hooks.Plan[hookstage.AllProcessedBidResponses]{
  2145  		hooks.Group[hookstage.AllProcessedBidResponses]{
  2146  			Timeout: 10 * time.Millisecond,
  2147  			Hooks: []hooks.HookWrapper[hookstage.AllProcessedBidResponses]{
  2148  				{Module: "foobar", Code: "foo", Hook: mockUpdateBiddersResponsesHook{}},
  2149  				{Module: "foobar", Code: "bar", Hook: mockFailedMutationHook{}},
  2150  			},
  2151  		},
  2152  		hooks.Group[hookstage.AllProcessedBidResponses]{
  2153  			Timeout: 10 * time.Millisecond,
  2154  			Hooks: []hooks.HookWrapper[hookstage.AllProcessedBidResponses]{
  2155  				{Module: "foobar", Code: "baz", Hook: mockFailureHook{}},
  2156  			},
  2157  		},
  2158  	}
  2159  }
  2160  
  2161  func (e TestApplyHookMutationsBuilder) PlanForAuctionResponseStage(_ string, _ *config.Account) hooks.Plan[hookstage.AuctionResponse] {
  2162  	return hooks.Plan[hookstage.AuctionResponse]{
  2163  		hooks.Group[hookstage.AuctionResponse]{
  2164  			Timeout: 1 * time.Millisecond,
  2165  			Hooks: []hooks.HookWrapper[hookstage.AuctionResponse]{
  2166  				{Module: "foobar", Code: "foo", Hook: mockUpdateBidResponseHook{}},
  2167  			},
  2168  		},
  2169  	}
  2170  }
  2171  
  2172  type TestRejectPlanBuilder struct {
  2173  	hooks.EmptyPlanBuilder
  2174  }
  2175  
  2176  func (e TestRejectPlanBuilder) PlanForEntrypointStage(_ string) hooks.Plan[hookstage.Entrypoint] {
  2177  	return hooks.Plan[hookstage.Entrypoint]{
  2178  		hooks.Group[hookstage.Entrypoint]{
  2179  			Timeout: 10 * time.Millisecond,
  2180  			Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
  2181  				{Module: "foobar", Code: "foo", Hook: mockUpdateHeaderEntrypointHook{}},
  2182  				{Module: "foobar", Code: "baz", Hook: mockErrorHook{}},
  2183  			},
  2184  		},
  2185  		hooks.Group[hookstage.Entrypoint]{
  2186  			Timeout: 10 * time.Millisecond,
  2187  			Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
  2188  				// reject stage
  2189  				{Module: "foobar", Code: "bar", Hook: mockRejectHook{}},
  2190  				// next hook rejected: we use timeout hook to make sure
  2191  				// that it runs longer than previous one, so it won't be executed earlier
  2192  				{Module: "foobar", Code: "baz", Hook: mockTimeoutHook{}},
  2193  			},
  2194  		},
  2195  		// group of hooks rejected
  2196  		hooks.Group[hookstage.Entrypoint]{
  2197  			Timeout: 10 * time.Millisecond,
  2198  			Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
  2199  				{Module: "foobar", Code: "foo", Hook: mockUpdateHeaderEntrypointHook{}},
  2200  				{Module: "foobar", Code: "baz", Hook: mockErrorHook{}},
  2201  			},
  2202  		},
  2203  	}
  2204  }
  2205  
  2206  func (e TestRejectPlanBuilder) PlanForRawAuctionStage(_ string, _ *config.Account) hooks.Plan[hookstage.RawAuctionRequest] {
  2207  	return hooks.Plan[hookstage.RawAuctionRequest]{
  2208  		hooks.Group[hookstage.RawAuctionRequest]{
  2209  			Timeout: 10 * time.Millisecond,
  2210  			Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{
  2211  				{Module: "foobar", Code: "foo", Hook: mockUpdateBodyHook{}},
  2212  				{Module: "foobar", Code: "baz", Hook: mockErrorHook{}},
  2213  			},
  2214  		},
  2215  		hooks.Group[hookstage.RawAuctionRequest]{
  2216  			Timeout: 10 * time.Millisecond,
  2217  			Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{
  2218  				{Module: "foobar", Code: "bar", Hook: mockRejectHook{}},
  2219  				// next hook rejected: we use timeout hook to make sure
  2220  				// that it runs longer than previous one, so it won't be executed earlier
  2221  				{Module: "foobar", Code: "baz", Hook: mockTimeoutHook{}},
  2222  			},
  2223  		},
  2224  		// group of hooks rejected
  2225  		hooks.Group[hookstage.RawAuctionRequest]{
  2226  			Timeout: 10 * time.Millisecond,
  2227  			Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{
  2228  				{Module: "foobar", Code: "foo", Hook: mockUpdateBodyHook{}},
  2229  				{Module: "foobar", Code: "baz", Hook: mockErrorHook{}},
  2230  			},
  2231  		},
  2232  	}
  2233  }
  2234  
  2235  func (e TestRejectPlanBuilder) PlanForProcessedAuctionStage(_ string, _ *config.Account) hooks.Plan[hookstage.ProcessedAuctionRequest] {
  2236  	return hooks.Plan[hookstage.ProcessedAuctionRequest]{
  2237  		hooks.Group[hookstage.ProcessedAuctionRequest]{
  2238  			Timeout: 10 * time.Millisecond,
  2239  			Hooks: []hooks.HookWrapper[hookstage.ProcessedAuctionRequest]{
  2240  				{Module: "foobar", Code: "foo", Hook: mockRejectHook{}},
  2241  			},
  2242  		},
  2243  		hooks.Group[hookstage.ProcessedAuctionRequest]{
  2244  			Timeout: 10 * time.Millisecond,
  2245  			Hooks: []hooks.HookWrapper[hookstage.ProcessedAuctionRequest]{
  2246  				{Module: "foobar", Code: "bar", Hook: mockUpdateBidRequestHook{}},
  2247  			},
  2248  		},
  2249  	}
  2250  }
  2251  
  2252  func (e TestRejectPlanBuilder) PlanForBidderRequestStage(_ string, _ *config.Account) hooks.Plan[hookstage.BidderRequest] {
  2253  	return hooks.Plan[hookstage.BidderRequest]{
  2254  		hooks.Group[hookstage.BidderRequest]{
  2255  			Timeout: 10 * time.Millisecond,
  2256  			Hooks: []hooks.HookWrapper[hookstage.BidderRequest]{
  2257  				{Module: "foobar", Code: "baz", Hook: mockErrorHook{}},
  2258  			},
  2259  		},
  2260  		hooks.Group[hookstage.BidderRequest]{
  2261  			Timeout: 10 * time.Millisecond,
  2262  			Hooks: []hooks.HookWrapper[hookstage.BidderRequest]{
  2263  				{Module: "foobar", Code: "foo", Hook: mockRejectHook{}},
  2264  			},
  2265  		},
  2266  		hooks.Group[hookstage.BidderRequest]{
  2267  			Timeout: 10 * time.Millisecond,
  2268  			Hooks: []hooks.HookWrapper[hookstage.BidderRequest]{
  2269  				{Module: "foobar", Code: "bar", Hook: mockUpdateBidRequestHook{}},
  2270  			},
  2271  		},
  2272  	}
  2273  }
  2274  
  2275  func (e TestRejectPlanBuilder) PlanForRawBidderResponseStage(_ string, _ *config.Account) hooks.Plan[hookstage.RawBidderResponse] {
  2276  	return hooks.Plan[hookstage.RawBidderResponse]{
  2277  		hooks.Group[hookstage.RawBidderResponse]{
  2278  			Timeout: 10 * time.Millisecond,
  2279  			Hooks: []hooks.HookWrapper[hookstage.RawBidderResponse]{
  2280  				{Module: "foobar", Code: "foo", Hook: mockRejectHook{}},
  2281  			},
  2282  		},
  2283  	}
  2284  }
  2285  
  2286  func (e TestRejectPlanBuilder) PlanForAllProcessedBidResponsesStage(_ string, _ *config.Account) hooks.Plan[hookstage.AllProcessedBidResponses] {
  2287  	return hooks.Plan[hookstage.AllProcessedBidResponses]{
  2288  		hooks.Group[hookstage.AllProcessedBidResponses]{
  2289  			Timeout: 10 * time.Millisecond,
  2290  			Hooks: []hooks.HookWrapper[hookstage.AllProcessedBidResponses]{
  2291  				{Module: "foobar", Code: "baz", Hook: mockErrorHook{}},
  2292  			},
  2293  		},
  2294  		// rejection ignored, stage doesn't support rejection
  2295  		hooks.Group[hookstage.AllProcessedBidResponses]{
  2296  			Timeout: 10 * time.Millisecond,
  2297  			Hooks: []hooks.HookWrapper[hookstage.AllProcessedBidResponses]{
  2298  				{Module: "foobar", Code: "foo", Hook: mockRejectHook{}},
  2299  			},
  2300  		},
  2301  		// hook executed and payload updated because this stage doesn't support rejection
  2302  		hooks.Group[hookstage.AllProcessedBidResponses]{
  2303  			Timeout: 10 * time.Millisecond,
  2304  			Hooks: []hooks.HookWrapper[hookstage.AllProcessedBidResponses]{
  2305  				{Module: "foobar", Code: "bar", Hook: mockUpdateBiddersResponsesHook{}},
  2306  			},
  2307  		},
  2308  	}
  2309  }
  2310  
  2311  func (e TestRejectPlanBuilder) PlanForAuctionResponseStage(_ string, _ *config.Account) hooks.Plan[hookstage.AuctionResponse] {
  2312  	return hooks.Plan[hookstage.AuctionResponse]{
  2313  		hooks.Group[hookstage.AuctionResponse]{
  2314  			Timeout: 1 * time.Millisecond,
  2315  			Hooks: []hooks.HookWrapper[hookstage.AuctionResponse]{
  2316  				{Module: "foobar", Code: "baz", Hook: mockErrorHook{}},
  2317  			},
  2318  		},
  2319  		// rejection ignored, stage doesn't support rejection
  2320  		hooks.Group[hookstage.AuctionResponse]{
  2321  			Timeout: 1 * time.Millisecond,
  2322  			Hooks: []hooks.HookWrapper[hookstage.AuctionResponse]{
  2323  				{Module: "foobar", Code: "foo", Hook: mockRejectHook{}},
  2324  			},
  2325  		},
  2326  		// hook executed and payload updated because this stage doesn't support rejection
  2327  		hooks.Group[hookstage.AuctionResponse]{
  2328  			Timeout: 1 * time.Millisecond,
  2329  			Hooks: []hooks.HookWrapper[hookstage.AuctionResponse]{
  2330  				{Module: "foobar", Code: "bar", Hook: mockUpdateBidResponseHook{}},
  2331  			},
  2332  		},
  2333  	}
  2334  }
  2335  
  2336  type TestWithTimeoutPlanBuilder struct {
  2337  	hooks.EmptyPlanBuilder
  2338  }
  2339  
  2340  func (e TestWithTimeoutPlanBuilder) PlanForEntrypointStage(_ string) hooks.Plan[hookstage.Entrypoint] {
  2341  	return hooks.Plan[hookstage.Entrypoint]{
  2342  		hooks.Group[hookstage.Entrypoint]{
  2343  			Timeout: 10 * time.Millisecond,
  2344  			Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
  2345  				{Module: "foobar", Code: "foo", Hook: mockUpdateHeaderEntrypointHook{}},
  2346  				{Module: "foobar", Code: "bar", Hook: mockTimeoutHook{}},
  2347  			},
  2348  		},
  2349  		hooks.Group[hookstage.Entrypoint]{
  2350  			Timeout: 10 * time.Millisecond,
  2351  			Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
  2352  				{Module: "foobar", Code: "baz", Hook: mockUpdateBodyHook{}},
  2353  			},
  2354  		},
  2355  	}
  2356  }
  2357  
  2358  func (e TestWithTimeoutPlanBuilder) PlanForRawAuctionStage(_ string, _ *config.Account) hooks.Plan[hookstage.RawAuctionRequest] {
  2359  	return hooks.Plan[hookstage.RawAuctionRequest]{
  2360  		hooks.Group[hookstage.RawAuctionRequest]{
  2361  			Timeout: 10 * time.Millisecond,
  2362  			Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{
  2363  				{Module: "foobar", Code: "foo", Hook: mockUpdateBodyHook{}},
  2364  			},
  2365  		},
  2366  		hooks.Group[hookstage.RawAuctionRequest]{
  2367  			Timeout: 10 * time.Millisecond,
  2368  			Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{
  2369  				{Module: "foobar", Code: "bar", Hook: mockTimeoutHook{}},
  2370  			},
  2371  		},
  2372  	}
  2373  }
  2374  
  2375  func (e TestWithTimeoutPlanBuilder) PlanForProcessedAuctionStage(_ string, _ *config.Account) hooks.Plan[hookstage.ProcessedAuctionRequest] {
  2376  	return hooks.Plan[hookstage.ProcessedAuctionRequest]{
  2377  		hooks.Group[hookstage.ProcessedAuctionRequest]{
  2378  			Timeout: 10 * time.Millisecond,
  2379  			Hooks: []hooks.HookWrapper[hookstage.ProcessedAuctionRequest]{
  2380  				{Module: "foobar", Code: "foo", Hook: mockTimeoutHook{}},
  2381  			},
  2382  		},
  2383  		hooks.Group[hookstage.ProcessedAuctionRequest]{
  2384  			Timeout: 10 * time.Millisecond,
  2385  			Hooks: []hooks.HookWrapper[hookstage.ProcessedAuctionRequest]{
  2386  				{Module: "foobar", Code: "bar", Hook: mockUpdateBidRequestHook{}},
  2387  			},
  2388  		},
  2389  	}
  2390  }
  2391  
  2392  func (e TestWithTimeoutPlanBuilder) PlanForBidderRequestStage(_ string, _ *config.Account) hooks.Plan[hookstage.BidderRequest] {
  2393  	return hooks.Plan[hookstage.BidderRequest]{
  2394  		hooks.Group[hookstage.BidderRequest]{
  2395  			Timeout: 10 * time.Millisecond,
  2396  			Hooks: []hooks.HookWrapper[hookstage.BidderRequest]{
  2397  				{Module: "foobar", Code: "foo", Hook: mockTimeoutHook{}},
  2398  			},
  2399  		},
  2400  		hooks.Group[hookstage.BidderRequest]{
  2401  			Timeout: 10 * time.Millisecond,
  2402  			Hooks: []hooks.HookWrapper[hookstage.BidderRequest]{
  2403  				{Module: "foobar", Code: "bar", Hook: mockUpdateBidRequestHook{}},
  2404  			},
  2405  		},
  2406  	}
  2407  }
  2408  
  2409  func (e TestWithTimeoutPlanBuilder) PlanForRawBidderResponseStage(_ string, _ *config.Account) hooks.Plan[hookstage.RawBidderResponse] {
  2410  	return hooks.Plan[hookstage.RawBidderResponse]{
  2411  		hooks.Group[hookstage.RawBidderResponse]{
  2412  			Timeout: 10 * time.Millisecond,
  2413  			Hooks: []hooks.HookWrapper[hookstage.RawBidderResponse]{
  2414  				{Module: "foobar", Code: "foo", Hook: mockTimeoutHook{}},
  2415  			},
  2416  		},
  2417  		hooks.Group[hookstage.RawBidderResponse]{
  2418  			Timeout: 10 * time.Millisecond,
  2419  			Hooks: []hooks.HookWrapper[hookstage.RawBidderResponse]{
  2420  				{Module: "foobar", Code: "bar", Hook: mockUpdateBidderResponseHook{}},
  2421  			},
  2422  		},
  2423  	}
  2424  }
  2425  
  2426  func (e TestWithTimeoutPlanBuilder) PlanForAllProcessedBidResponsesStage(_ string, _ *config.Account) hooks.Plan[hookstage.AllProcessedBidResponses] {
  2427  	return hooks.Plan[hookstage.AllProcessedBidResponses]{
  2428  		hooks.Group[hookstage.AllProcessedBidResponses]{
  2429  			Timeout: 10 * time.Millisecond,
  2430  			Hooks: []hooks.HookWrapper[hookstage.AllProcessedBidResponses]{
  2431  				{Module: "foobar", Code: "foo", Hook: mockTimeoutHook{}},
  2432  			},
  2433  		},
  2434  		hooks.Group[hookstage.AllProcessedBidResponses]{
  2435  			Timeout: 10 * time.Millisecond,
  2436  			Hooks: []hooks.HookWrapper[hookstage.AllProcessedBidResponses]{
  2437  				{Module: "foobar", Code: "bar", Hook: mockUpdateBiddersResponsesHook{}},
  2438  			},
  2439  		},
  2440  	}
  2441  }
  2442  
  2443  func (e TestWithTimeoutPlanBuilder) PlanForAuctionResponseStage(_ string, _ *config.Account) hooks.Plan[hookstage.AuctionResponse] {
  2444  	return hooks.Plan[hookstage.AuctionResponse]{
  2445  		hooks.Group[hookstage.AuctionResponse]{
  2446  			Timeout: 1 * time.Millisecond,
  2447  			Hooks: []hooks.HookWrapper[hookstage.AuctionResponse]{
  2448  				{Module: "foobar", Code: "foo", Hook: mockTimeoutHook{}},
  2449  			},
  2450  		},
  2451  		hooks.Group[hookstage.AuctionResponse]{
  2452  			Timeout: 1 * time.Millisecond,
  2453  			Hooks: []hooks.HookWrapper[hookstage.AuctionResponse]{
  2454  				{Module: "foobar", Code: "bar", Hook: mockUpdateBidResponseHook{}},
  2455  			},
  2456  		},
  2457  	}
  2458  }
  2459  
  2460  type TestWithModuleContextsPlanBuilder struct {
  2461  	hooks.EmptyPlanBuilder
  2462  }
  2463  
  2464  func (e TestWithModuleContextsPlanBuilder) PlanForEntrypointStage(_ string) hooks.Plan[hookstage.Entrypoint] {
  2465  	return hooks.Plan[hookstage.Entrypoint]{
  2466  		hooks.Group[hookstage.Entrypoint]{
  2467  			Timeout: 10 * time.Millisecond,
  2468  			Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
  2469  				{Module: "module-1", Code: "foo", Hook: mockModuleContextHook{key: "entrypoint-ctx-1", val: "some-ctx-1"}},
  2470  			},
  2471  		},
  2472  		hooks.Group[hookstage.Entrypoint]{
  2473  			Timeout: 10 * time.Millisecond,
  2474  			Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
  2475  				{Module: "module-2", Code: "bar", Hook: mockModuleContextHook{key: "entrypoint-ctx-2", val: "some-ctx-2"}},
  2476  				{Module: "module-1", Code: "baz", Hook: mockModuleContextHook{key: "entrypoint-ctx-3", val: "some-ctx-3"}},
  2477  			},
  2478  		},
  2479  	}
  2480  }
  2481  
  2482  func (e TestWithModuleContextsPlanBuilder) PlanForRawAuctionStage(_ string, _ *config.Account) hooks.Plan[hookstage.RawAuctionRequest] {
  2483  	return hooks.Plan[hookstage.RawAuctionRequest]{
  2484  		hooks.Group[hookstage.RawAuctionRequest]{
  2485  			Timeout: 10 * time.Millisecond,
  2486  			Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{
  2487  				{Module: "module-1", Code: "foo", Hook: mockModuleContextHook{key: "raw-auction-ctx-1", val: "some-ctx-1"}},
  2488  				{Module: "module-2", Code: "baz", Hook: mockModuleContextHook{key: "raw-auction-ctx-2", val: "some-ctx-2"}},
  2489  			},
  2490  		},
  2491  		hooks.Group[hookstage.RawAuctionRequest]{
  2492  			Timeout: 10 * time.Millisecond,
  2493  			Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{
  2494  				{Module: "module-1", Code: "bar", Hook: mockModuleContextHook{key: "raw-auction-ctx-3", val: "some-ctx-3"}},
  2495  			},
  2496  		},
  2497  	}
  2498  }
  2499  
  2500  func (e TestWithModuleContextsPlanBuilder) PlanForProcessedAuctionStage(_ string, _ *config.Account) hooks.Plan[hookstage.ProcessedAuctionRequest] {
  2501  	return hooks.Plan[hookstage.ProcessedAuctionRequest]{
  2502  		hooks.Group[hookstage.ProcessedAuctionRequest]{
  2503  			Timeout: 10 * time.Millisecond,
  2504  			Hooks: []hooks.HookWrapper[hookstage.ProcessedAuctionRequest]{
  2505  				{Module: "module-1", Code: "foo", Hook: mockModuleContextHook{key: "processed-auction-ctx-1", val: "some-ctx-1"}},
  2506  			},
  2507  		},
  2508  		hooks.Group[hookstage.ProcessedAuctionRequest]{
  2509  			Timeout: 10 * time.Millisecond,
  2510  			Hooks: []hooks.HookWrapper[hookstage.ProcessedAuctionRequest]{
  2511  				{Module: "module-2", Code: "bar", Hook: mockModuleContextHook{key: "processed-auction-ctx-2", val: "some-ctx-2"}},
  2512  				{Module: "module-1", Code: "baz", Hook: mockModuleContextHook{key: "processed-auction-ctx-3", val: "some-ctx-3"}},
  2513  			},
  2514  		},
  2515  	}
  2516  }
  2517  
  2518  func (e TestWithModuleContextsPlanBuilder) PlanForBidderRequestStage(_ string, _ *config.Account) hooks.Plan[hookstage.BidderRequest] {
  2519  	return hooks.Plan[hookstage.BidderRequest]{
  2520  		hooks.Group[hookstage.BidderRequest]{
  2521  			Timeout: 10 * time.Millisecond,
  2522  			Hooks: []hooks.HookWrapper[hookstage.BidderRequest]{
  2523  				{Module: "module-1", Code: "foo", Hook: mockModuleContextHook{key: "bidder-request-ctx-1", val: "some-ctx-1"}},
  2524  			},
  2525  		},
  2526  		hooks.Group[hookstage.BidderRequest]{
  2527  			Timeout: 10 * time.Millisecond,
  2528  			Hooks: []hooks.HookWrapper[hookstage.BidderRequest]{
  2529  				{Module: "module-2", Code: "bar", Hook: mockModuleContextHook{key: "bidder-request-ctx-2", val: "some-ctx-2"}},
  2530  			},
  2531  		},
  2532  	}
  2533  }
  2534  
  2535  func (e TestWithModuleContextsPlanBuilder) PlanForRawBidderResponseStage(_ string, _ *config.Account) hooks.Plan[hookstage.RawBidderResponse] {
  2536  	return hooks.Plan[hookstage.RawBidderResponse]{
  2537  		hooks.Group[hookstage.RawBidderResponse]{
  2538  			Timeout: 10 * time.Millisecond,
  2539  			Hooks: []hooks.HookWrapper[hookstage.RawBidderResponse]{
  2540  				{Module: "module-1", Code: "foo", Hook: mockModuleContextHook{key: "raw-bidder-response-ctx-1", val: "some-ctx-1"}},
  2541  				{Module: "module-2", Code: "baz", Hook: mockModuleContextHook{key: "raw-bidder-response-ctx-2", val: "some-ctx-2"}},
  2542  			},
  2543  		},
  2544  		hooks.Group[hookstage.RawBidderResponse]{
  2545  			Timeout: 10 * time.Millisecond,
  2546  			Hooks: []hooks.HookWrapper[hookstage.RawBidderResponse]{
  2547  				{Module: "module-1", Code: "bar", Hook: mockModuleContextHook{key: "raw-bidder-response-ctx-3", val: "some-ctx-3"}},
  2548  			},
  2549  		},
  2550  	}
  2551  }
  2552  
  2553  func (e TestWithModuleContextsPlanBuilder) PlanForAllProcessedBidResponsesStage(_ string, _ *config.Account) hooks.Plan[hookstage.AllProcessedBidResponses] {
  2554  	return hooks.Plan[hookstage.AllProcessedBidResponses]{
  2555  		hooks.Group[hookstage.AllProcessedBidResponses]{
  2556  			Timeout: 10 * time.Millisecond,
  2557  			Hooks: []hooks.HookWrapper[hookstage.AllProcessedBidResponses]{
  2558  				{Module: "module-1", Code: "foo", Hook: mockModuleContextHook{key: "all-processed-bid-responses-ctx-1", val: "some-ctx-1"}},
  2559  			},
  2560  		},
  2561  		hooks.Group[hookstage.AllProcessedBidResponses]{
  2562  			Timeout: 10 * time.Millisecond,
  2563  			Hooks: []hooks.HookWrapper[hookstage.AllProcessedBidResponses]{
  2564  				{Module: "module-2", Code: "bar", Hook: mockModuleContextHook{key: "all-processed-bid-responses-ctx-2", val: "some-ctx-2"}},
  2565  			},
  2566  		},
  2567  	}
  2568  }
  2569  
  2570  func (e TestWithModuleContextsPlanBuilder) PlanForAuctionResponseStage(_ string, _ *config.Account) hooks.Plan[hookstage.AuctionResponse] {
  2571  	return hooks.Plan[hookstage.AuctionResponse]{
  2572  		hooks.Group[hookstage.AuctionResponse]{
  2573  			Timeout: 1 * time.Millisecond,
  2574  			Hooks: []hooks.HookWrapper[hookstage.AuctionResponse]{
  2575  				{Module: "module-1", Code: "foo", Hook: mockModuleContextHook{key: "auction-response-ctx-1", val: "some-ctx-1"}},
  2576  				{Module: "module-2", Code: "baz", Hook: mockModuleContextHook{key: "auction-response-ctx-2", val: "some-ctx-2"}},
  2577  			},
  2578  		},
  2579  		hooks.Group[hookstage.AuctionResponse]{
  2580  			Timeout: 1 * time.Millisecond,
  2581  			Hooks: []hooks.HookWrapper[hookstage.AuctionResponse]{
  2582  				{Module: "module-1", Code: "bar", Hook: mockModuleContextHook{key: "auction-response-ctx-3", val: "some-ctx-3"}},
  2583  			},
  2584  		},
  2585  	}
  2586  }
  2587  
  2588  type TestAllHookResultsBuilder struct {
  2589  	hooks.EmptyPlanBuilder
  2590  }
  2591  
  2592  func (e TestAllHookResultsBuilder) PlanForEntrypointStage(_ string) hooks.Plan[hookstage.Entrypoint] {
  2593  	return hooks.Plan[hookstage.Entrypoint]{
  2594  		hooks.Group[hookstage.Entrypoint]{
  2595  			Timeout: 10 * time.Millisecond,
  2596  			Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
  2597  				{Module: "module.x-1", Code: "code-1", Hook: mockUpdateHeaderEntrypointHook{}},
  2598  				{Module: "module.x-1", Code: "code-3", Hook: mockTimeoutHook{}},
  2599  				{Module: "module.x-1", Code: "code-4", Hook: mockFailureHook{}},
  2600  				{Module: "module.x-1", Code: "code-5", Hook: mockErrorHook{}},
  2601  				{Module: "module.x-1", Code: "code-6", Hook: mockFailedMutationHook{}},
  2602  				{Module: "module.x-1", Code: "code-7", Hook: mockModuleContextHook{key: "key", val: "val"}},
  2603  			},
  2604  		},
  2605  		// place the reject hook in a separate group because it rejects the stage completely
  2606  		// thus we can not make accurate mock calls if it is processed in parallel with others
  2607  		hooks.Group[hookstage.Entrypoint]{
  2608  			Timeout: 10 * time.Second,
  2609  			Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
  2610  				{Module: "module.x-1", Code: "code-2", Hook: mockRejectHook{}},
  2611  			},
  2612  		},
  2613  	}
  2614  }