github.com/prebid/prebid-server@v0.275.0/endpoints/openrtb2/amp_auction_test.go (about) 1 package openrtb2 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/http" 9 "net/http/httptest" 10 "net/url" 11 "os" 12 "strconv" 13 "testing" 14 "time" 15 16 "github.com/julienschmidt/httprouter" 17 "github.com/prebid/openrtb/v19/openrtb2" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/require" 20 21 "github.com/prebid/prebid-server/amp" 22 "github.com/prebid/prebid-server/analytics" 23 analyticsConf "github.com/prebid/prebid-server/analytics/config" 24 "github.com/prebid/prebid-server/config" 25 "github.com/prebid/prebid-server/errortypes" 26 "github.com/prebid/prebid-server/exchange" 27 "github.com/prebid/prebid-server/hooks" 28 "github.com/prebid/prebid-server/hooks/hookexecution" 29 "github.com/prebid/prebid-server/hooks/hookstage" 30 "github.com/prebid/prebid-server/metrics" 31 metricsConfig "github.com/prebid/prebid-server/metrics/config" 32 "github.com/prebid/prebid-server/openrtb_ext" 33 "github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher" 34 ) 35 36 // TestGoodRequests makes sure that the auction runs properly-formatted stored bids correctly. 37 func TestGoodAmpRequests(t *testing.T) { 38 testGroups := []struct { 39 desc string 40 dir string 41 testFiles []string 42 }{ 43 { 44 desc: "Valid supplementary, tag_id param only", 45 dir: "sample-requests/amp/valid-supplementary/", 46 testFiles: []string{ 47 "aliased-buyeruids.json", 48 "aliases.json", 49 "imp-with-stored-resp.json", 50 "gdpr-no-consentstring.json", 51 "gdpr.json", 52 }, 53 }, 54 { 55 desc: "Valid, consent handling in query", 56 dir: "sample-requests/amp/consent-through-query/", 57 testFiles: []string{ 58 "addtl-consent-through-query.json", 59 "gdpr-tcf1-consent-through-query.json", 60 "gdpr-tcf2-consent-through-query.json", 61 "gdpr-legacy-tcf2-consent-through-query.json", 62 "gdpr-ccpa-through-query.json", 63 }, 64 }, 65 } 66 67 for _, tgroup := range testGroups { 68 for _, filename := range tgroup.testFiles { 69 // Read test case and unmarshal 70 fileJsonData, err := os.ReadFile(tgroup.dir + filename) 71 if !assert.NoError(t, err, "Failed to fetch a valid request: %v. Test file: %s", err, filename) { 72 continue 73 } 74 75 test := testCase{} 76 if !assert.NoError(t, json.Unmarshal(fileJsonData, &test), "Failed to unmarshal data from file: %s. Error: %v", filename, err) { 77 continue 78 } 79 80 // build http request 81 request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?%s", test.Query), nil) 82 recorder := httptest.NewRecorder() 83 84 // build the stored requests and configure endpoint conf 85 query := request.URL.Query() 86 tagID := query.Get("tag_id") 87 if !assert.Greater(t, len(tagID), 0, "AMP test %s file is missing tag_id field", filename) { 88 continue 89 } 90 91 test.StoredRequest = map[string]json.RawMessage{tagID: test.BidRequest} 92 test.endpointType = AMP_ENDPOINT 93 94 cfg := &config.Configuration{ 95 MaxRequestSize: maxSize, 96 GDPR: config.GDPR{Enabled: true}, 97 } 98 if test.Config != nil { 99 cfg.BlacklistedApps = test.Config.BlacklistedApps 100 cfg.BlacklistedAppMap = test.Config.getBlacklistedAppMap() 101 cfg.BlacklistedAccts = test.Config.BlacklistedAccounts 102 cfg.BlacklistedAcctMap = test.Config.getBlackListedAccountMap() 103 cfg.AccountRequired = test.Config.AccountRequired 104 } 105 106 // Set test up 107 ampEndpoint, ex, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg) 108 if !assert.NoError(t, err) { 109 continue 110 } 111 112 // runTestCase 113 ampEndpoint(recorder, request, nil) 114 115 // Close servers 116 for _, mockBidServer := range mockBidServers { 117 mockBidServer.Close() 118 } 119 mockCurrencyRatesServer.Close() 120 121 // Assertions 122 if assert.Equal(t, test.ExpectedReturnCode, recorder.Code, "Expected status %d. Got %d. Amp test file: %s", http.StatusOK, recorder.Code, filename) { 123 if test.ExpectedReturnCode == http.StatusOK { 124 assert.JSONEq(t, string(test.ExpectedAmpResponse), string(recorder.Body.Bytes()), "Not the expected response. Test file: %s", filename) 125 } else { 126 assert.Equal(t, test.ExpectedErrorMessage, recorder.Body.String(), filename) 127 } 128 } 129 if test.ExpectedValidatedBidReq != nil { 130 // compare as json to ignore whitespace and ext field ordering 131 actualJson, err := json.Marshal(ex.actualValidatedBidReq) 132 if assert.NoError(t, err, "Error converting actual bid request to json. Test file: %s", filename) { 133 assert.JSONEq(t, string(test.ExpectedValidatedBidReq), string(actualJson), "Not the expected validated request. Test file: %s", filename) 134 } 135 } 136 } 137 } 138 } 139 140 func TestAccountErrors(t *testing.T) { 141 tests := []struct { 142 description string 143 storedReqID string 144 filename string 145 }{ 146 { 147 description: "Malformed account config", 148 storedReqID: "1", 149 filename: "account-malformed/malformed-acct.json", 150 }, 151 { 152 description: "Blocked account", 153 storedReqID: "1", 154 filename: "blacklisted/blacklisted-site-publisher.json", 155 }, 156 } 157 158 for _, tt := range tests { 159 fileJsonData, err := os.ReadFile("sample-requests/" + tt.filename) 160 if !assert.NoError(t, err, "Failed to fetch a valid request: %v. Test file: %s", err, tt.filename) { 161 continue 162 } 163 164 test := testCase{} 165 if !assert.NoError(t, json.Unmarshal(fileJsonData, &test), "Failed to unmarshal data from file: %s. Error: %v", tt.filename, err) { 166 continue 167 } 168 test.StoredRequest = map[string]json.RawMessage{tt.storedReqID: test.BidRequest} 169 test.endpointType = AMP_ENDPOINT 170 171 cfg := &config.Configuration{ 172 BlacklistedAccts: []string{"bad_acct"}, 173 BlacklistedAcctMap: map[string]bool{"bad_acct": true}, 174 MaxRequestSize: maxSize, 175 } 176 cfg.MarshalAccountDefaults() 177 178 ampEndpoint, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg) 179 if !assert.NoError(t, err) { 180 continue 181 } 182 183 request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s&", tt.storedReqID), nil) 184 recorder := httptest.NewRecorder() 185 ampEndpoint(recorder, request, nil) 186 187 for _, mockBidServer := range mockBidServers { 188 mockBidServer.Close() 189 } 190 mockCurrencyRatesServer.Close() 191 192 assert.Equal(t, test.ExpectedReturnCode, recorder.Code, "%s: %s", tt.description, tt.filename) 193 assert.Equal(t, test.ExpectedErrorMessage, recorder.Body.String(), "%s: %s", tt.description, tt.filename) 194 } 195 } 196 197 // Prevents #683 198 func TestAMPPageInfo(t *testing.T) { 199 const page = "http://test.somepage.co.uk:1234?myquery=1&other=2" 200 stored := map[string]json.RawMessage{ 201 "1": json.RawMessage(validRequest(t, "site.json")), 202 } 203 exchange := &mockAmpExchange{} 204 205 endpoint, _ := NewAmpEndpoint( 206 fakeUUIDGenerator{}, 207 exchange, 208 newParamsValidator(t), 209 &mockAmpStoredReqFetcher{stored}, 210 empty_fetcher.EmptyFetcher{}, 211 &config.Configuration{MaxRequestSize: maxSize}, 212 &metricsConfig.NilMetricsEngine{}, 213 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 214 map[string]string{}, 215 []byte{}, 216 openrtb_ext.BuildBidderMap(), 217 empty_fetcher.EmptyFetcher{}, 218 hooks.EmptyPlanBuilder{}, 219 nil, 220 ) 221 request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&curl=%s", url.QueryEscape(page)), nil) 222 recorder := httptest.NewRecorder() 223 endpoint(recorder, request, nil) 224 225 if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", recorder.Code, recorder.Body.String()) { 226 return 227 } 228 if !assert.NotNil(t, exchange.lastRequest.Site) { 229 return 230 } 231 assert.Equal(t, page, exchange.lastRequest.Site.Page) 232 assert.Equal(t, "test.somepage.co.uk", exchange.lastRequest.Site.Domain) 233 } 234 235 func TestGDPRConsent(t *testing.T) { 236 consent := "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA" 237 existingConsent := "BONV8oqONXwgmADACHENAO7pqzAAppY" 238 239 testCases := []struct { 240 description string 241 consent string 242 userExt *openrtb_ext.ExtUser 243 nilUser bool 244 expectedUserExt openrtb_ext.ExtUser 245 }{ 246 { 247 description: "Nil User", 248 consent: consent, 249 nilUser: true, 250 expectedUserExt: openrtb_ext.ExtUser{ 251 Consent: consent, 252 }, 253 }, 254 { 255 description: "Nil User Ext", 256 consent: consent, 257 userExt: nil, 258 expectedUserExt: openrtb_ext.ExtUser{ 259 Consent: consent, 260 }, 261 }, 262 { 263 description: "Overrides Existing Consent", 264 consent: consent, 265 userExt: &openrtb_ext.ExtUser{ 266 Consent: existingConsent, 267 }, 268 expectedUserExt: openrtb_ext.ExtUser{ 269 Consent: consent, 270 }, 271 }, 272 { 273 description: "Overrides Existing Consent - With Sibling Data", 274 consent: consent, 275 userExt: &openrtb_ext.ExtUser{ 276 Consent: existingConsent, 277 }, 278 expectedUserExt: openrtb_ext.ExtUser{ 279 Consent: consent, 280 }, 281 }, 282 { 283 description: "Does Not Override Existing Consent If Empty", 284 consent: "", 285 userExt: &openrtb_ext.ExtUser{ 286 Consent: existingConsent, 287 }, 288 expectedUserExt: openrtb_ext.ExtUser{ 289 Consent: existingConsent, 290 }, 291 }, 292 } 293 294 for _, test := range testCases { 295 // Build Request 296 bid, err := getTestBidRequest(test.nilUser, test.userExt, true, nil) 297 if err != nil { 298 t.Fatalf("Failed to marshal the complete openrtb2.BidRequest object %v", err) 299 } 300 301 // Simulated Stored Request Backend 302 stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} 303 304 // Build Exchange Endpoint 305 mockExchange := &mockAmpExchange{} 306 endpoint, _ := NewAmpEndpoint( 307 fakeUUIDGenerator{}, 308 mockExchange, 309 newParamsValidator(t), 310 &mockAmpStoredReqFetcher{stored}, 311 empty_fetcher.EmptyFetcher{}, 312 &config.Configuration{ 313 MaxRequestSize: maxSize, 314 GDPR: config.GDPR{Enabled: true}, 315 }, 316 &metricsConfig.NilMetricsEngine{}, 317 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 318 map[string]string{}, 319 []byte{}, 320 openrtb_ext.BuildBidderMap(), 321 empty_fetcher.EmptyFetcher{}, 322 hooks.EmptyPlanBuilder{}, 323 nil, 324 ) 325 326 // Invoke Endpoint 327 request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&consent_type=2&consent_string=%s", test.consent), nil) 328 responseRecorder := httptest.NewRecorder() 329 endpoint(responseRecorder, request, nil) 330 331 // Parse Response 332 var response AmpResponse 333 if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { 334 t.Fatalf("Error unmarshalling response: %s", err.Error()) 335 } 336 337 // Assert Result 338 result := mockExchange.lastRequest 339 if !assert.NotNil(t, result, test.description+":lastRequest") { 340 return 341 } 342 if !assert.NotNil(t, result.User, test.description+":lastRequest.User") { 343 return 344 } 345 if !assert.NotNil(t, result.User.Ext, test.description+":lastRequest.User.Ext") { 346 return 347 } 348 var ue openrtb_ext.ExtUser 349 err = json.Unmarshal(result.User.Ext, &ue) 350 if !assert.NoError(t, err, test.description+":deserialize") { 351 return 352 } 353 assert.Equal(t, test.expectedUserExt, ue, test.description) 354 assert.Equal(t, expectedErrorsFromHoldAuction, response.ORTB2.Ext.Errors, test.description+":errors") 355 assert.Empty(t, response.ORTB2.Ext.Warnings, test.description+":warnings") 356 357 // Invoke Endpoint With Legacy Param 358 requestLegacy := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&consent_type=2&gdpr_consent=%s", test.consent), nil) 359 responseRecorderLegacy := httptest.NewRecorder() 360 endpoint(responseRecorderLegacy, requestLegacy, nil) 361 362 // Parse Resonse 363 var responseLegacy AmpResponse 364 if err := json.Unmarshal(responseRecorderLegacy.Body.Bytes(), &responseLegacy); err != nil { 365 t.Fatalf("Error unmarshalling response: %s", err.Error()) 366 } 367 368 // Assert Result With Legacy Param 369 resultLegacy := mockExchange.lastRequest 370 if !assert.NotNil(t, resultLegacy, test.description+":legacy:lastRequest") { 371 return 372 } 373 if !assert.NotNil(t, resultLegacy.User, test.description+":legacy:lastRequest.User") { 374 return 375 } 376 if !assert.NotNil(t, resultLegacy.User.Ext, test.description+":legacy:lastRequest.User.Ext") { 377 return 378 } 379 var ueLegacy openrtb_ext.ExtUser 380 err = json.Unmarshal(resultLegacy.User.Ext, &ueLegacy) 381 if !assert.NoError(t, err, test.description+":legacy:deserialize") { 382 return 383 } 384 assert.Equal(t, test.expectedUserExt, ueLegacy, test.description+":legacy") 385 assert.Equal(t, expectedErrorsFromHoldAuction, responseLegacy.ORTB2.Ext.Errors, test.description+":legacy:errors") 386 assert.Empty(t, responseLegacy.ORTB2.Ext.Warnings, test.description+":legacy:warnings") 387 } 388 } 389 390 func TestOverrideWithParams(t *testing.T) { 391 e := &endpointDeps{ 392 cfg: &config.Configuration{ 393 GDPR: config.GDPR{ 394 Enabled: true, 395 }, 396 }, 397 } 398 399 type testInput struct { 400 ampParams amp.Params 401 bidRequest *openrtb2.BidRequest 402 } 403 type testOutput struct { 404 bidRequest *openrtb2.BidRequest 405 errorMsgs []string 406 expectFatalErrors bool 407 } 408 testCases := []struct { 409 desc string 410 given testInput 411 expected testOutput 412 }{ 413 { 414 desc: "bid request with no Site field - amp.Params empty - expect Site to be added", 415 given: testInput{ 416 ampParams: amp.Params{}, 417 bidRequest: &openrtb2.BidRequest{ 418 Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, 419 }, 420 }, 421 expected: testOutput{ 422 bidRequest: &openrtb2.BidRequest{ 423 Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, 424 Site: &openrtb2.Site{Ext: json.RawMessage(`{"amp":1}`)}, 425 }, 426 errorMsgs: nil, 427 }, 428 }, 429 { 430 desc: "amp.Params with Size field - expect Site and Banner format fields to be added", 431 given: testInput{ 432 ampParams: amp.Params{ 433 Size: amp.Size{ 434 Width: 480, 435 Height: 320, 436 }, 437 }, 438 bidRequest: &openrtb2.BidRequest{ 439 Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, 440 }, 441 }, 442 expected: testOutput{ 443 bidRequest: &openrtb2.BidRequest{ 444 Imp: []openrtb2.Imp{ 445 { 446 Banner: &openrtb2.Banner{ 447 Format: []openrtb2.Format{ 448 { 449 W: 480, 450 H: 320, 451 }, 452 }, 453 }, 454 }, 455 }, 456 Site: &openrtb2.Site{Ext: json.RawMessage(`{"amp":1}`)}, 457 }, 458 errorMsgs: nil, 459 }, 460 }, 461 { 462 desc: "amp.Params with CanonicalURL field - expect Site to be aded with Page and Domain fields", 463 given: testInput{ 464 ampParams: amp.Params{CanonicalURL: "http://www.foobar.com"}, 465 bidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}}, 466 }, 467 expected: testOutput{ 468 bidRequest: &openrtb2.BidRequest{ 469 Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, 470 Site: &openrtb2.Site{ 471 Page: "http://www.foobar.com", 472 Domain: "www.foobar.com", 473 Ext: json.RawMessage(`{"amp":1}`), 474 }, 475 }, 476 errorMsgs: nil, 477 }, 478 }, 479 { 480 desc: "amp.Params with Trace field - expect ext.prebid.trace to be added", 481 given: testInput{ 482 ampParams: amp.Params{Trace: "verbose"}, 483 bidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}}, 484 }, 485 expected: testOutput{ 486 bidRequest: &openrtb2.BidRequest{ 487 Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, 488 Site: &openrtb2.Site{Ext: json.RawMessage(`{"amp":1}`)}, 489 Ext: json.RawMessage(`{"prebid":{"trace":"verbose"}}`), 490 }, 491 errorMsgs: nil, 492 }, 493 }, 494 { 495 desc: "amp.Params with Trace field - expect ext.prebid.trace to be merged with existing ext fields", 496 given: testInput{ 497 ampParams: amp.Params{Trace: "verbose"}, 498 bidRequest: &openrtb2.BidRequest{ 499 Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, 500 Ext: json.RawMessage(`{"prebid":{"debug":true}}`), 501 }, 502 }, 503 expected: testOutput{ 504 bidRequest: &openrtb2.BidRequest{ 505 Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, 506 Site: &openrtb2.Site{Ext: json.RawMessage(`{"amp":1}`)}, 507 Ext: json.RawMessage(`{"prebid":{"debug":true,"trace":"verbose"}}`), 508 }, 509 errorMsgs: nil, 510 }, 511 }, 512 { 513 desc: "bid request with malformed User.Ext - amp.Params with AdditionalConsent - expect error", 514 given: testInput{ 515 ampParams: amp.Params{AdditionalConsent: "1~X.X.X.X"}, 516 bidRequest: &openrtb2.BidRequest{ 517 Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, 518 User: &openrtb2.User{Ext: json.RawMessage(`malformed`)}, 519 }, 520 }, 521 expected: testOutput{ 522 bidRequest: &openrtb2.BidRequest{ 523 Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, 524 Site: &openrtb2.Site{Ext: json.RawMessage(`{"amp":1}`)}, 525 User: &openrtb2.User{Ext: json.RawMessage(`malformed`)}, 526 }, 527 errorMsgs: []string{"invalid character 'm' looking for beginning of value"}, 528 expectFatalErrors: true, 529 }, 530 }, 531 { 532 desc: "bid request with valid imp[0].ext - amp.Params with malformed targeting value - expect error because imp[0].ext won't be unable to get merged with targeting values", 533 given: testInput{ 534 ampParams: amp.Params{Targeting: "{123,}"}, 535 bidRequest: &openrtb2.BidRequest{ 536 Imp: []openrtb2.Imp{ 537 { 538 Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}, 539 Ext: []byte(`{"appnexus":{"placementId":123}}`), 540 }, 541 }, 542 }, 543 }, 544 expected: testOutput{ 545 bidRequest: &openrtb2.BidRequest{ 546 Imp: []openrtb2.Imp{ 547 { 548 Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}, 549 Ext: json.RawMessage(`{"appnexus":{"placementId":123}}`), 550 }, 551 }, 552 Site: &openrtb2.Site{Ext: json.RawMessage(`{"amp":1}`)}, 553 }, 554 errorMsgs: []string{"unable to merge imp.ext with targeting data, check targeting data is correct: Invalid JSON Patch"}, 555 }, 556 }, 557 { 558 desc: "bid request with malformed user.ext.prebid - amp.Params with GDPR consent values - expect policy writer to return error", 559 given: testInput{ 560 ampParams: amp.Params{ 561 ConsentType: amp.ConsentTCF2, 562 Consent: "CPdECS0PdECS0ACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", 563 }, 564 bidRequest: &openrtb2.BidRequest{ 565 Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, 566 User: &openrtb2.User{Ext: json.RawMessage(`{"prebid":{malformed}}`)}, 567 }, 568 }, 569 expected: testOutput{ 570 bidRequest: &openrtb2.BidRequest{ 571 Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}, 572 User: &openrtb2.User{Ext: json.RawMessage(`{"prebid":{malformed}}`)}, 573 Site: &openrtb2.Site{Ext: json.RawMessage(`{"amp":1}`)}, 574 }, 575 errorMsgs: []string{"invalid character 'm' looking for beginning of object key string"}, 576 expectFatalErrors: true, 577 }, 578 }, 579 } 580 581 for _, test := range testCases { 582 errs := e.overrideWithParams(test.given.ampParams, test.given.bidRequest) 583 584 assert.Equal(t, test.expected.bidRequest, test.given.bidRequest, test.desc) 585 assert.Len(t, errs, len(test.expected.errorMsgs), test.desc) 586 if len(test.expected.errorMsgs) > 0 { 587 assert.Equal(t, test.expected.errorMsgs[0], errs[0].Error(), test.desc) 588 assert.Equal(t, test.expected.expectFatalErrors, errortypes.ContainsFatalError(errs), test.desc) 589 } 590 } 591 } 592 593 func TestSetConsentedProviders(t *testing.T) { 594 595 sampleBidRequest := &openrtb2.BidRequest{} 596 597 testCases := []struct { 598 description string 599 givenAdditionalConsent string 600 givenBidRequest *openrtb2.BidRequest 601 expectedBidRequest *openrtb2.BidRequest 602 expectedError bool 603 }{ 604 { 605 description: "empty additional consent bid request unmodified", 606 givenAdditionalConsent: "", 607 givenBidRequest: sampleBidRequest, 608 expectedBidRequest: sampleBidRequest, 609 expectedError: false, 610 }, 611 { 612 description: "nil bid request, expect error", 613 givenAdditionalConsent: "ADDITIONAL_CONSENT_STRING", 614 givenBidRequest: nil, 615 expectedBidRequest: nil, 616 expectedError: true, 617 }, 618 { 619 description: "malformed user.ext, expect error", 620 givenAdditionalConsent: "ADDITIONAL_CONSENT_STRING", 621 givenBidRequest: &openrtb2.BidRequest{ 622 User: &openrtb2.User{ 623 Ext: json.RawMessage(`malformed`), 624 }, 625 }, 626 expectedBidRequest: &openrtb2.BidRequest{ 627 User: &openrtb2.User{ 628 Ext: json.RawMessage(`malformed`), 629 }, 630 }, 631 expectedError: true, 632 }, 633 { 634 description: "non-empty additional consent bid request will carry this value in user.ext.ConsentedProvidersSettings.consented_providers", 635 givenAdditionalConsent: "ADDITIONAL_CONSENT_STRING", 636 givenBidRequest: sampleBidRequest, 637 expectedBidRequest: &openrtb2.BidRequest{ 638 User: &openrtb2.User{ 639 Ext: json.RawMessage(`{"ConsentedProvidersSettings":{"consented_providers":"ADDITIONAL_CONSENT_STRING"}}`), 640 }, 641 }, 642 expectedError: false, 643 }, 644 } 645 646 for _, test := range testCases { 647 err := setConsentedProviders(test.givenBidRequest, amp.Params{AdditionalConsent: test.givenAdditionalConsent}) 648 649 if test.expectedError { 650 assert.Error(t, err, test.description) 651 } else { 652 assert.NoError(t, err, test.description) 653 } 654 assert.Equal(t, test.expectedBidRequest, test.givenBidRequest, test.description) 655 } 656 } 657 658 func TestCCPAConsent(t *testing.T) { 659 consent := "1NYN" 660 existingConsent := "1NNN" 661 662 var gdpr int8 = 1 663 664 testCases := []struct { 665 description string 666 consent string 667 regsExt *openrtb_ext.ExtRegs 668 nilRegs bool 669 expectedRegExt openrtb_ext.ExtRegs 670 }{ 671 { 672 description: "Nil Regs", 673 consent: consent, 674 nilRegs: true, 675 expectedRegExt: openrtb_ext.ExtRegs{ 676 USPrivacy: consent, 677 }, 678 }, 679 { 680 description: "Nil Regs Ext", 681 consent: consent, 682 regsExt: nil, 683 expectedRegExt: openrtb_ext.ExtRegs{ 684 USPrivacy: consent, 685 }, 686 }, 687 { 688 description: "Overrides Existing Consent", 689 consent: consent, 690 regsExt: &openrtb_ext.ExtRegs{ 691 USPrivacy: existingConsent, 692 }, 693 expectedRegExt: openrtb_ext.ExtRegs{ 694 USPrivacy: consent, 695 }, 696 }, 697 { 698 description: "Overrides Existing Consent - With Sibling Data", 699 consent: consent, 700 regsExt: &openrtb_ext.ExtRegs{ 701 USPrivacy: existingConsent, 702 GDPR: &gdpr, 703 }, 704 expectedRegExt: openrtb_ext.ExtRegs{ 705 USPrivacy: consent, 706 GDPR: &gdpr, 707 }, 708 }, 709 { 710 description: "Does Not Override Existing Consent If Empty", 711 consent: "", 712 regsExt: &openrtb_ext.ExtRegs{ 713 USPrivacy: existingConsent, 714 }, 715 expectedRegExt: openrtb_ext.ExtRegs{ 716 USPrivacy: existingConsent, 717 }, 718 }, 719 } 720 721 for _, test := range testCases { 722 // Build Request 723 bid, err := getTestBidRequest(true, nil, test.nilRegs, test.regsExt) 724 if err != nil { 725 t.Fatalf("Failed to marshal the complete openrtb2.BidRequest object %v", err) 726 } 727 728 // Simulated Stored Request Backend 729 stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} 730 731 // Build Exchange Endpoint 732 mockExchange := &mockAmpExchange{} 733 endpoint, _ := NewAmpEndpoint( 734 fakeUUIDGenerator{}, 735 mockExchange, 736 newParamsValidator(t), 737 &mockAmpStoredReqFetcher{stored}, 738 empty_fetcher.EmptyFetcher{}, 739 &config.Configuration{MaxRequestSize: maxSize}, 740 &metricsConfig.NilMetricsEngine{}, 741 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 742 map[string]string{}, 743 []byte{}, 744 openrtb_ext.BuildBidderMap(), 745 empty_fetcher.EmptyFetcher{}, 746 hooks.EmptyPlanBuilder{}, 747 nil, 748 ) 749 750 // Invoke Endpoint 751 request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&consent_type=3&consent_string=%s", test.consent), nil) 752 responseRecorder := httptest.NewRecorder() 753 endpoint(responseRecorder, request, nil) 754 755 // Parse Response 756 var response AmpResponse 757 if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { 758 t.Fatalf("Error unmarshalling response: %s", err.Error()) 759 } 760 761 // Assert Result 762 result := mockExchange.lastRequest 763 if !assert.NotNil(t, result, test.description+":lastRequest") { 764 return 765 } 766 if !assert.NotNil(t, result.Regs, test.description+":lastRequest.Regs") { 767 return 768 } 769 if !assert.NotNil(t, result.Regs.Ext, test.description+":lastRequest.Regs.Ext") { 770 return 771 } 772 var re openrtb_ext.ExtRegs 773 err = json.Unmarshal(result.Regs.Ext, &re) 774 if !assert.NoError(t, err, test.description+":deserialize") { 775 return 776 } 777 assert.Equal(t, test.expectedRegExt, re, test.description) 778 assert.Equal(t, expectedErrorsFromHoldAuction, response.ORTB2.Ext.Errors) 779 assert.Empty(t, response.ORTB2.Ext.Warnings) 780 } 781 } 782 783 func TestConsentWarnings(t *testing.T) { 784 type inputTest struct { 785 regs *openrtb_ext.ExtRegs 786 invalidConsentURL bool 787 expectedWarnings map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage 788 } 789 invalidConsent := "invalid" 790 791 bidderWarning := openrtb_ext.ExtBidderMessage{ 792 Code: 10003, 793 Message: "debug turned off for bidder", 794 } 795 invalidCCPAWarning := openrtb_ext.ExtBidderMessage{ 796 Code: 10001, 797 Message: "Consent string '" + invalidConsent + "' is not a valid CCPA consent string.", 798 } 799 invalidConsentWarning := openrtb_ext.ExtBidderMessage{ 800 Code: 10001, 801 Message: "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)", 802 } 803 804 testData := []inputTest{ 805 { 806 regs: nil, 807 invalidConsentURL: false, 808 expectedWarnings: nil, 809 }, 810 { 811 regs: nil, 812 invalidConsentURL: true, 813 expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{openrtb_ext.BidderReservedGeneral: {invalidCCPAWarning}}, 814 }, 815 { 816 regs: &openrtb_ext.ExtRegs{USPrivacy: "invalid"}, 817 invalidConsentURL: true, 818 expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{ 819 openrtb_ext.BidderReservedGeneral: {invalidCCPAWarning, invalidConsentWarning}, 820 openrtb_ext.BidderName("appnexus"): {bidderWarning}, 821 }, 822 }, 823 { 824 regs: &openrtb_ext.ExtRegs{USPrivacy: "1NYN"}, 825 invalidConsentURL: false, 826 expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{openrtb_ext.BidderName("appnexus"): {bidderWarning}}, 827 }, 828 } 829 830 for _, testCase := range testData { 831 832 bid, err := getTestBidRequest(true, nil, testCase.regs == nil, testCase.regs) 833 if err != nil { 834 t.Fatalf("Failed to marshal the complete openrtb2.BidRequest object %v", err) 835 } 836 837 // Simulated Stored Request Backend 838 stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} 839 840 // Build Exchange Endpoint 841 var mockExchange exchange.Exchange 842 if testCase.regs != nil { 843 mockExchange = &mockAmpExchangeWarnings{} 844 } else { 845 mockExchange = &mockAmpExchange{} 846 } 847 endpoint, _ := NewAmpEndpoint( 848 fakeUUIDGenerator{}, 849 mockExchange, 850 newParamsValidator(t), 851 &mockAmpStoredReqFetcher{stored}, 852 empty_fetcher.EmptyFetcher{}, 853 &config.Configuration{MaxRequestSize: maxSize}, 854 &metricsConfig.NilMetricsEngine{}, 855 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 856 map[string]string{}, 857 []byte{}, 858 openrtb_ext.BuildBidderMap(), 859 empty_fetcher.EmptyFetcher{}, 860 hooks.EmptyPlanBuilder{}, 861 nil, 862 ) 863 864 // Invoke Endpoint 865 var request *http.Request 866 867 if testCase.invalidConsentURL { 868 request = httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1&consent_type=3&consent_string="+invalidConsent, nil) 869 870 } else { 871 request = httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1", nil) 872 } 873 874 responseRecorder := httptest.NewRecorder() 875 endpoint(responseRecorder, request, nil) 876 877 // Parse Response 878 var response AmpResponse 879 if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { 880 t.Fatalf("Error unmarshalling response: %s", err.Error()) 881 } 882 883 // Assert Result 884 if testCase.regs == nil { 885 result := mockExchange.(*mockAmpExchange).lastRequest 886 assert.NotNil(t, result, "lastRequest") 887 assert.Nil(t, result.User, "lastRequest.User") 888 assert.Nil(t, result.Regs, "lastRequest.Regs") 889 assert.Equal(t, expectedErrorsFromHoldAuction, response.ORTB2.Ext.Errors) 890 if testCase.invalidConsentURL { 891 assert.Equal(t, testCase.expectedWarnings, response.ORTB2.Ext.Warnings) 892 } else { 893 assert.Empty(t, response.ORTB2.Ext.Warnings) 894 } 895 896 } else { 897 assert.Equal(t, testCase.expectedWarnings, response.ORTB2.Ext.Warnings) 898 } 899 } 900 } 901 902 func TestNewAndLegacyConsentBothProvided(t *testing.T) { 903 validConsentGDPR1 := "COwGVJOOwGVJOADACHENAOCAAO6as_-AAAhoAFNLAAoAAAA" 904 validConsentGDPR2 := "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA" 905 906 testCases := []struct { 907 description string 908 consent string 909 consentLegacy string 910 userExt *openrtb_ext.ExtUser 911 expectedUserExt openrtb_ext.ExtUser 912 }{ 913 { 914 description: "New Consent Wins", 915 consent: validConsentGDPR1, 916 consentLegacy: validConsentGDPR2, 917 expectedUserExt: openrtb_ext.ExtUser{ 918 Consent: validConsentGDPR1, 919 }, 920 }, 921 { 922 description: "New Consent Wins - Reverse", 923 consent: validConsentGDPR2, 924 consentLegacy: validConsentGDPR1, 925 expectedUserExt: openrtb_ext.ExtUser{ 926 Consent: validConsentGDPR2, 927 }, 928 }, 929 } 930 931 for _, test := range testCases { 932 // Build Request 933 bid, err := getTestBidRequest(false, nil, true, nil) 934 if err != nil { 935 t.Fatalf("Failed to marshal the complete openrtb2.BidRequest object %v", err) 936 } 937 938 // Simulated Stored Request Backend 939 stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} 940 941 // Build Exchange Endpoint 942 mockExchange := &mockAmpExchange{} 943 endpoint, _ := NewAmpEndpoint( 944 fakeUUIDGenerator{}, 945 mockExchange, 946 newParamsValidator(t), 947 &mockAmpStoredReqFetcher{stored}, 948 empty_fetcher.EmptyFetcher{}, 949 &config.Configuration{ 950 MaxRequestSize: maxSize, 951 GDPR: config.GDPR{Enabled: true}, 952 }, 953 &metricsConfig.NilMetricsEngine{}, 954 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 955 map[string]string{}, 956 []byte{}, 957 openrtb_ext.BuildBidderMap(), 958 empty_fetcher.EmptyFetcher{}, 959 hooks.EmptyPlanBuilder{}, 960 nil, 961 ) 962 963 // Invoke Endpoint 964 request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&consent_type=2&consent_string=%s&gdpr_consent=%s", test.consent, test.consentLegacy), nil) 965 responseRecorder := httptest.NewRecorder() 966 endpoint(responseRecorder, request, nil) 967 968 // Parse Response 969 var response AmpResponse 970 if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { 971 t.Fatalf("Error unmarshalling response: %s", err.Error()) 972 } 973 974 // Assert Result 975 result := mockExchange.lastRequest 976 if !assert.NotNil(t, result, test.description+":lastRequest") { 977 return 978 } 979 if !assert.NotNil(t, result.User, test.description+":lastRequest.User") { 980 return 981 } 982 if !assert.NotNil(t, result.User.Ext, test.description+":lastRequest.User.Ext") { 983 return 984 } 985 var ue openrtb_ext.ExtUser 986 err = json.Unmarshal(result.User.Ext, &ue) 987 if !assert.NoError(t, err, test.description+":deserialize") { 988 return 989 } 990 assert.Equal(t, test.expectedUserExt, ue, test.description) 991 assert.Equal(t, expectedErrorsFromHoldAuction, response.ORTB2.Ext.Errors) 992 assert.Empty(t, response.ORTB2.Ext.Warnings) 993 } 994 } 995 996 func TestAMPSiteExt(t *testing.T) { 997 stored := map[string]json.RawMessage{ 998 "1": json.RawMessage(validRequest(t, "site.json")), 999 } 1000 exchange := &mockAmpExchange{} 1001 endpoint, _ := NewAmpEndpoint( 1002 fakeUUIDGenerator{}, 1003 exchange, 1004 newParamsValidator(t), 1005 &mockAmpStoredReqFetcher{stored}, 1006 empty_fetcher.EmptyFetcher{}, 1007 &config.Configuration{MaxRequestSize: maxSize}, 1008 &metricsConfig.NilMetricsEngine{}, 1009 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 1010 nil, 1011 nil, 1012 openrtb_ext.BuildBidderMap(), 1013 empty_fetcher.EmptyFetcher{}, 1014 hooks.EmptyPlanBuilder{}, 1015 nil, 1016 ) 1017 request, err := http.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1", nil) 1018 if !assert.NoError(t, err) { 1019 return 1020 } 1021 recorder := httptest.NewRecorder() 1022 endpoint(recorder, request, nil) 1023 1024 if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", recorder.Code, recorder.Body.String()) { 1025 return 1026 } 1027 if !assert.NotNil(t, exchange.lastRequest.Site) { 1028 return 1029 } 1030 assert.JSONEq(t, `{"amp":1}`, string(exchange.lastRequest.Site.Ext)) 1031 } 1032 1033 // TestBadRequests makes sure we return 400's on bad requests. 1034 func TestAmpBadRequests(t *testing.T) { 1035 dir := "sample-requests/invalid-whole" 1036 files, err := os.ReadDir(dir) 1037 assert.NoError(t, err, "Failed to read folder: %s", dir) 1038 1039 badRequests := make(map[string]json.RawMessage, len(files)) 1040 for index, file := range files { 1041 badRequests[strconv.Itoa(100+index)] = readFile(t, "sample-requests/invalid-whole/"+file.Name()) 1042 } 1043 1044 endpoint, _ := NewAmpEndpoint( 1045 fakeUUIDGenerator{}, 1046 &mockAmpExchange{}, 1047 newParamsValidator(t), 1048 &mockAmpStoredReqFetcher{badRequests}, 1049 empty_fetcher.EmptyFetcher{}, 1050 &config.Configuration{MaxRequestSize: maxSize}, 1051 &metricsConfig.NilMetricsEngine{}, 1052 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 1053 map[string]string{}, 1054 []byte{}, 1055 openrtb_ext.BuildBidderMap(), 1056 empty_fetcher.EmptyFetcher{}, 1057 hooks.EmptyPlanBuilder{}, 1058 nil, 1059 ) 1060 for requestID := range badRequests { 1061 request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s", requestID), nil) 1062 recorder := httptest.NewRecorder() 1063 1064 endpoint(recorder, request, nil) 1065 1066 if recorder.Code != http.StatusBadRequest { 1067 t.Errorf("Expected status %d. Got %d. Input was: %s", http.StatusBadRequest, recorder.Code, fmt.Sprintf("/openrtb2/auction/amp?config=%s", requestID)) 1068 } 1069 } 1070 } 1071 1072 // TestAmpDebug makes sure we get debug information back when requested 1073 func TestAmpDebug(t *testing.T) { 1074 requests := map[string]json.RawMessage{ 1075 "2": json.RawMessage(validRequest(t, "site.json")), 1076 } 1077 1078 endpoint, _ := NewAmpEndpoint( 1079 fakeUUIDGenerator{}, 1080 &mockAmpExchange{}, 1081 newParamsValidator(t), 1082 &mockAmpStoredReqFetcher{requests}, 1083 empty_fetcher.EmptyFetcher{}, 1084 &config.Configuration{MaxRequestSize: maxSize}, 1085 &metricsConfig.NilMetricsEngine{}, 1086 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 1087 map[string]string{}, 1088 []byte{}, 1089 openrtb_ext.BuildBidderMap(), 1090 empty_fetcher.EmptyFetcher{}, 1091 hooks.EmptyPlanBuilder{}, 1092 nil, 1093 ) 1094 1095 for requestID := range requests { 1096 request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s&debug=1", requestID), nil) 1097 recorder := httptest.NewRecorder() 1098 endpoint(recorder, request, nil) 1099 1100 if recorder.Code != http.StatusOK { 1101 t.Errorf("Expected status %d. Got %d. Request config ID was %s", http.StatusOK, recorder.Code, requestID) 1102 t.Errorf("Response body was: %s", recorder.Body) 1103 t.Errorf("Request was: %s", string(requests[requestID])) 1104 } 1105 1106 var response AmpResponse 1107 if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { 1108 t.Fatalf("Error unmarshalling response: %s", err.Error()) 1109 } 1110 1111 if response.Targeting == nil || len(response.Targeting) == 0 { 1112 t.Errorf("Bad response, no targeting data.\n Response was: %v", recorder.Body) 1113 } 1114 if len(response.Targeting) != 3 { 1115 t.Errorf("Bad targeting data. Expected 3 keys, got %d.", len(response.Targeting)) 1116 } 1117 1118 if response.ORTB2.Ext.Debug == nil { 1119 t.Errorf("Debug requested but not present") 1120 } 1121 } 1122 } 1123 1124 func TestInitAmpTargetingAndCache(t *testing.T) { 1125 trueVal := true 1126 emptyTargetingAndCache := &openrtb_ext.ExtRequestPrebid{ 1127 Targeting: &openrtb_ext.ExtRequestTargeting{}, 1128 Cache: &openrtb_ext.ExtRequestPrebidCache{ 1129 Bids: &openrtb_ext.ExtRequestPrebidCacheBids{}, 1130 }, 1131 } 1132 1133 testCases := []struct { 1134 name string 1135 request *openrtb2.BidRequest 1136 expectedPrebid *openrtb_ext.ExtRequestPrebid 1137 expectedErrs []string 1138 }{ 1139 { 1140 name: "malformed", 1141 request: &openrtb2.BidRequest{Ext: json.RawMessage("malformed")}, 1142 expectedErrs: []string{"invalid character 'm' looking for beginning of value"}, 1143 }, 1144 { 1145 name: "nil", 1146 request: &openrtb2.BidRequest{}, 1147 expectedPrebid: emptyTargetingAndCache, 1148 }, 1149 { 1150 name: "empty", 1151 request: &openrtb2.BidRequest{Ext: json.RawMessage(`{"ext":{}}`)}, 1152 expectedPrebid: emptyTargetingAndCache, 1153 }, 1154 { 1155 name: "missing targeting + cache", 1156 request: &openrtb2.BidRequest{Ext: json.RawMessage(`{"ext":{"prebid":{}}}`)}, 1157 expectedPrebid: emptyTargetingAndCache, 1158 }, 1159 { 1160 name: "missing targeting", 1161 request: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":true}}}}`)}, 1162 expectedPrebid: &openrtb_ext.ExtRequestPrebid{ 1163 Targeting: &openrtb_ext.ExtRequestTargeting{}, 1164 Cache: &openrtb_ext.ExtRequestPrebidCache{ 1165 Bids: &openrtb_ext.ExtRequestPrebidCacheBids{ 1166 ReturnCreative: &trueVal, 1167 }, 1168 }, 1169 }, 1170 }, 1171 { 1172 name: "missing cache", 1173 request: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"targeting":{"includewinners":true}}}`)}, 1174 expectedPrebid: &openrtb_ext.ExtRequestPrebid{ 1175 Targeting: &openrtb_ext.ExtRequestTargeting{ 1176 IncludeWinners: &trueVal, 1177 }, 1178 Cache: &openrtb_ext.ExtRequestPrebidCache{ 1179 Bids: &openrtb_ext.ExtRequestPrebidCacheBids{}, 1180 }, 1181 }, 1182 }, 1183 } 1184 1185 for _, tc := range testCases { 1186 t.Run(tc.name, func(t *testing.T) { 1187 // setup 1188 req := &openrtb_ext.RequestWrapper{BidRequest: tc.request} 1189 1190 // run 1191 actualErrs := initAmpTargetingAndCache(req) 1192 1193 // assertions 1194 require.NoError(t, req.RebuildRequest(), "rebuild request") 1195 1196 actualErrsMsgs := make([]string, len(actualErrs)) 1197 for i, v := range actualErrs { 1198 actualErrsMsgs[i] = v.Error() 1199 } 1200 assert.ElementsMatch(t, tc.expectedErrs, actualErrsMsgs, "errors") 1201 1202 actualReqExt, _ := req.GetRequestExt() 1203 actualPrebid := actualReqExt.GetPrebid() 1204 assert.Equal(t, tc.expectedPrebid, actualPrebid, "prebid ext") 1205 }) 1206 } 1207 } 1208 1209 func TestQueryParamOverrides(t *testing.T) { 1210 requests := map[string]json.RawMessage{ 1211 "1": json.RawMessage(validRequest(t, "site.json")), 1212 } 1213 1214 endpoint, _ := NewAmpEndpoint( 1215 fakeUUIDGenerator{}, 1216 &mockAmpExchange{}, 1217 newParamsValidator(t), 1218 &mockAmpStoredReqFetcher{requests}, 1219 empty_fetcher.EmptyFetcher{}, 1220 &config.Configuration{MaxRequestSize: maxSize}, 1221 &metricsConfig.NilMetricsEngine{}, 1222 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 1223 map[string]string{}, 1224 []byte{}, 1225 openrtb_ext.BuildBidderMap(), 1226 empty_fetcher.EmptyFetcher{}, 1227 hooks.EmptyPlanBuilder{}, 1228 nil, 1229 ) 1230 1231 requestID := "1" 1232 curl := "http://example.com" 1233 slot := "1234" 1234 timeout := int64(500) 1235 account := "12345" 1236 1237 request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s&debug=1&curl=%s&slot=%s&timeout=%d&account=%s", requestID, curl, slot, timeout, account), nil) 1238 recorder := httptest.NewRecorder() 1239 endpoint(recorder, request, nil) 1240 1241 if recorder.Code != http.StatusOK { 1242 t.Errorf("Expected status %d. Got %d. Request config ID was %s", http.StatusOK, recorder.Code, requestID) 1243 t.Errorf("Response body was: %s", recorder.Body) 1244 t.Errorf("Request was: %s", string(requests[requestID])) 1245 } 1246 1247 var response AmpResponse 1248 if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { 1249 t.Fatalf("Error unmarshalling response: %s", err.Error()) 1250 } 1251 1252 var resolvedRequest openrtb2.BidRequest 1253 err := json.Unmarshal(response.ORTB2.Ext.Debug.ResolvedRequest, &resolvedRequest) 1254 assert.NoError(t, err, "resolved request should have a correct format") 1255 if resolvedRequest.TMax != timeout { 1256 t.Errorf("Expected TMax to equal timeout (%d), got: %d", timeout, resolvedRequest.TMax) 1257 } 1258 1259 resolvedImp := resolvedRequest.Imp[0] 1260 if resolvedImp.TagID != slot { 1261 t.Errorf("Expected Imp.TagId to equal slot (%s), got: %s", slot, resolvedImp.TagID) 1262 } 1263 1264 if resolvedRequest.Site == nil || resolvedRequest.Site.Page != curl { 1265 t.Errorf("Expected Site.Page to equal curl (%s), got: %s", curl, resolvedRequest.Site.Page) 1266 } 1267 1268 if resolvedRequest.Site == nil || resolvedRequest.Site.Publisher == nil || resolvedRequest.Site.Publisher.ID != account { 1269 t.Errorf("Expected Site.Publisher.ID to equal (%s), got: %s", account, resolvedRequest.Site.Publisher.ID) 1270 } 1271 } 1272 1273 func TestOverrideDimensions(t *testing.T) { 1274 formatOverrideSpec{ 1275 overrideWidth: 20, 1276 overrideHeight: 40, 1277 expect: []openrtb2.Format{{ 1278 W: 20, 1279 H: 40, 1280 }}, 1281 }.execute(t) 1282 } 1283 1284 func TestOverrideHeightNormalWidth(t *testing.T) { 1285 formatOverrideSpec{ 1286 width: 20, 1287 overrideHeight: 40, 1288 expect: []openrtb2.Format{{ 1289 W: 20, 1290 H: 40, 1291 }}, 1292 }.execute(t) 1293 } 1294 1295 func TestOverrideWidthNormalHeight(t *testing.T) { 1296 formatOverrideSpec{ 1297 overrideWidth: 20, 1298 height: 40, 1299 expect: []openrtb2.Format{{ 1300 W: 20, 1301 H: 40, 1302 }}, 1303 }.execute(t) 1304 } 1305 1306 func TestMultisize(t *testing.T) { 1307 formatOverrideSpec{ 1308 multisize: "200x50,100x60", 1309 expect: []openrtb2.Format{{ 1310 W: 200, 1311 H: 50, 1312 }, { 1313 W: 100, 1314 H: 60, 1315 }}, 1316 }.execute(t) 1317 } 1318 1319 func TestSizeWithMultisize(t *testing.T) { 1320 formatOverrideSpec{ 1321 width: 20, 1322 height: 40, 1323 multisize: "200x50,100x60", 1324 expect: []openrtb2.Format{{ 1325 W: 20, 1326 H: 40, 1327 }, { 1328 W: 200, 1329 H: 50, 1330 }, { 1331 W: 100, 1332 H: 60, 1333 }}, 1334 }.execute(t) 1335 } 1336 1337 func TestHeightOnly(t *testing.T) { 1338 formatOverrideSpec{ 1339 height: 200, 1340 expect: []openrtb2.Format{{ 1341 W: 300, 1342 H: 200, 1343 }}, 1344 }.execute(t) 1345 } 1346 1347 func TestWidthOnly(t *testing.T) { 1348 formatOverrideSpec{ 1349 width: 150, 1350 expect: []openrtb2.Format{{ 1351 W: 150, 1352 H: 600, 1353 }}, 1354 }.execute(t) 1355 } 1356 1357 type formatOverrideSpec struct { 1358 width uint64 1359 height uint64 1360 overrideWidth uint64 1361 overrideHeight uint64 1362 multisize string 1363 account string 1364 expect []openrtb2.Format 1365 } 1366 1367 func (s formatOverrideSpec) execute(t *testing.T) { 1368 requests := map[string]json.RawMessage{ 1369 "1": json.RawMessage(validRequest(t, "site.json")), 1370 } 1371 1372 endpoint, _ := NewAmpEndpoint( 1373 fakeUUIDGenerator{}, 1374 &mockAmpExchange{}, 1375 newParamsValidator(t), 1376 &mockAmpStoredReqFetcher{requests}, 1377 empty_fetcher.EmptyFetcher{}, 1378 &config.Configuration{MaxRequestSize: maxSize}, 1379 &metricsConfig.NilMetricsEngine{}, 1380 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 1381 map[string]string{}, 1382 []byte{}, 1383 openrtb_ext.BuildBidderMap(), 1384 empty_fetcher.EmptyFetcher{}, 1385 hooks.EmptyPlanBuilder{}, 1386 nil, 1387 ) 1388 1389 url := fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&debug=1&w=%d&h=%d&ow=%d&oh=%d&ms=%s&account=%s", s.width, s.height, s.overrideWidth, s.overrideHeight, s.multisize, s.account) 1390 request := httptest.NewRequest("GET", url, nil) 1391 recorder := httptest.NewRecorder() 1392 endpoint(recorder, request, nil) 1393 if recorder.Code != http.StatusOK { 1394 t.Errorf("Expected status %d. Got %d. Request config ID was 1", http.StatusOK, recorder.Code) 1395 t.Errorf("Response body was: %s", recorder.Body) 1396 t.Errorf("Request was: %s", string(requests["1"])) 1397 } 1398 var response AmpResponse 1399 if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { 1400 t.Fatalf("Error unmarshalling response: %s", err.Error()) 1401 } 1402 var resolvedRequest openrtb2.BidRequest 1403 err := json.Unmarshal(response.ORTB2.Ext.Debug.ResolvedRequest, &resolvedRequest) 1404 assert.NoError(t, err, "resolved request should have the correct format") 1405 formats := resolvedRequest.Imp[0].Banner.Format 1406 if len(formats) != len(s.expect) { 1407 t.Fatalf("Bad formats length. Expected %v, got %v", s.expect, formats) 1408 } 1409 for i := 0; i < len(formats); i++ { 1410 if formats[i].W != s.expect[i].W { 1411 t.Errorf("format[%d].W were not equal. Expected %d, got %d", i, s.expect[i].W, formats[i].W) 1412 } 1413 if formats[i].H != s.expect[i].H { 1414 t.Errorf("format[%d].H were not equal. Expected %d, got %d", i, s.expect[i].H, formats[i].H) 1415 } 1416 } 1417 } 1418 1419 type mockAmpExchange struct { 1420 lastRequest *openrtb2.BidRequest 1421 requestExt json.RawMessage 1422 } 1423 1424 var expectedErrorsFromHoldAuction map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage = map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{ 1425 openrtb_ext.BidderName("openx"): { 1426 { 1427 Code: 1, 1428 Message: "The request exceeded the timeout allocated", 1429 }, 1430 }, 1431 } 1432 1433 func (m *mockAmpExchange) HoldAuction(ctx context.Context, auctionRequest *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { 1434 r := auctionRequest.BidRequestWrapper 1435 m.lastRequest = r.BidRequest 1436 1437 response := &openrtb2.BidResponse{ 1438 SeatBid: []openrtb2.SeatBid{{ 1439 Bid: []openrtb2.Bid{{ 1440 AdM: "<script></script>", 1441 Ext: json.RawMessage(`{ "prebid": {"targeting": { "hb_pb": "1.20", "hb_appnexus_pb": "1.20", "hb_cache_id": "some_id"}}}`), 1442 }}, 1443 }}, 1444 Ext: json.RawMessage(`{ "errors": {"openx":[ { "code": 1, "message": "The request exceeded the timeout allocated" } ] } }`), 1445 } 1446 1447 if m.requestExt != nil { 1448 response.Ext = m.requestExt 1449 } 1450 if len(auctionRequest.StoredAuctionResponses) > 0 { 1451 var seatBids []openrtb2.SeatBid 1452 1453 if err := json.Unmarshal(auctionRequest.StoredAuctionResponses[r.BidRequest.Imp[0].ID], &seatBids); err != nil { 1454 return nil, err 1455 } 1456 response.SeatBid = seatBids 1457 } 1458 1459 if r.BidRequest.Test == 1 { 1460 resolvedRequest, err := json.Marshal(r.BidRequest) 1461 if err != nil { 1462 resolvedRequest = json.RawMessage("{}") 1463 } 1464 response.Ext = json.RawMessage(fmt.Sprintf(`{"debug": {"httpcalls": {}, "resolvedrequest": %s}}`, resolvedRequest)) 1465 } 1466 1467 return &exchange.AuctionResponse{BidResponse: response}, nil 1468 } 1469 1470 type mockAmpExchangeWarnings struct{} 1471 1472 func (m *mockAmpExchangeWarnings) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { 1473 response := &openrtb2.BidResponse{ 1474 SeatBid: []openrtb2.SeatBid{{ 1475 Bid: []openrtb2.Bid{{ 1476 AdM: "<script></script>", 1477 Ext: json.RawMessage(`{ "prebid": {"targeting": { "hb_pb": "1.20", "hb_appnexus_pb": "1.20", "hb_cache_id": "some_id"}}}`), 1478 }}, 1479 }}, 1480 Ext: json.RawMessage(`{ "warnings": {"appnexus": [{"code": 10003, "message": "debug turned off for bidder"}] }}`), 1481 } 1482 return &exchange.AuctionResponse{BidResponse: response}, nil 1483 } 1484 1485 func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool, regsExt *openrtb_ext.ExtRegs) ([]byte, error) { 1486 var width int64 = 300 1487 var height int64 = 300 1488 bidRequest := &openrtb2.BidRequest{ 1489 ID: "test-request-id", 1490 Imp: []openrtb2.Imp{ 1491 { 1492 ID: "/19968336/header-bid-tag-0", 1493 Ext: json.RawMessage(`{"appnexus": { "placementId":12883451 }}`), 1494 Banner: &openrtb2.Banner{ 1495 Format: []openrtb2.Format{ 1496 { 1497 W: width, 1498 H: 250, 1499 }, 1500 { 1501 W: width, 1502 H: 240, 1503 }, 1504 }, 1505 W: &width, 1506 H: &height, 1507 }, 1508 }, 1509 }, 1510 Site: &openrtb2.Site{ 1511 ID: "site-id", 1512 Page: "some-page", 1513 }, 1514 } 1515 1516 var userExtData []byte 1517 if userExt != nil { 1518 var err error 1519 userExtData, err = json.Marshal(userExt) 1520 if err != nil { 1521 return nil, err 1522 } 1523 } 1524 1525 if !nilUser { 1526 bidRequest.User = &openrtb2.User{ 1527 ID: "aUserId", 1528 BuyerUID: "aBuyerID", 1529 Ext: userExtData, 1530 } 1531 } 1532 1533 var regsExtData []byte 1534 if regsExt != nil { 1535 var err error 1536 regsExtData, err = json.Marshal(regsExt) 1537 if err != nil { 1538 return nil, err 1539 } 1540 } 1541 1542 if !nilRegs { 1543 bidRequest.Regs = &openrtb2.Regs{ 1544 COPPA: 1, 1545 Ext: regsExtData, 1546 } 1547 } 1548 return json.Marshal(bidRequest) 1549 } 1550 1551 func TestSetEffectiveAmpPubID(t *testing.T) { 1552 testPubID := "test-pub" 1553 1554 testCases := []struct { 1555 description string 1556 req *openrtb2.BidRequest 1557 account string 1558 expectedPubID string 1559 }{ 1560 { 1561 description: "No publisher ID provided", 1562 req: &openrtb2.BidRequest{ 1563 App: &openrtb2.App{ 1564 Publisher: nil, 1565 }, 1566 }, 1567 expectedPubID: "", 1568 }, 1569 { 1570 description: "Publisher ID present in req.App.Publisher.ID", 1571 req: &openrtb2.BidRequest{ 1572 App: &openrtb2.App{ 1573 Publisher: &openrtb2.Publisher{ 1574 ID: testPubID, 1575 }, 1576 }, 1577 }, 1578 expectedPubID: testPubID, 1579 }, 1580 { 1581 description: "Publisher ID present in req.Site.Publisher.ID", 1582 req: &openrtb2.BidRequest{ 1583 Site: &openrtb2.Site{ 1584 Publisher: &openrtb2.Publisher{ 1585 ID: testPubID, 1586 }, 1587 }, 1588 }, 1589 expectedPubID: testPubID, 1590 }, 1591 { 1592 description: "Publisher ID present in account parameter", 1593 req: &openrtb2.BidRequest{ 1594 App: &openrtb2.App{ 1595 Publisher: &openrtb2.Publisher{ 1596 ID: "", 1597 }, 1598 }, 1599 }, 1600 account: testPubID, 1601 expectedPubID: testPubID, 1602 }, 1603 { 1604 description: "req.Site.Publisher present but ID set to empty string", 1605 req: &openrtb2.BidRequest{ 1606 Site: &openrtb2.Site{ 1607 Publisher: &openrtb2.Publisher{ 1608 ID: "", 1609 }, 1610 }, 1611 }, 1612 expectedPubID: "", 1613 }, 1614 } 1615 1616 for _, test := range testCases { 1617 setEffectiveAmpPubID(test.req, test.account) 1618 if test.req.Site != nil { 1619 if test.req.Site.Publisher == nil { 1620 assert.Empty(t, test.expectedPubID, 1621 "should return the expected Publisher ID for test case: %s", test.description) 1622 } else { 1623 assert.Equal(t, test.expectedPubID, test.req.Site.Publisher.ID, 1624 "should return the expected Publisher ID for test case: %s", test.description) 1625 } 1626 } else { 1627 if test.req.App.Publisher == nil { 1628 assert.Empty(t, test.expectedPubID, 1629 "should return the expected Publisher ID for test case: %s", test.description) 1630 } else { 1631 assert.Equal(t, test.expectedPubID, test.req.App.Publisher.ID, 1632 "should return the expected Publisher ID for test case: %s", test.description) 1633 } 1634 } 1635 } 1636 } 1637 1638 type mockLogger struct { 1639 ampObject *analytics.AmpObject 1640 auctionObject *analytics.AuctionObject 1641 } 1642 1643 func newMockLogger(ao *analytics.AmpObject, aucObj *analytics.AuctionObject) analytics.PBSAnalyticsModule { 1644 return &mockLogger{ 1645 ampObject: ao, 1646 auctionObject: aucObj, 1647 } 1648 } 1649 1650 func (logger mockLogger) LogAuctionObject(ao *analytics.AuctionObject) { 1651 *logger.auctionObject = *ao 1652 } 1653 func (logger mockLogger) LogVideoObject(vo *analytics.VideoObject) { 1654 } 1655 func (logger mockLogger) LogCookieSyncObject(cookieObject *analytics.CookieSyncObject) { 1656 } 1657 func (logger mockLogger) LogSetUIDObject(uuidObj *analytics.SetUIDObject) { 1658 } 1659 func (logger mockLogger) LogNotificationEventObject(uuidObj *analytics.NotificationEvent) { 1660 } 1661 func (logger mockLogger) LogAmpObject(ao *analytics.AmpObject) { 1662 *logger.ampObject = *ao 1663 } 1664 1665 func TestBuildAmpObject(t *testing.T) { 1666 testCases := []struct { 1667 description string 1668 inTagId string 1669 exchange *mockAmpExchange 1670 inStoredRequest json.RawMessage 1671 expectedAmpObject *analytics.AmpObject 1672 }{ 1673 { 1674 description: "Stored Amp request with nil body. Only the error gets logged", 1675 inTagId: "test", 1676 inStoredRequest: nil, 1677 expectedAmpObject: &analytics.AmpObject{ 1678 Status: http.StatusOK, 1679 Errors: []error{fmt.Errorf("unexpected end of JSON input")}, 1680 }, 1681 }, 1682 { 1683 description: "Stored Amp request with no imps that should return error. Only the error gets logged", 1684 inTagId: "test", 1685 inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[],"tmax":500}`), 1686 expectedAmpObject: &analytics.AmpObject{ 1687 Status: http.StatusOK, 1688 Errors: []error{fmt.Errorf("data for tag_id='test' does not define the required imp array")}, 1689 }, 1690 }, 1691 { 1692 description: "Wrong tag_id, error gets logged", 1693 inTagId: "unknown", 1694 inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[{"id":"some-impression-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}}],"tmax":500}`), 1695 expectedAmpObject: &analytics.AmpObject{ 1696 Status: http.StatusOK, 1697 Errors: []error{fmt.Errorf("unexpected end of JSON input")}, 1698 }, 1699 }, 1700 { 1701 description: "Valid stored Amp request, correct tag_id, a valid response should be logged", 1702 inTagId: "test", 1703 inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[{"id":"some-impression-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}}],"tmax":500}`), 1704 expectedAmpObject: &analytics.AmpObject{ 1705 Status: http.StatusOK, 1706 Errors: nil, 1707 RequestWrapper: &openrtb_ext.RequestWrapper{ 1708 BidRequest: &openrtb2.BidRequest{ 1709 ID: "some-request-id", 1710 Device: &openrtb2.Device{ 1711 IP: "192.0.2.1", 1712 }, 1713 Site: &openrtb2.Site{ 1714 Page: "prebid.org", 1715 Ext: json.RawMessage(`{"amp":1}`), 1716 }, 1717 Imp: []openrtb2.Imp{ 1718 { 1719 ID: "some-impression-id", 1720 Banner: &openrtb2.Banner{ 1721 Format: []openrtb2.Format{ 1722 { 1723 W: 300, 1724 H: 250, 1725 }, 1726 }, 1727 }, 1728 Secure: func(val int8) *int8 { return &val }(1), //(*int8)(1), 1729 Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}`), 1730 }, 1731 }, 1732 AT: 1, 1733 TMax: 500, 1734 Ext: json.RawMessage(`{"prebid":{"cache":{"bids":{}},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"mediatypepricegranularity":{},"includewinners":true,"includebidderkeys":true}}}`), 1735 }, 1736 }, 1737 AuctionResponse: &openrtb2.BidResponse{ 1738 SeatBid: []openrtb2.SeatBid{{ 1739 Bid: []openrtb2.Bid{{ 1740 AdM: "<script></script>", 1741 Ext: json.RawMessage(`{ "prebid": {"targeting": { "hb_pb": "1.20", "hb_appnexus_pb": "1.20", "hb_cache_id": "some_id"}}}`), 1742 }}, 1743 Seat: "", 1744 }}, 1745 Ext: json.RawMessage(`{ "errors": {"openx":[ { "code": 1, "message": "The request exceeded the timeout allocated" } ] } }`), 1746 }, 1747 AmpTargetingValues: map[string]string{ 1748 "hb_appnexus_pb": "1.20", 1749 "hb_cache_id": "some_id", 1750 "hb_pb": "1.20", 1751 }, 1752 Origin: "", 1753 }, 1754 }, 1755 { 1756 description: "Global targeting from bid response should be applied for Amp", 1757 inTagId: "test", 1758 inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[{"id":"some-impression-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}}],"tmax":500}`), 1759 exchange: &mockAmpExchange{requestExt: json.RawMessage(`{ "prebid": {"targeting": { "test_key": "test_value", "hb_appnexus_pb": "9999" } }, "errors": {"openx":[ { "code": 1, "message": "The request exceeded the timeout allocated" } ] } }`)}, 1760 expectedAmpObject: &analytics.AmpObject{ 1761 Status: http.StatusOK, 1762 Errors: nil, 1763 RequestWrapper: &openrtb_ext.RequestWrapper{ 1764 BidRequest: &openrtb2.BidRequest{ 1765 ID: "some-request-id", 1766 Device: &openrtb2.Device{ 1767 IP: "192.0.2.1", 1768 }, 1769 Site: &openrtb2.Site{ 1770 Page: "prebid.org", 1771 Ext: json.RawMessage(`{"amp":1}`), 1772 }, 1773 Imp: []openrtb2.Imp{ 1774 { 1775 ID: "some-impression-id", 1776 Banner: &openrtb2.Banner{ 1777 Format: []openrtb2.Format{ 1778 { 1779 W: 300, 1780 H: 250, 1781 }, 1782 }, 1783 }, 1784 Secure: func(val int8) *int8 { return &val }(1), //(*int8)(1), 1785 Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}`), 1786 }, 1787 }, 1788 AT: 1, 1789 TMax: 500, 1790 Ext: json.RawMessage(`{"prebid":{"cache":{"bids":{}},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"mediatypepricegranularity":{},"includewinners":true,"includebidderkeys":true}}}`), 1791 }, 1792 }, 1793 AuctionResponse: &openrtb2.BidResponse{ 1794 SeatBid: []openrtb2.SeatBid{{ 1795 Bid: []openrtb2.Bid{{ 1796 AdM: "<script></script>", 1797 Ext: json.RawMessage(`{ "prebid": {"targeting": { "hb_pb": "1.20", "hb_appnexus_pb": "1.20", "hb_cache_id": "some_id"}}}`), 1798 }}, 1799 Seat: "", 1800 }}, 1801 Ext: json.RawMessage(`{ "prebid": {"targeting": { "test_key": "test_value", "hb_appnexus_pb": "9999" } }, "errors": {"openx":[ { "code": 1, "message": "The request exceeded the timeout allocated" } ] } }`), 1802 }, 1803 AmpTargetingValues: map[string]string{ 1804 "hb_appnexus_pb": "1.20", // Bid level has higher priority than global 1805 "hb_cache_id": "some_id", 1806 "hb_pb": "1.20", 1807 "test_key": "test_value", // New global key added 1808 }, 1809 Origin: "", 1810 }, 1811 }, 1812 } 1813 1814 request := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=test", nil) 1815 recorder := httptest.NewRecorder() 1816 1817 for _, test := range testCases { 1818 // Set up test, declare a new mock logger every time 1819 exchange := test.exchange 1820 if exchange == nil { 1821 exchange = &mockAmpExchange{} 1822 } 1823 actualAmpObject, endpoint := ampObjectTestSetup(t, test.inTagId, test.inStoredRequest, false, exchange) 1824 // Run test 1825 endpoint(recorder, request, nil) 1826 1827 // assert AmpObject 1828 assert.Equalf(t, test.expectedAmpObject.Status, actualAmpObject.Status, "Amp Object Status field doesn't match expected: %s\n", test.description) 1829 assert.Lenf(t, actualAmpObject.Errors, len(test.expectedAmpObject.Errors), "Amp Object Errors array doesn't match expected: %s\n", test.description) 1830 var expectedRequest *openrtb2.BidRequest 1831 var actualRequest *openrtb2.BidRequest 1832 if test.expectedAmpObject.RequestWrapper != nil { 1833 expectedRequest = test.expectedAmpObject.RequestWrapper.BidRequest 1834 } 1835 if actualAmpObject.RequestWrapper != nil { 1836 actualRequest = test.expectedAmpObject.RequestWrapper.BidRequest 1837 } 1838 assert.Equalf(t, expectedRequest, actualRequest, "Amp Object BidRequest doesn't match expected: %s\n", test.description) 1839 assert.Equalf(t, test.expectedAmpObject.AuctionResponse, actualAmpObject.AuctionResponse, "Amp Object BidResponse doesn't match expected: %s\n", test.description) 1840 assert.Equalf(t, test.expectedAmpObject.AmpTargetingValues, actualAmpObject.AmpTargetingValues, "Amp Object AmpTargetingValues doesn't match expected: %s\n", test.description) 1841 assert.Equalf(t, test.expectedAmpObject.Origin, actualAmpObject.Origin, "Amp Object Origin field doesn't match expected: %s\n", test.description) 1842 } 1843 } 1844 1845 func TestIdGeneration(t *testing.T) { 1846 uuid := "foo" 1847 1848 testCases := []struct { 1849 description string 1850 givenInStoredRequest json.RawMessage 1851 givenGenerateRequestID bool 1852 expectedID string 1853 }{ 1854 { 1855 description: "The givenGenerateRequestID flag is set to true, so even though the stored amp request already has an id, we should still generate a new uuid", 1856 givenInStoredRequest: json.RawMessage(`{"id":"ThisID","site":{"page":"prebid.org"},"imp":[{"id":"some-imp-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":1}}}],"tmax":1}`), 1857 givenGenerateRequestID: true, 1858 expectedID: uuid, 1859 }, 1860 { 1861 description: "The givenGenerateRequestID flag is set to true and the stored amp request ID is blank, so we should generate a new uuid for the request", 1862 givenInStoredRequest: json.RawMessage(`{"id":"","site":{"page":"prebid.org"},"imp":[{"id":"some-imp-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":1}}}],"tmax":1}`), 1863 givenGenerateRequestID: true, 1864 expectedID: uuid, 1865 }, 1866 { 1867 description: "The givenGenerateRequestID flag is false, so the ID shouldn't change", 1868 givenInStoredRequest: json.RawMessage(`{"id":"ThisID","site":{"page":"prebid.org"},"imp":[{"id":"some-imp-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":1}}}],"tmax":1}`), 1869 givenGenerateRequestID: false, 1870 expectedID: "ThisID", 1871 }, 1872 { 1873 description: "The givenGenerateRequestID flag is true, and the id field isn't included in the stored request, we should still generate a uuid", 1874 givenInStoredRequest: json.RawMessage(`{"site":{"page":"prebid.org"},"imp":[{"id":"some-imp-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":1}}}],"tmax":1}`), 1875 givenGenerateRequestID: true, 1876 expectedID: uuid, 1877 }, 1878 { 1879 description: "The givenGenerateRequestID flag is false, but id field is the macro option {{UUID}}, we should generate a uuid", 1880 givenInStoredRequest: json.RawMessage(`{"id":"{{UUID}}","site":{"page":"prebid.org"},"imp":[{"id":"some-imp-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":1}}}],"tmax":1}`), 1881 givenGenerateRequestID: false, 1882 expectedID: uuid, 1883 }, 1884 { 1885 description: "Macro ID case sensitivity check. The id is {{uuid}}, but we should only generate an id if it's all uppercase {{UUID}}. So the ID shouldn't change.", 1886 givenInStoredRequest: json.RawMessage(`{"id":"{{uuid}}","site":{"page":"prebid.org"},"imp":[{"id":"some-imp-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":1}}}],"tmax":1}`), 1887 givenGenerateRequestID: false, 1888 expectedID: "{{uuid}}", 1889 }, 1890 } 1891 1892 request := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=test", nil) 1893 recorder := httptest.NewRecorder() 1894 1895 for _, test := range testCases { 1896 // Set up and run test 1897 actualAmpObject, endpoint := ampObjectTestSetup(t, "test", test.givenInStoredRequest, test.givenGenerateRequestID, &mockAmpExchange{}) 1898 endpoint(recorder, request, nil) 1899 assert.Equalf(t, test.expectedID, actualAmpObject.RequestWrapper.ID, "Bid Request ID is incorrect: %s\n", test.description) 1900 } 1901 } 1902 1903 func ampObjectTestSetup(t *testing.T, inTagId string, inStoredRequest json.RawMessage, generateRequestID bool, exchange *mockAmpExchange) (*analytics.AmpObject, httprouter.Handle) { 1904 actualAmpObject := analytics.AmpObject{} 1905 logger := newMockLogger(&actualAmpObject, nil) 1906 1907 mockAmpFetcher := &mockAmpStoredReqFetcher{ 1908 data: map[string]json.RawMessage{ 1909 inTagId: json.RawMessage(inStoredRequest), 1910 }, 1911 } 1912 1913 endpoint, _ := NewAmpEndpoint( 1914 fakeUUIDGenerator{id: "foo", err: nil}, 1915 exchange, 1916 newParamsValidator(t), 1917 mockAmpFetcher, 1918 empty_fetcher.EmptyFetcher{}, 1919 &config.Configuration{MaxRequestSize: maxSize, GenerateRequestID: generateRequestID}, 1920 &metricsConfig.NilMetricsEngine{}, 1921 logger, 1922 map[string]string{}, 1923 []byte{}, 1924 openrtb_ext.BuildBidderMap(), 1925 empty_fetcher.EmptyFetcher{}, 1926 hooks.EmptyPlanBuilder{}, 1927 nil, 1928 ) 1929 return &actualAmpObject, endpoint 1930 } 1931 1932 func TestAmpAuctionResponseHeaders(t *testing.T) { 1933 testCases := []struct { 1934 description string 1935 requestURLArguments string 1936 expectedStatus int 1937 expectedHeaders func(http.Header) 1938 }{ 1939 { 1940 description: "Success Response", 1941 requestURLArguments: "?tag_id=1&__amp_source_origin=foo", 1942 expectedStatus: 200, 1943 expectedHeaders: func(h http.Header) { 1944 h.Set("AMP-Access-Control-Allow-Source-Origin", "foo") 1945 h.Set("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin") 1946 h.Set("X-Prebid", "pbs-go/unknown") 1947 h.Set("Content-Type", "text/plain; charset=utf-8") 1948 }, 1949 }, 1950 { 1951 description: "Failure Response", 1952 requestURLArguments: "?tag_id=invalid&__amp_source_origin=foo", 1953 expectedStatus: 400, 1954 expectedHeaders: func(h http.Header) { 1955 h.Set("AMP-Access-Control-Allow-Source-Origin", "foo") 1956 h.Set("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin") 1957 h.Set("X-Prebid", "pbs-go/unknown") 1958 }, 1959 }, 1960 } 1961 1962 storedRequests := map[string]json.RawMessage{ 1963 "1": json.RawMessage(validRequest(t, "site.json")), 1964 } 1965 exchange := &nobidExchange{} 1966 endpoint, _ := NewAmpEndpoint( 1967 fakeUUIDGenerator{}, 1968 exchange, 1969 newParamsValidator(t), 1970 &mockAmpStoredReqFetcher{storedRequests}, 1971 empty_fetcher.EmptyFetcher{}, 1972 &config.Configuration{MaxRequestSize: maxSize}, 1973 &metricsConfig.NilMetricsEngine{}, 1974 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 1975 map[string]string{}, 1976 []byte{}, 1977 openrtb_ext.BuildBidderMap(), 1978 empty_fetcher.EmptyFetcher{}, 1979 hooks.EmptyPlanBuilder{}, 1980 nil, 1981 ) 1982 1983 for _, test := range testCases { 1984 httpReq := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp"+test.requestURLArguments), nil) 1985 recorder := httptest.NewRecorder() 1986 1987 endpoint(recorder, httpReq, nil) 1988 1989 expectedHeaders := http.Header{} 1990 test.expectedHeaders(expectedHeaders) 1991 1992 assert.Equal(t, test.expectedStatus, recorder.Result().StatusCode, test.description+":statuscode") 1993 assert.Equal(t, expectedHeaders, recorder.Result().Header, test.description+":statuscode") 1994 } 1995 } 1996 1997 func TestRequestWithTargeting(t *testing.T) { 1998 stored := map[string]json.RawMessage{ 1999 "1": json.RawMessage(validRequest(t, "site.json")), 2000 } 2001 exchange := &mockAmpExchange{} 2002 endpoint, _ := NewAmpEndpoint( 2003 fakeUUIDGenerator{}, 2004 exchange, 2005 newParamsValidator(t), 2006 &mockAmpStoredReqFetcher{stored}, 2007 empty_fetcher.EmptyFetcher{}, 2008 &config.Configuration{MaxRequestSize: maxSize}, 2009 &metricsConfig.NilMetricsEngine{}, 2010 analyticsConf.NewPBSAnalytics(&config.Analytics{}), 2011 nil, 2012 nil, 2013 openrtb_ext.BuildBidderMap(), 2014 empty_fetcher.EmptyFetcher{}, 2015 hooks.EmptyPlanBuilder{}, 2016 nil, 2017 ) 2018 url, err := url.Parse("/openrtb2/auction/amp") 2019 assert.NoError(t, err, "unexpected error received while parsing url") 2020 values := url.Query() 2021 values.Add("targeting", `{"gam-key1":"val1", "gam-key2":"val2"}`) 2022 values.Add("tag_id", "1") 2023 url.RawQuery = values.Encode() 2024 2025 request, err := http.NewRequest("GET", url.String(), nil) 2026 if !assert.NoError(t, err) { 2027 return 2028 } 2029 recorder := httptest.NewRecorder() 2030 endpoint(recorder, request, nil) 2031 2032 if assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", recorder.Code, recorder.Body.String()) { 2033 assert.JSONEq(t, `{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}, "data":{"gam-key1":"val1", "gam-key2":"val2"}}`, string(exchange.lastRequest.Imp[0].Ext)) 2034 } 2035 } 2036 2037 func TestSetTargeting(t *testing.T) { 2038 tests := []struct { 2039 description string 2040 bidRequest openrtb2.BidRequest 2041 targeting string 2042 expectedImpExt string 2043 wantError bool 2044 errorMessage string 2045 }{ 2046 { 2047 description: "valid imp ext, valid targeting data", 2048 bidRequest: openrtb2.BidRequest{Imp: []openrtb2.Imp{{Ext: []byte(`{"appnexus":{"placementId":123}}`)}}}, 2049 targeting: `{"gam-key1":"val1", "gam-key2":"val2"}`, 2050 expectedImpExt: `{"appnexus":{"placementId":123}, "data": {"gam-key1":"val1", "gam-key2":"val2"}}`, 2051 wantError: false, 2052 errorMessage: "", 2053 }, 2054 { 2055 description: "valid imp ext, empty targeting data", 2056 bidRequest: openrtb2.BidRequest{Imp: []openrtb2.Imp{{Ext: []byte(`{"appnexus":{"placementId":123}}`)}}}, 2057 targeting: ``, 2058 expectedImpExt: `{"appnexus":{"placementId":123}}`, 2059 wantError: false, 2060 errorMessage: "", 2061 }, 2062 { 2063 description: "empty imp ext, valid targeting data", 2064 bidRequest: openrtb2.BidRequest{Imp: []openrtb2.Imp{{Ext: []byte(`{}`)}}}, 2065 targeting: `{"gam-key1":"val1", "gam-key2":"val2"}`, 2066 expectedImpExt: `{"data": {"gam-key1":"val1", "gam-key2":"val2"}}`, 2067 wantError: false, 2068 errorMessage: "", 2069 }, 2070 { 2071 description: "nil imp ext, valid targeting data", 2072 bidRequest: openrtb2.BidRequest{Imp: []openrtb2.Imp{{Ext: nil}}}, 2073 targeting: `{"gam-key1":"val1", "gam-key2":"val2"}`, 2074 expectedImpExt: `{"data": {"gam-key1":"val1", "gam-key2":"val2"}}`, 2075 wantError: false, 2076 errorMessage: "", 2077 }, 2078 { 2079 description: "imp ext has data, valid targeting data", 2080 bidRequest: openrtb2.BidRequest{Imp: []openrtb2.Imp{{Ext: []byte(`{"data":{"placementId":123}}`)}}}, 2081 targeting: `{"gam-key1":"val1", "gam-key2":"val2"}`, 2082 expectedImpExt: `{"data": {"gam-key1":"val1", "gam-key2":"val2", "placementId":123}}`, 2083 wantError: false, 2084 errorMessage: "", 2085 }, 2086 { 2087 description: "imp ext has data and other fields, valid targeting data", 2088 bidRequest: openrtb2.BidRequest{Imp: []openrtb2.Imp{{Ext: []byte(`{"data":{"placementId":123}, "prebid": 123}`)}}}, 2089 targeting: `{"gam-key1":"val1", "gam-key2":"val2"}`, 2090 expectedImpExt: `{"data": {"gam-key1":"val1", "gam-key2":"val2", "placementId":123}, "prebid":123}`, 2091 wantError: false, 2092 errorMessage: "", 2093 }, 2094 { 2095 description: "imp ext has invalid format, valid targeting data", 2096 bidRequest: openrtb2.BidRequest{Imp: []openrtb2.Imp{{Ext: []byte(`{123:{}`)}}}, 2097 targeting: `{"gam-key1":"val1", "gam-key2":"val2"}`, 2098 expectedImpExt: ``, 2099 wantError: true, 2100 errorMessage: "unable to merge imp.ext with targeting data, check targeting data is correct: Invalid JSON Document", 2101 }, 2102 { 2103 description: "valid imp ext, invalid targeting data", 2104 bidRequest: openrtb2.BidRequest{Imp: []openrtb2.Imp{{Ext: []byte(`{"appnexus":{"placementId":123}}`)}}}, 2105 targeting: `{123,}`, 2106 expectedImpExt: ``, 2107 wantError: true, 2108 errorMessage: "unable to merge imp.ext with targeting data, check targeting data is correct: Invalid JSON Patch", 2109 }, 2110 } 2111 2112 for _, test := range tests { 2113 req := &test.bidRequest 2114 err := setTargeting(req, test.targeting) 2115 if test.wantError { 2116 assert.EqualErrorf(t, err, test.errorMessage, "error is incorrect for test case: %s", test.description) 2117 } else { 2118 assert.NoError(t, err, "error should be nil for test case: %s", test.description) 2119 assert.JSONEq(t, test.expectedImpExt, string(req.Imp[0].Ext), "incorrect impression extension returned for test %s", test.description) 2120 } 2121 2122 } 2123 } 2124 2125 func TestValidAmpResponseWhenRequestRejected(t *testing.T) { 2126 const nbr int = 123 2127 2128 testCases := []struct { 2129 description string 2130 file string 2131 planBuilder hooks.ExecutionPlanBuilder 2132 }{ 2133 { 2134 description: "Assert correct AmpResponse when request rejected at entrypoint stage", 2135 file: "sample-requests/hooks/amp_entrypoint_reject.json", 2136 planBuilder: mockPlanBuilder{entrypointPlan: makePlan[hookstage.Entrypoint](mockRejectionHook{nbr, nil})}, 2137 }, 2138 { 2139 // raw_auction stage not executed for AMP endpoint, so we expect full response 2140 description: "Assert correct AmpResponse when request rejected at raw_auction stage", 2141 file: "sample-requests/amp/valid-supplementary/aliased-buyeruids.json", 2142 planBuilder: mockPlanBuilder{rawAuctionPlan: makePlan[hookstage.RawAuctionRequest](mockRejectionHook{nbr, nil})}, 2143 }, 2144 { 2145 description: "Assert correct AmpResponse when request rejected at processed_auction stage", 2146 file: "sample-requests/hooks/amp_processed_auction_request_reject.json", 2147 planBuilder: mockPlanBuilder{processedAuctionPlan: makePlan[hookstage.ProcessedAuctionRequest](mockRejectionHook{nbr, nil})}, 2148 }, 2149 { 2150 // bidder_request stage rejects only bidder, so we expect bidder rejection warning added 2151 description: "Assert correct AmpResponse when request rejected at bidder-request stage", 2152 file: "sample-requests/hooks/amp_bidder_reject.json", 2153 planBuilder: mockPlanBuilder{bidderRequestPlan: makePlan[hookstage.BidderRequest](mockRejectionHook{nbr, nil})}, 2154 }, 2155 { 2156 // raw_bidder_response stage rejects only bidder, so we expect bidder rejection warning added 2157 description: "Assert correct AmpResponse when request rejected at raw_bidder_response stage", 2158 file: "sample-requests/hooks/amp_bidder_response_reject.json", 2159 planBuilder: mockPlanBuilder{rawBidderResponsePlan: makePlan[hookstage.RawBidderResponse](mockRejectionHook{nbr, nil})}, 2160 }, 2161 { 2162 // no debug information should be added for raw_auction stage because it's not executed for amp endpoint 2163 description: "Assert correct AmpResponse with debug information from modules added to ext.prebid.modules", 2164 file: "sample-requests/hooks/amp.json", 2165 planBuilder: mockPlanBuilder{ 2166 entrypointPlan: hooks.Plan[hookstage.Entrypoint]{ 2167 { 2168 Timeout: 5 * time.Millisecond, 2169 Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{ 2170 entryPointHookUpdateWithErrors, 2171 entryPointHookUpdateWithErrorsAndWarnings, 2172 }, 2173 }, 2174 { 2175 Timeout: 5 * time.Millisecond, 2176 Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{ 2177 entryPointHookUpdate, 2178 }, 2179 }, 2180 }, 2181 rawAuctionPlan: hooks.Plan[hookstage.RawAuctionRequest]{ 2182 { 2183 Timeout: 5 * time.Millisecond, 2184 Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{ 2185 rawAuctionHookNone, 2186 }, 2187 }, 2188 }, 2189 }, 2190 }, 2191 } 2192 2193 for _, tc := range testCases { 2194 t.Run(tc.description, func(t *testing.T) { 2195 fileData, err := os.ReadFile(tc.file) 2196 assert.NoError(t, err, "Failed to read test file.") 2197 2198 test := testCase{} 2199 assert.NoError(t, json.Unmarshal(fileData, &test), "Failed to parse test file.") 2200 2201 request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?%s", test.Query), nil) 2202 recorder := httptest.NewRecorder() 2203 query := request.URL.Query() 2204 tagID := query.Get("tag_id") 2205 2206 test.StoredRequest = map[string]json.RawMessage{tagID: test.BidRequest} 2207 test.planBuilder = tc.planBuilder 2208 test.endpointType = AMP_ENDPOINT 2209 2210 cfg := &config.Configuration{MaxRequestSize: maxSize, AccountDefaults: config.Account{DebugAllow: true}} 2211 ampEndpointHandler, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg) 2212 assert.NoError(t, err, "Failed to build test endpoint.") 2213 2214 ampEndpointHandler(recorder, request, nil) 2215 assert.Equal(t, recorder.Code, http.StatusOK, "Endpoint should return 200 OK.") 2216 2217 var actualAmpResp AmpResponse 2218 var expectedAmpResp AmpResponse 2219 assert.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &actualAmpResp), "Unable to unmarshal actual AmpResponse.") 2220 assert.NoError(t, json.Unmarshal(test.ExpectedAmpResponse, &expectedAmpResp), "Unable to unmarshal expected AmpResponse.") 2221 2222 // validate modules data separately, because it has dynamic data 2223 if expectedAmpResp.ORTB2.Ext.Prebid == nil { 2224 assert.Nil(t, actualAmpResp.ORTB2.Ext.Prebid, "AmpResponse.ortb2.ext.prebid expected to be nil.") 2225 } else { 2226 hookexecution.AssertEqualModulesData(t, expectedAmpResp.ORTB2.Ext.Prebid.Modules, actualAmpResp.ORTB2.Ext.Prebid.Modules) 2227 } 2228 2229 // reset modules to validate amp responses 2230 actualAmpResp.ORTB2.Ext.Prebid = nil 2231 expectedAmpResp.ORTB2.Ext.Prebid = nil 2232 assert.Equal(t, expectedAmpResp, actualAmpResp, "Invalid AMP Response.") 2233 2234 // Close servers regardless if the test case was run or not 2235 for _, mockBidServer := range mockBidServers { 2236 mockBidServer.Close() 2237 } 2238 mockCurrencyRatesServer.Close() 2239 }) 2240 } 2241 } 2242 2243 func TestSendAmpResponse_LogsErrors(t *testing.T) { 2244 testCases := []struct { 2245 description string 2246 expectedErrors []error 2247 expectedStatus int 2248 writer http.ResponseWriter 2249 request *openrtb2.BidRequest 2250 response *openrtb2.BidResponse 2251 hookExecutor hookexecution.HookStageExecutor 2252 }{ 2253 { 2254 description: "Error logged when bid.ext unmarshal fails", 2255 expectedErrors: []error{ 2256 errors.New("Critical error while unpacking AMP targets: unexpected end of JSON input"), 2257 }, 2258 expectedStatus: http.StatusInternalServerError, 2259 writer: httptest.NewRecorder(), 2260 request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, 2261 response: &openrtb2.BidResponse{ID: "some-id", SeatBid: []openrtb2.SeatBid{ 2262 {Bid: []openrtb2.Bid{{Ext: json.RawMessage(`"hb_cache_id`)}}}, 2263 }}, 2264 hookExecutor: &hookexecution.EmptyHookExecutor{}, 2265 }, 2266 { 2267 description: "Error logged when test mode activated but no debug present in response", 2268 expectedErrors: []error{ 2269 errors.New("test set on request but debug not present in response"), 2270 }, 2271 expectedStatus: 0, 2272 writer: httptest.NewRecorder(), 2273 request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, 2274 response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("{}")}, 2275 hookExecutor: &hookexecution.EmptyHookExecutor{}, 2276 }, 2277 { 2278 description: "Error logged when response encoding fails", 2279 expectedErrors: []error{ 2280 errors.New("/openrtb2/amp Failed to send response: failed writing response"), 2281 }, 2282 expectedStatus: 0, 2283 writer: errorResponseWriter{}, 2284 request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, 2285 response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage(`{"debug": {}}`)}, 2286 hookExecutor: &hookexecution.EmptyHookExecutor{}, 2287 }, 2288 { 2289 description: "Error logged if hook enrichment returns warnings", 2290 expectedErrors: []error{ 2291 errors.New("Value is not a string: 1"), 2292 errors.New("Value is not a boolean: active"), 2293 }, 2294 expectedStatus: 0, 2295 writer: httptest.NewRecorder(), 2296 request: &openrtb2.BidRequest{ID: "some-id", Ext: json.RawMessage(`{"prebid": {"debug": "active", "trace": 1}}`)}, 2297 response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("{}")}, 2298 hookExecutor: &mockStageExecutor{ 2299 outcomes: []hookexecution.StageOutcome{ 2300 { 2301 Entity: "bid-request", 2302 Stage: hooks.StageBidderRequest.String(), 2303 Groups: []hookexecution.GroupOutcome{ 2304 { 2305 InvocationResults: []hookexecution.HookOutcome{ 2306 { 2307 HookID: hookexecution.HookID{ 2308 ModuleCode: "foobar", 2309 HookImplCode: "foo", 2310 }, 2311 Status: hookexecution.StatusSuccess, 2312 Action: hookexecution.ActionNone, 2313 Warnings: []string{"warning message"}, 2314 }, 2315 }, 2316 }, 2317 }, 2318 }, 2319 }, 2320 }, 2321 }, 2322 } 2323 2324 for _, test := range testCases { 2325 t.Run(test.description, func(t *testing.T) { 2326 labels := metrics.Labels{} 2327 ao := analytics.AmpObject{} 2328 account := &config.Account{DebugAllow: true} 2329 reqWrapper := openrtb_ext.RequestWrapper{BidRequest: test.request} 2330 2331 labels, ao = sendAmpResponse(test.writer, test.hookExecutor, &exchange.AuctionResponse{BidResponse: test.response}, &reqWrapper, account, labels, ao, nil) 2332 2333 assert.Equal(t, ao.Errors, test.expectedErrors, "Invalid errors.") 2334 assert.Equal(t, test.expectedStatus, ao.Status, "Invalid HTTP response status.") 2335 }) 2336 } 2337 } 2338 2339 type errorResponseWriter struct{} 2340 2341 func (e errorResponseWriter) Header() http.Header { 2342 return http.Header{} 2343 } 2344 2345 func (e errorResponseWriter) Write(bytes []byte) (int, error) { 2346 return 0, errors.New("failed writing response") 2347 } 2348 2349 func (e errorResponseWriter) WriteHeader(statusCode int) {} 2350 2351 func TestSetSeatNonBid(t *testing.T) { 2352 type args struct { 2353 finalExtBidResponse *openrtb_ext.ExtBidResponse 2354 request *openrtb_ext.RequestWrapper 2355 auctionResponse *exchange.AuctionResponse 2356 } 2357 tests := []struct { 2358 name string 2359 args args 2360 want bool 2361 }{ 2362 { 2363 name: "nil-auctionResponse", 2364 args: args{auctionResponse: nil}, 2365 want: false, 2366 }, 2367 { 2368 name: "nil-request", 2369 args: args{auctionResponse: &exchange.AuctionResponse{}, request: nil}, 2370 want: false, 2371 }, 2372 { 2373 name: "invalid-req-ext", 2374 args: args{auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`invalid json`)}}}, 2375 want: false, 2376 }, 2377 { 2378 name: "nil-prebid", 2379 args: args{auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: nil}}}, 2380 want: false, 2381 }, 2382 { 2383 name: "returnallbidstatus-is-false", 2384 args: args{auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid" : {"returnallbidstatus" : false}}`)}}}, 2385 want: false, 2386 }, 2387 { 2388 name: "finalExtBidResponse-is-nil", 2389 args: args{finalExtBidResponse: nil}, 2390 want: false, 2391 }, 2392 { 2393 name: "returnallbidstatus-is-true-and-responseExt.Prebid-is-nil", 2394 args: args{finalExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: nil}, auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid" : {"returnallbidstatus" : true}}`)}}}, 2395 want: true, 2396 }, 2397 { 2398 name: "returnallbidstatus-is-true-and-responseExt.Prebid-is-not-nil", 2399 args: args{finalExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: nil}, auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid" : {"returnallbidstatus" : true}}`)}}}, 2400 want: true, 2401 }, 2402 } 2403 for _, tt := range tests { 2404 t.Run(tt.name, func(t *testing.T) { 2405 if got := setSeatNonBid(tt.args.finalExtBidResponse, tt.args.request, tt.args.auctionResponse); got != tt.want { 2406 t.Errorf("setSeatNonBid() = %v, want %v", got, tt.want) 2407 } 2408 }) 2409 } 2410 }