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