github.com/prebid/prebid-server/v2@v2.18.0/privacysandbox/topics_test.go (about) 1 package privacysandbox 2 3 import ( 4 "encoding/json" 5 "sort" 6 "testing" 7 8 "github.com/prebid/openrtb/v20/openrtb2" 9 "github.com/prebid/prebid-server/v2/errortypes" 10 "github.com/stretchr/testify/assert" 11 ) 12 13 func TestParseTopicsFromHeader(t *testing.T) { 14 type args struct { 15 secBrowsingTopics string 16 } 17 tests := []struct { 18 name string 19 args args 20 wantTopic []Topic 21 wantError []error 22 }{ 23 { 24 name: "empty header", 25 args: args{secBrowsingTopics: " "}, 26 wantTopic: []Topic{}, 27 wantError: nil, 28 }, 29 { 30 name: "invalid header value", 31 args: args{secBrowsingTopics: "some-sec-cookie-value"}, 32 wantTopic: []Topic{}, 33 wantError: []error{ 34 &errortypes.DebugWarning{ 35 Message: "Invalid field in Sec-Browsing-Topics header: some-sec-cookie-value", 36 WarningCode: errortypes.SecBrowsingTopicsWarningCode, 37 }, 38 }, 39 }, 40 { 41 name: "header with only finish padding", 42 args: args{secBrowsingTopics: "();p=P0000000000000000000000000000000"}, 43 wantTopic: []Topic{}, 44 wantError: nil, 45 }, 46 { 47 name: "header with one valid field", 48 args: args{secBrowsingTopics: "(1);v=chrome.1:1:2, ();p=P00000000000"}, 49 wantTopic: []Topic{ 50 { 51 SegTax: 600, 52 SegClass: "2", 53 SegIDs: []int{1}, 54 }, 55 }, 56 wantError: nil, 57 }, 58 { 59 name: "header without finish padding", 60 args: args{secBrowsingTopics: "(1);v=chrome.1:1:2"}, 61 wantTopic: []Topic{ 62 { 63 SegTax: 600, 64 SegClass: "2", 65 SegIDs: []int{1}, 66 }, 67 }, 68 wantError: nil, 69 }, 70 { 71 name: "header with more than 10 valid field, should return only 10", 72 args: args{secBrowsingTopics: "(1);v=chrome.1:1:2, (2);v=chrome.1:1:2, (3);v=chrome.1:1:2, (4);v=chrome.1:1:2, (5);v=chrome.1:1:2, (6);v=chrome.1:1:2, (7);v=chrome.1:1:2, (8);v=chrome.1:1:2, (9);v=chrome.1:1:2, (10);v=chrome.1:1:2, (11);v=chrome.1:1:2, (12);v=chrome.1:1:2, ();p=P00000000000"}, 73 wantTopic: []Topic{ 74 { 75 SegTax: 600, 76 SegClass: "2", 77 SegIDs: []int{1}, 78 }, 79 { 80 SegTax: 600, 81 SegClass: "2", 82 SegIDs: []int{2}, 83 }, 84 { 85 SegTax: 600, 86 SegClass: "2", 87 SegIDs: []int{3}, 88 }, 89 { 90 SegTax: 600, 91 SegClass: "2", 92 SegIDs: []int{4}, 93 }, 94 { 95 SegTax: 600, 96 SegClass: "2", 97 SegIDs: []int{5}, 98 }, 99 { 100 SegTax: 600, 101 SegClass: "2", 102 SegIDs: []int{6}, 103 }, 104 { 105 SegTax: 600, 106 SegClass: "2", 107 SegIDs: []int{7}, 108 }, 109 { 110 SegTax: 600, 111 SegClass: "2", 112 SegIDs: []int{8}, 113 }, 114 { 115 SegTax: 600, 116 SegClass: "2", 117 SegIDs: []int{9}, 118 }, 119 { 120 SegTax: 600, 121 SegClass: "2", 122 SegIDs: []int{10}, 123 }, 124 }, 125 wantError: []error{ 126 &errortypes.DebugWarning{ 127 Message: "Invalid field in Sec-Browsing-Topics header: (11);v=chrome.1:1:2 discarded due to limit reached.", 128 WarningCode: errortypes.SecBrowsingTopicsWarningCode, 129 }, 130 &errortypes.DebugWarning{ 131 Message: "Invalid field in Sec-Browsing-Topics header: (12);v=chrome.1:1:2 discarded due to limit reached.", 132 WarningCode: errortypes.SecBrowsingTopicsWarningCode, 133 }, 134 }, 135 }, 136 { 137 name: "header with one valid field having multiple segIDs", 138 args: args{secBrowsingTopics: "(1 2);v=chrome.1:1:2, ();p=P00000000000"}, 139 wantTopic: []Topic{ 140 { 141 SegTax: 600, 142 SegClass: "2", 143 SegIDs: []int{1, 2}, 144 }, 145 }, 146 wantError: nil, 147 }, 148 { 149 name: "header with two valid fields having different taxonomies", 150 args: args{secBrowsingTopics: "(1);v=chrome.1:1:2, (1);v=chrome.1:2:2, ();p=P0000000000"}, 151 wantTopic: []Topic{ 152 { 153 SegTax: 600, 154 SegClass: "2", 155 SegIDs: []int{1}, 156 }, 157 { 158 SegTax: 601, 159 SegClass: "2", 160 SegIDs: []int{1}, 161 }, 162 }, 163 wantError: nil, 164 }, 165 { 166 name: "header with one valid field and another invalid field (w/o segIDs), should return only one valid field", 167 args: args{secBrowsingTopics: "(1);v=chrome.1:2:3, ();v=chrome.1:2:3, ();p=P0000000000"}, 168 wantTopic: []Topic{ 169 { 170 SegTax: 601, 171 SegClass: "3", 172 SegIDs: []int{1}, 173 }, 174 }, 175 wantError: []error{ 176 &errortypes.DebugWarning{ 177 Message: "Invalid field in Sec-Browsing-Topics header: ();v=chrome.1:2:3", 178 WarningCode: errortypes.SecBrowsingTopicsWarningCode, 179 }, 180 }, 181 }, 182 { 183 name: "header with two valid fields having different model version", 184 args: args{secBrowsingTopics: "(1);v=chrome.1:2:3, (2);v=chrome.1:2:3, ();p=P0000000000"}, 185 wantTopic: []Topic{ 186 { 187 SegTax: 601, 188 SegClass: "3", 189 SegIDs: []int{1}, 190 }, 191 { 192 SegTax: 601, 193 SegClass: "3", 194 SegIDs: []int{2}, 195 }, 196 }, 197 wantError: nil, 198 }, 199 { 200 name: "header with one valid fields and two invalid fields (one with taxanomy < 0 and another with taxanomy > 10), should return only one valid field", 201 args: args{secBrowsingTopics: "(1);v=chrome.1:11:2, (1);v=chrome.1:5:6, (1);v=chrome.1:0:2, ();p=P0000000000"}, 202 wantTopic: []Topic{ 203 { 204 SegTax: 604, 205 SegClass: "6", 206 SegIDs: []int{1}, 207 }, 208 }, 209 wantError: []error{ 210 &errortypes.DebugWarning{ 211 Message: "Invalid field in Sec-Browsing-Topics header: (1);v=chrome.1:11:2", 212 WarningCode: errortypes.SecBrowsingTopicsWarningCode, 213 }, 214 &errortypes.DebugWarning{ 215 Message: "Invalid field in Sec-Browsing-Topics header: (1);v=chrome.1:0:2", 216 WarningCode: errortypes.SecBrowsingTopicsWarningCode, 217 }, 218 }, 219 }, 220 { 221 name: "header with with valid fields having special characters (whitespaces, etc)", 222 args: args{secBrowsingTopics: "(1 2 4 6 7 4567 ) ; v=chrome.1: 1 : 2, (1);v=chrome.1, ();p=P0000000000"}, 223 wantTopic: []Topic{ 224 { 225 SegTax: 600, 226 SegClass: "2", 227 SegIDs: []int{1, 2, 4, 6, 7, 4567}, 228 }, 229 }, 230 wantError: []error{ 231 &errortypes.DebugWarning{ 232 Message: "Invalid field in Sec-Browsing-Topics header: (1);v=chrome.1", 233 WarningCode: errortypes.SecBrowsingTopicsWarningCode, 234 }, 235 }, 236 }, 237 { 238 name: "header with one valid field having a negative segId, drop field", 239 args: args{secBrowsingTopics: "(1 -3);v=chrome.1:1:2, ();p=P00000000000"}, 240 wantTopic: []Topic{}, 241 wantError: []error{ 242 &errortypes.DebugWarning{ 243 Message: "Invalid field in Sec-Browsing-Topics header: (1 -3);v=chrome.1:1:2", 244 WarningCode: errortypes.SecBrowsingTopicsWarningCode, 245 }, 246 }, 247 }, 248 { 249 name: "header with one valid field having a segId=0, drop field", 250 args: args{secBrowsingTopics: "(1 0);v=chrome.1:1:2, ();p=P00000000000"}, 251 wantTopic: []Topic{}, 252 wantError: []error{ 253 &errortypes.DebugWarning{ 254 Message: "Invalid field in Sec-Browsing-Topics header: (1 0);v=chrome.1:1:2", 255 WarningCode: errortypes.SecBrowsingTopicsWarningCode, 256 }, 257 }, 258 }, 259 { 260 name: "header with one valid field having a segId value more than MaxInt, drop field", 261 args: args{secBrowsingTopics: "(1 9223372036854775808);v=chrome.1:1:2, ();p=P00000000000"}, 262 wantTopic: []Topic{}, 263 wantError: []error{ 264 &errortypes.DebugWarning{ 265 Message: "Invalid field in Sec-Browsing-Topics header: (1 9223372036854775808);v=chrome.1:1:2", 266 WarningCode: errortypes.SecBrowsingTopicsWarningCode, 267 }, 268 }, 269 }, 270 } 271 for _, tt := range tests { 272 t.Run(tt.name, func(t *testing.T) { 273 gotTopic, gotError := ParseTopicsFromHeader(tt.args.secBrowsingTopics) 274 assert.Equal(t, tt.wantTopic, gotTopic) 275 assert.Equal(t, tt.wantError, gotError) 276 }) 277 } 278 } 279 280 func TestUpdateUserDataWithTopics(t *testing.T) { 281 type args struct { 282 userData []openrtb2.Data 283 headerData []Topic 284 topicsDomain string 285 } 286 tests := []struct { 287 name string 288 args args 289 want []openrtb2.Data 290 }{ 291 { 292 name: "empty topics, empty user data, no change in user data", 293 args: args{ 294 userData: nil, 295 headerData: nil, 296 }, 297 want: nil, 298 }, 299 { 300 name: "empty topics, non-empty user data, no change in user data", 301 args: args{ 302 userData: []openrtb2.Data{ 303 { 304 ID: "1", 305 Name: "data1", 306 Segment: []openrtb2.Segment{ 307 {ID: "1"}, 308 {ID: "2"}, 309 }, 310 }, 311 }, 312 headerData: nil, 313 }, 314 want: []openrtb2.Data{ 315 { 316 ID: "1", 317 Name: "data1", 318 Segment: []openrtb2.Segment{ 319 {ID: "1"}, 320 {ID: "2"}, 321 }, 322 }, 323 }, 324 }, 325 { 326 name: "topicsDomain empty, no change in user data", 327 args: args{ 328 userData: []openrtb2.Data{ 329 { 330 ID: "1", 331 Name: "data1", 332 Segment: []openrtb2.Segment{ 333 {ID: "1"}, 334 {ID: "2"}, 335 }, 336 }, 337 }, 338 headerData: []Topic{ 339 { 340 SegTax: 600, 341 SegClass: "2", 342 SegIDs: []int{1, 2}, 343 }, 344 }, 345 topicsDomain: "", 346 }, 347 want: []openrtb2.Data{ 348 { 349 ID: "1", 350 Name: "data1", 351 Segment: []openrtb2.Segment{ 352 {ID: "1"}, 353 {ID: "2"}, 354 }, 355 }, 356 }, 357 }, 358 { 359 name: "non-empty topics, empty user data, topics from header copied to user data", 360 args: args{ 361 userData: nil, 362 headerData: []Topic{ 363 { 364 SegTax: 600, 365 SegClass: "2", 366 SegIDs: []int{1, 2}, 367 }, 368 }, 369 topicsDomain: "ads.pubmatic.com", 370 }, 371 want: []openrtb2.Data{ 372 { 373 Name: "ads.pubmatic.com", 374 Segment: []openrtb2.Segment{ 375 {ID: "1"}, 376 {ID: "2"}, 377 }, 378 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 379 }, 380 }, 381 }, 382 { 383 name: "non-empty topics, non-empty user data, topics from header copied to user data", 384 args: args{ 385 userData: []openrtb2.Data{ 386 { 387 ID: "1", 388 Name: "data1", 389 Segment: []openrtb2.Segment{ 390 {ID: "1"}, 391 {ID: "2"}, 392 }, 393 }, 394 }, 395 headerData: []Topic{ 396 { 397 SegTax: 600, 398 SegClass: "2", 399 SegIDs: []int{3, 4}, 400 }, 401 }, 402 topicsDomain: "ads.pubmatic.com", 403 }, 404 want: []openrtb2.Data{ 405 { 406 ID: "1", 407 Name: "data1", 408 Segment: []openrtb2.Segment{ 409 {ID: "1"}, 410 {ID: "2"}, 411 }, 412 }, 413 { 414 Name: "ads.pubmatic.com", 415 Segment: []openrtb2.Segment{ 416 {ID: "3"}, 417 {ID: "4"}, 418 }, 419 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 420 }, 421 }, 422 }, 423 { 424 name: "non-empty topics, user data with invalid data.ext field, topics from header copied to user data", 425 args: args{ 426 userData: []openrtb2.Data{ 427 { 428 ID: "1", 429 Name: "data1", 430 Segment: []openrtb2.Segment{ 431 {ID: "1"}, 432 {ID: "2"}, 433 }, 434 Ext: json.RawMessage(`{`), 435 }, 436 }, 437 headerData: []Topic{ 438 { 439 SegTax: 600, 440 SegClass: "2", 441 SegIDs: []int{3, 4}, 442 }, 443 }, 444 topicsDomain: "ads.pubmatic.com", 445 }, 446 want: []openrtb2.Data{ 447 { 448 ID: "1", 449 Name: "data1", 450 Segment: []openrtb2.Segment{ 451 {ID: "1"}, 452 {ID: "2"}, 453 }, 454 Ext: json.RawMessage(`{`), 455 }, 456 { 457 Name: "ads.pubmatic.com", 458 Segment: []openrtb2.Segment{ 459 {ID: "3"}, 460 {ID: "4"}, 461 }, 462 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 463 }, 464 }, 465 }, 466 { 467 name: "non-empty topics, user data with invalid topic details (invalid segtax and segclass), topics from header copied to user data", 468 args: args{ 469 userData: []openrtb2.Data{ 470 { 471 ID: "1", 472 Name: "chrome.com", 473 Segment: []openrtb2.Segment{ 474 {ID: "1"}, 475 {ID: "2"}, 476 }, 477 Ext: json.RawMessage(`{"segtax":0,"segclass":""}`), 478 }, 479 }, 480 headerData: []Topic{ 481 { 482 SegTax: 600, 483 SegClass: "2", 484 SegIDs: []int{3, 4}, 485 }, 486 }, 487 topicsDomain: "ads.pubmatic.com", 488 }, 489 want: []openrtb2.Data{ 490 { 491 ID: "1", 492 Name: "chrome.com", 493 Segment: []openrtb2.Segment{ 494 {ID: "1"}, 495 {ID: "2"}, 496 }, 497 Ext: json.RawMessage(`{"segtax":0,"segclass":""}`), 498 }, 499 { 500 Name: "ads.pubmatic.com", 501 Segment: []openrtb2.Segment{ 502 {ID: "3"}, 503 {ID: "4"}, 504 }, 505 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 506 }, 507 }, 508 }, 509 { 510 name: "non-empty topics, user data with non matching topic details (different topicdomains, segtax and segclass), topics from header copied to user data", 511 args: args{ 512 userData: []openrtb2.Data{ 513 { 514 ID: "1", 515 Name: "chrome.com", 516 Segment: []openrtb2.Segment{ 517 {ID: "1"}, 518 {ID: "2"}, 519 }, 520 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 521 }, 522 { 523 ID: "2", 524 Name: "ads.pubmatic.com", 525 Segment: []openrtb2.Segment{ 526 {ID: "5"}, 527 {ID: "6"}, 528 }, 529 Ext: json.RawMessage(`{"segtax":601,"segclass":"3"}`), 530 }, 531 { 532 ID: "3", 533 Name: "ads.pubmatic.com", 534 Segment: []openrtb2.Segment{ 535 {ID: "7"}, 536 {ID: "8"}, 537 }, 538 Ext: json.RawMessage(`{"segtax":602,"segclass":"4"}`), 539 }, 540 }, 541 headerData: []Topic{ 542 { 543 SegTax: 600, 544 SegClass: "2", 545 SegIDs: []int{3, 4}, 546 }, 547 { 548 SegTax: 602, 549 SegClass: "2", 550 SegIDs: []int{3, 4}, 551 }, 552 }, 553 topicsDomain: "ads.pubmatic.com", 554 }, 555 want: []openrtb2.Data{ 556 { 557 ID: "1", 558 Name: "chrome.com", 559 Segment: []openrtb2.Segment{ 560 {ID: "1"}, 561 {ID: "2"}, 562 }, 563 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 564 }, 565 { 566 ID: "2", 567 Name: "ads.pubmatic.com", 568 Segment: []openrtb2.Segment{ 569 {ID: "5"}, 570 {ID: "6"}, 571 }, 572 Ext: json.RawMessage(`{"segtax":601,"segclass":"3"}`), 573 }, 574 { 575 ID: "3", 576 Name: "ads.pubmatic.com", 577 Segment: []openrtb2.Segment{ 578 {ID: "7"}, 579 {ID: "8"}, 580 }, 581 Ext: json.RawMessage(`{"segtax":602,"segclass":"4"}`), 582 }, 583 { 584 Name: "ads.pubmatic.com", 585 Segment: []openrtb2.Segment{ 586 {ID: "3"}, 587 {ID: "4"}, 588 }, 589 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 590 }, 591 { 592 Name: "ads.pubmatic.com", 593 Segment: []openrtb2.Segment{ 594 {ID: "3"}, 595 {ID: "4"}, 596 }, 597 Ext: json.RawMessage(`{"segtax":602,"segclass":"2"}`), 598 }, 599 }, 600 }, 601 { 602 name: "non-empty topics, user data with same topic details (matching segtax and segclass), topics from header merged with user data (filter unique segIDs)", 603 args: args{ 604 userData: []openrtb2.Data{ 605 { 606 ID: "1", 607 Name: "ads.pubmatic.com", 608 Segment: []openrtb2.Segment{ 609 {ID: "1"}, 610 {ID: "2"}, 611 {ID: "3"}, 612 }, 613 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 614 }, 615 }, 616 headerData: []Topic{ 617 { 618 SegTax: 600, 619 SegClass: "2", 620 SegIDs: []int{2, 3, 4}, 621 }, 622 }, 623 topicsDomain: "ads.pubmatic.com", 624 }, 625 want: []openrtb2.Data{ 626 { 627 ID: "1", 628 Name: "ads.pubmatic.com", 629 Segment: []openrtb2.Segment{ 630 {ID: "1"}, 631 {ID: "2"}, 632 {ID: "3"}, 633 {ID: "4"}, 634 }, 635 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 636 }, 637 }, 638 }, 639 { 640 name: "non-empty topics, user data with duplicate topic details (matching segtax and segclass and segIDs), topics from header merged with user data (filter unique segIDs), user.data will not be deduped", 641 args: args{ 642 userData: []openrtb2.Data{ 643 { 644 ID: "1", 645 Name: "ads.pubmatic.com", 646 Segment: []openrtb2.Segment{ 647 {ID: "1"}, 648 {ID: "2"}, 649 {ID: "3"}, 650 }, 651 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 652 }, 653 { 654 ID: "1", 655 Name: "ads.pubmatic.com", 656 Segment: []openrtb2.Segment{ 657 {ID: "1"}, 658 {ID: "2"}, 659 {ID: "3"}, 660 }, 661 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 662 }, 663 }, 664 headerData: []Topic{ 665 { 666 SegTax: 600, 667 SegClass: "2", 668 SegIDs: []int{2, 3, 4}, 669 }, 670 }, 671 topicsDomain: "ads.pubmatic.com", 672 }, 673 want: []openrtb2.Data{ 674 { 675 ID: "1", 676 Name: "ads.pubmatic.com", 677 Segment: []openrtb2.Segment{ 678 {ID: "1"}, 679 {ID: "2"}, 680 {ID: "3"}, 681 {ID: "4"}, 682 }, 683 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 684 }, 685 { 686 ID: "1", 687 Name: "ads.pubmatic.com", 688 Segment: []openrtb2.Segment{ 689 {ID: "1"}, 690 {ID: "2"}, 691 {ID: "3"}, 692 }, 693 Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), 694 }, 695 }, 696 }, 697 } 698 for _, tt := range tests { 699 t.Run(tt.name, func(t *testing.T) { 700 got := UpdateUserDataWithTopics(tt.args.userData, tt.args.headerData, tt.args.topicsDomain) 701 sort.Slice(got, func(i, j int) bool { 702 if got[i].Name == got[j].Name { 703 return string(got[i].Ext) < string(got[j].Ext) 704 } 705 return got[i].Name < got[j].Name 706 }) 707 sort.Slice(tt.want, func(i, j int) bool { 708 if tt.want[i].Name == tt.want[j].Name { 709 return string(tt.want[i].Ext) < string(tt.want[j].Ext) 710 } 711 return tt.want[i].Name < tt.want[j].Name 712 }) 713 714 for g := range got { 715 sort.Slice(got[g].Segment, func(i, j int) bool { 716 return got[g].Segment[i].ID < got[g].Segment[j].ID 717 }) 718 } 719 assert.Equal(t, tt.want, got, tt.name) 720 }) 721 } 722 }