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