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