github.com/prebid/prebid-server/v2@v2.18.0/analytics/agma/agma_module_test.go (about) 1 package agma 2 3 import ( 4 "encoding/json" 5 "io" 6 "net/http" 7 "net/http/httptest" 8 "sync" 9 "syscall" 10 "testing" 11 "time" 12 13 "github.com/benbjohnson/clock" 14 "github.com/prebid/openrtb/v20/openrtb2" 15 "github.com/prebid/prebid-server/v2/analytics" 16 "github.com/prebid/prebid-server/v2/config" 17 "github.com/prebid/prebid-server/v2/openrtb_ext" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/mock" 20 ) 21 22 var agmaConsent = "CP6-v9RP6-v9RNlAAAENCZCAAICAAAAAAAAAIxQAQIxAAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A" 23 24 var mockValidAuctionObject = analytics.AuctionObject{ 25 Status: http.StatusOK, 26 StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), 27 RequestWrapper: &openrtb_ext.RequestWrapper{ 28 BidRequest: &openrtb2.BidRequest{ 29 ID: "some-id", 30 Site: &openrtb2.Site{ 31 ID: "track-me-site", 32 Publisher: &openrtb2.Publisher{ 33 ID: "track-me", 34 }, 35 }, 36 Device: &openrtb2.Device{ 37 UA: "ua", 38 }, 39 User: &openrtb2.User{ 40 Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), 41 }, 42 }, 43 }, 44 } 45 46 var mockValidVideoObject = analytics.VideoObject{ 47 Status: http.StatusOK, 48 StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), 49 RequestWrapper: &openrtb_ext.RequestWrapper{ 50 BidRequest: &openrtb2.BidRequest{ 51 ID: "some-id", 52 App: &openrtb2.App{ 53 ID: "track-me-app", 54 Publisher: &openrtb2.Publisher{ 55 ID: "track-me", 56 }, 57 }, 58 Device: &openrtb2.Device{ 59 UA: "ua", 60 }, 61 User: &openrtb2.User{ 62 Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), 63 }, 64 }, 65 }, 66 } 67 68 var mockValidAmpObject = analytics.AmpObject{ 69 Status: http.StatusOK, 70 StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), 71 RequestWrapper: &openrtb_ext.RequestWrapper{ 72 BidRequest: &openrtb2.BidRequest{ 73 ID: "some-id", 74 Site: &openrtb2.Site{ 75 ID: "track-me-site", 76 Publisher: &openrtb2.Publisher{ 77 ID: "track-me", 78 }, 79 }, 80 Device: &openrtb2.Device{ 81 UA: "ua", 82 }, 83 User: &openrtb2.User{ 84 Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), 85 }, 86 }, 87 }, 88 } 89 90 var mockValidAccounts = []config.AgmaAnalyticsAccount{ 91 { 92 PublisherId: "track-me", 93 Code: "abc", 94 SiteAppId: "track-me-app", 95 }, 96 { 97 PublisherId: "track-me", 98 Code: "abcd", 99 SiteAppId: "track-me-site", 100 }, 101 } 102 103 type MockedSender struct { 104 mock.Mock 105 } 106 107 func (m *MockedSender) Send(payload []byte) error { 108 args := m.Called(payload) 109 return args.Error(0) 110 } 111 112 func TestConfigParsingError(t *testing.T) { 113 testCases := []struct { 114 name string 115 config config.AgmaAnalytics 116 shouldFail bool 117 }{ 118 { 119 name: "Test with invalid/empty URL", 120 config: config.AgmaAnalytics{ 121 Enabled: true, 122 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 123 Url: "%%2815197306101420000%29", 124 Timeout: "1s", 125 Gzip: false, 126 }, 127 }, 128 shouldFail: true, 129 }, 130 { 131 name: "Test with invalid timout", 132 config: config.AgmaAnalytics{ 133 Enabled: true, 134 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 135 Url: "http://localhost:8000/event", 136 Timeout: "1x", 137 Gzip: false, 138 }, 139 }, 140 shouldFail: true, 141 }, 142 { 143 name: "Test with no accounts", 144 config: config.AgmaAnalytics{ 145 Enabled: true, 146 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 147 Url: "http://localhost:8000/event", 148 Timeout: "1s", 149 Gzip: false, 150 }, 151 Buffers: config.AgmaAnalyticsBuffer{ 152 EventCount: 1, 153 BufferSize: "1Kb", 154 Timeout: "1s", 155 }, 156 Accounts: []config.AgmaAnalyticsAccount{}, 157 }, 158 shouldFail: true, 159 }, 160 } 161 clockMock := clock.NewMock() 162 for _, tc := range testCases { 163 t.Run(tc.name, func(t *testing.T) { 164 _, err := NewModule(&http.Client{}, tc.config, clockMock) 165 if tc.shouldFail { 166 assert.Error(t, err) 167 } else { 168 assert.NoError(t, err) 169 } 170 }) 171 } 172 } 173 174 func TestShouldTrackEvent(t *testing.T) { 175 cfg := config.AgmaAnalytics{ 176 Enabled: true, 177 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 178 Url: "http://localhost:8000/event", 179 Timeout: "5s", 180 }, 181 Buffers: config.AgmaAnalyticsBuffer{ 182 EventCount: 1, 183 BufferSize: "1Kb", 184 Timeout: "1s", 185 }, 186 Accounts: []config.AgmaAnalyticsAccount{ 187 { 188 PublisherId: "track-me", 189 Code: "abc", 190 }, 191 }, 192 } 193 mockedSender := new(MockedSender) 194 mockedSender.On("Send", mock.Anything).Return(nil) 195 clockMock := clock.NewMock() 196 logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) 197 assert.NoError(t, err) 198 199 // no userExt 200 shouldTrack, code := logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ 201 BidRequest: &openrtb2.BidRequest{ 202 ID: "some-id", 203 App: &openrtb2.App{ 204 ID: "com.app.test", 205 Publisher: &openrtb2.Publisher{ 206 ID: "track-me-not", 207 }, 208 }, 209 User: &openrtb2.User{ 210 Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), 211 }, 212 }, 213 }) 214 215 assert.False(t, shouldTrack) 216 assert.Equal(t, "", code) 217 218 // no userExt 219 shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ 220 BidRequest: &openrtb2.BidRequest{ 221 App: &openrtb2.App{ 222 ID: "com.app.test", 223 Publisher: &openrtb2.Publisher{ 224 ID: "track-me", 225 }, 226 }, 227 }, 228 }) 229 230 assert.False(t, shouldTrack) 231 assert.Equal(t, "", code) 232 233 // Constent: No agma 234 shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ 235 BidRequest: &openrtb2.BidRequest{ 236 App: &openrtb2.App{ 237 ID: "com.app.test", 238 Publisher: &openrtb2.Publisher{ 239 ID: "track-me", 240 }, 241 }, 242 User: &openrtb2.User{ 243 Ext: json.RawMessage(`{"consent": "CP4LywcP4LywcLRAAAENCZCAAAIAAAIAAAAAIxQAQIwgAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A"}`), 244 }, 245 }, 246 }) 247 248 assert.False(t, shouldTrack) 249 assert.Equal(t, "", code) 250 251 // Constent: No Purpose 9 252 shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ 253 BidRequest: &openrtb2.BidRequest{ 254 App: &openrtb2.App{ 255 ID: "com.app.test", 256 Publisher: &openrtb2.Publisher{ 257 ID: "track-me", 258 }, 259 }, 260 User: &openrtb2.User{ 261 Ext: json.RawMessage(`{"consent": "CP4LywcP4LywcLRAAAENCZCAAIAAAAAAAAAAIxQAQIxAAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A"}`), 262 }, 263 }, 264 }) 265 266 assert.False(t, shouldTrack) 267 assert.Equal(t, "", code) 268 269 // No valid sites / apps / empty publisher app 270 shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ 271 BidRequest: &openrtb2.BidRequest{ 272 App: &openrtb2.App{ 273 ID: "", 274 Publisher: &openrtb2.Publisher{ 275 ID: "", 276 }, 277 }, 278 User: &openrtb2.User{ 279 Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), 280 }, 281 }, 282 }) 283 284 assert.False(t, shouldTrack) 285 assert.Equal(t, "", code) 286 } 287 288 func TestShouldTrackMultipleAccounts(t *testing.T) { 289 cfg := config.AgmaAnalytics{ 290 Enabled: true, 291 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 292 Url: "http://localhost:8000/event", 293 Timeout: "5s", 294 }, 295 Buffers: config.AgmaAnalyticsBuffer{ 296 EventCount: 1, 297 BufferSize: "1Kb", 298 Timeout: "1s", 299 }, 300 Accounts: []config.AgmaAnalyticsAccount{ 301 { 302 PublisherId: "track-me-a", 303 Code: "abc", 304 }, 305 { 306 PublisherId: "track-me-b", 307 Code: "123", 308 }, 309 }, 310 } 311 mockedSender := new(MockedSender) 312 mockedSender.On("Send", mock.Anything).Return(nil) 313 clockMock := clock.NewMock() 314 logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) 315 assert.NoError(t, err) 316 317 shouldTrack, code := logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ 318 BidRequest: &openrtb2.BidRequest{ 319 ID: "some-id", 320 App: &openrtb2.App{ 321 ID: "com.app.test", 322 Publisher: &openrtb2.Publisher{ 323 ID: "track-me-a", 324 }, 325 }, 326 User: &openrtb2.User{ 327 Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), 328 }, 329 }, 330 }) 331 332 assert.True(t, shouldTrack) 333 assert.Equal(t, "abc", code) 334 335 shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ 336 BidRequest: &openrtb2.BidRequest{ 337 ID: "some-id", 338 Site: &openrtb2.Site{ 339 ID: "site-test", 340 Publisher: &openrtb2.Publisher{ 341 ID: "track-me-b", 342 }, 343 }, 344 User: &openrtb2.User{ 345 Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), 346 }, 347 }, 348 }) 349 350 assert.True(t, shouldTrack) 351 assert.Equal(t, "123", code) 352 } 353 354 func TestShouldNotTrackLog(t *testing.T) { 355 testCases := []struct { 356 name string 357 config config.AgmaAnalytics 358 }{ 359 { 360 name: "Test with do-not-track PublisherId", 361 config: config.AgmaAnalytics{ 362 Enabled: true, 363 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 364 Url: "http://localhost:8000/event", 365 Timeout: "5s", 366 }, 367 Buffers: config.AgmaAnalyticsBuffer{ 368 EventCount: 1, 369 BufferSize: "1Kb", 370 Timeout: "1s", 371 }, 372 Accounts: []config.AgmaAnalyticsAccount{ 373 { 374 PublisherId: "do-not-track", 375 Code: "abc", 376 }, 377 }, 378 }, 379 }, 380 { 381 name: "Test with do-not-track PublisherId", 382 config: config.AgmaAnalytics{ 383 Enabled: true, 384 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 385 Url: "http://localhost:8000/event", 386 Timeout: "5s", 387 }, 388 Buffers: config.AgmaAnalyticsBuffer{ 389 EventCount: 1, 390 BufferSize: "1Kb", 391 Timeout: "1s", 392 }, 393 Accounts: []config.AgmaAnalyticsAccount{ 394 { 395 PublisherId: "track-me", 396 Code: "abc", 397 SiteAppId: "do-not-track", 398 }, 399 }, 400 }, 401 }, 402 } 403 for _, tc := range testCases { 404 t.Run(tc.name, func(t *testing.T) { 405 mockedSender := new(MockedSender) 406 mockedSender.On("Send", mock.Anything).Return(nil) 407 clockMock := clock.NewMock() 408 logger, err := newAgmaLogger(tc.config, mockedSender.Send, clockMock) 409 assert.NoError(t, err) 410 411 go logger.start() 412 assert.Zero(t, logger.eventCount) 413 414 logger.LogAuctionObject(&mockValidAuctionObject) 415 logger.LogVideoObject(&mockValidVideoObject) 416 logger.LogAmpObject(&mockValidAmpObject) 417 418 clockMock.Add(2 * time.Minute) 419 mockedSender.AssertNumberOfCalls(t, "Send", 0) 420 assert.Zero(t, logger.eventCount) 421 }) 422 } 423 } 424 425 func TestRaceAllEvents(t *testing.T) { 426 cfg := config.AgmaAnalytics{ 427 Enabled: true, 428 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 429 Url: "http://localhost:8000/event", 430 Timeout: "5s", 431 }, 432 Buffers: config.AgmaAnalyticsBuffer{ 433 EventCount: 10000, 434 BufferSize: "100Mb", 435 Timeout: "5m", 436 }, 437 Accounts: mockValidAccounts, 438 } 439 mockedSender := new(MockedSender) 440 mockedSender.On("Send", mock.Anything).Return(nil) 441 clockMock := clock.NewMock() 442 logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) 443 assert.NoError(t, err) 444 445 go logger.start() 446 447 logger.LogAuctionObject(&mockValidAuctionObject) 448 logger.LogVideoObject(&mockValidVideoObject) 449 logger.LogAmpObject(&mockValidAmpObject) 450 clockMock.Add(10 * time.Millisecond) 451 452 logger.mux.RLock() 453 assert.Equal(t, int64(3), logger.eventCount) 454 logger.mux.RUnlock() 455 } 456 457 func TestFlushOnSigterm(t *testing.T) { 458 cfg := config.AgmaAnalytics{ 459 Enabled: true, 460 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 461 Url: "http://localhost:8000/event", 462 Timeout: "5s", 463 }, 464 Buffers: config.AgmaAnalyticsBuffer{ 465 EventCount: 10000, 466 BufferSize: "100Mb", 467 Timeout: "5m", 468 }, 469 Accounts: mockValidAccounts, 470 } 471 mockedSender := new(MockedSender) 472 mockedSender.On("Send", mock.Anything).Return(nil) 473 clockMock := clock.NewMock() 474 logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) 475 assert.NoError(t, err) 476 477 done := make(chan struct{}) 478 go func() { 479 logger.start() 480 close(done) 481 }() 482 483 logger.LogAuctionObject(&mockValidAuctionObject) 484 logger.LogVideoObject(&mockValidVideoObject) 485 logger.LogAmpObject(&mockValidAmpObject) 486 487 logger.sigTermCh <- syscall.SIGTERM 488 <-done 489 490 time.Sleep(100 * time.Millisecond) 491 492 mockedSender.AssertCalled(t, "Send", mock.Anything) 493 } 494 495 func TestRaceBufferCount(t *testing.T) { 496 cfg := config.AgmaAnalytics{ 497 Enabled: true, 498 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 499 Url: "http://localhost:8000/event", 500 Timeout: "5s", 501 }, 502 Buffers: config.AgmaAnalyticsBuffer{ 503 EventCount: 2, 504 BufferSize: "100Mb", 505 Timeout: "5m", 506 }, 507 Accounts: []config.AgmaAnalyticsAccount{ 508 { 509 PublisherId: "track-me", 510 Code: "abc", 511 }, 512 }, 513 } 514 mockedSender := new(MockedSender) 515 mockedSender.On("Send", mock.Anything).Return(nil) 516 clockMock := clock.NewMock() 517 logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) 518 assert.NoError(t, err) 519 520 go logger.start() 521 assert.Zero(t, logger.eventCount) 522 523 // Test EventCount Buffer 524 logger.LogAuctionObject(&mockValidAuctionObject) 525 526 clockMock.Add(1 * time.Millisecond) 527 528 logger.mux.RLock() 529 assert.Equal(t, int64(1), logger.eventCount) 530 logger.mux.RUnlock() 531 532 assert.Equal(t, false, logger.isFull()) 533 534 // add 1 more 535 logger.LogAuctionObject(&mockValidAuctionObject) 536 clockMock.Add(1 * time.Millisecond) 537 538 // should trigger send and flash the buffer 539 mockedSender.AssertCalled(t, "Send", mock.Anything) 540 541 logger.mux.RLock() 542 assert.Equal(t, int64(0), logger.eventCount) 543 logger.mux.RUnlock() 544 } 545 546 func TestBufferSize(t *testing.T) { 547 cfg := config.AgmaAnalytics{ 548 Enabled: true, 549 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 550 Url: "http://localhost:8000/event", 551 Timeout: "5s", 552 }, 553 Buffers: config.AgmaAnalyticsBuffer{ 554 EventCount: 1000, 555 BufferSize: "20Kb", 556 Timeout: "5m", 557 }, 558 Accounts: []config.AgmaAnalyticsAccount{ 559 { 560 PublisherId: "track-me", 561 Code: "abc", 562 }, 563 }, 564 } 565 mockedSender := new(MockedSender) 566 mockedSender.On("Send", mock.Anything).Return(nil) 567 clockMock := clock.NewMock() 568 logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) 569 assert.NoError(t, err) 570 571 go logger.start() 572 573 for i := 0; i < 50; i++ { 574 logger.LogAuctionObject(&mockValidAuctionObject) 575 } 576 clockMock.Add(10 * time.Millisecond) 577 mockedSender.AssertCalled(t, "Send", mock.Anything) 578 mockedSender.AssertNumberOfCalls(t, "Send", 1) 579 } 580 581 func TestBufferTime(t *testing.T) { 582 cfg := config.AgmaAnalytics{ 583 Enabled: true, 584 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 585 Url: "http://localhost:8000/event", 586 Timeout: "5s", 587 }, 588 Buffers: config.AgmaAnalyticsBuffer{ 589 EventCount: 1000, 590 BufferSize: "100mb", 591 Timeout: "5m", 592 }, 593 Accounts: []config.AgmaAnalyticsAccount{ 594 { 595 PublisherId: "track-me", 596 Code: "abc", 597 }, 598 }, 599 } 600 mockedSender := new(MockedSender) 601 mockedSender.On("Send", mock.Anything).Return(nil) 602 clockMock := clock.NewMock() 603 logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) 604 assert.NoError(t, err) 605 606 go logger.start() 607 608 for i := 0; i < 5; i++ { 609 logger.LogAuctionObject(&mockValidAuctionObject) 610 } 611 clockMock.Add(10 * time.Minute) 612 mockedSender.AssertCalled(t, "Send", mock.Anything) 613 mockedSender.AssertNumberOfCalls(t, "Send", 1) 614 } 615 616 func TestRaceEnd2End(t *testing.T) { 617 var mu sync.Mutex 618 619 requestBodyAsString := "" 620 621 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 622 // Check for reponse 623 requestBody, err := io.ReadAll(r.Body) 624 mu.Lock() 625 requestBodyAsString = string(requestBody) 626 mu.Unlock() 627 if err != nil { 628 http.Error(w, "Error reading request body", 500) 629 return 630 } 631 632 w.WriteHeader(http.StatusOK) 633 })) 634 cfg := config.AgmaAnalytics{ 635 Enabled: true, 636 Endpoint: config.AgmaAnalyticsHttpEndpoint{ 637 Url: server.URL, 638 Timeout: "5s", 639 }, 640 Buffers: config.AgmaAnalyticsBuffer{ 641 EventCount: 2, 642 BufferSize: "100mb", 643 Timeout: "5m", 644 }, 645 Accounts: mockValidAccounts, 646 } 647 648 clockMock := clock.NewMock() 649 clockMock.Set(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)) 650 651 logger, err := NewModule(&http.Client{}, cfg, clockMock) 652 assert.NoError(t, err) 653 654 logger.LogAmpObject(&mockValidAmpObject) 655 logger.LogAmpObject(&mockValidAmpObject) 656 657 time.Sleep(250 * time.Millisecond) 658 659 expected := "[{\"type\":\"amp\",\"id\":\"some-id\",\"code\":\"abcd\",\"site\":{\"id\":\"track-me-site\",\"publisher\":{\"id\":\"track-me\"}},\"device\":{\"ua\":\"ua\"},\"user\":{\"ext\":{\"consent\": \"" + agmaConsent + "\"}},\"created_at\":\"2023-02-01T00:00:00Z\"},{\"type\":\"amp\",\"id\":\"some-id\",\"code\":\"abcd\",\"site\":{\"id\":\"track-me-site\",\"publisher\":{\"id\":\"track-me\"}},\"device\":{\"ua\":\"ua\"},\"user\":{\"ext\":{\"consent\": \"" + agmaConsent + "\"}},\"created_at\":\"2023-02-01T00:00:00Z\"}]" 660 661 mu.Lock() 662 actual := requestBodyAsString 663 mu.Unlock() 664 665 assert.Equal(t, expected, actual) 666 }