github.com/grafana/synthetic-monitoring-agent@v0.64.2/pkg/pb/synthetic_monitoring/checks_extra_test.go (about) 1 package synthetic_monitoring 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "flag" 8 "reflect" 9 "slices" 10 "strconv" 11 "strings" 12 "testing" 13 14 "github.com/rs/zerolog" 15 "github.com/stretchr/testify/require" 16 ) 17 18 var testDebugOutput = flag.Bool("test.debug-output", false, "include test debug output") 19 20 func TestCheckValidate(t *testing.T) { 21 type TestCase struct { 22 input Check 23 expectError bool 24 } 25 26 testcases := map[string]TestCase{ 27 "invalid tenant": { 28 input: Check{ 29 Id: 1, 30 TenantId: BadID, 31 Target: "127.0.0.1", 32 Job: "job", 33 Frequency: 1000, 34 Timeout: 1000, 35 Probes: []int64{1}, 36 Settings: CheckSettings{ 37 Ping: &PingSettings{}, 38 }, 39 }, 40 expectError: true, 41 }, 42 "invalid label": { 43 input: Check{ 44 Id: 1, 45 TenantId: 1, 46 Target: "127.0.0.1", 47 Job: "job", 48 Frequency: 1000, 49 Timeout: 1000, 50 Probes: []int64{1}, 51 Settings: CheckSettings{ 52 Ping: &PingSettings{}, 53 }, 54 Labels: []Label{{Name: "name ", Value: "value"}}, 55 }, 56 expectError: true, 57 }, 58 "duplicate label names": { 59 input: Check{ 60 Id: 1, 61 TenantId: 1, 62 Target: "127.0.0.1", 63 Job: "job", 64 Frequency: 1000, 65 Timeout: 1000, 66 Probes: []int64{1}, 67 Settings: CheckSettings{ 68 Ping: &PingSettings{}, 69 }, 70 Labels: []Label{ 71 {Name: "name", Value: "1"}, 72 {Name: "name", Value: "2"}, 73 }, 74 }, 75 expectError: true, 76 }, 77 "duplicate label values": { 78 input: Check{ 79 Id: 1, 80 TenantId: 1, 81 Target: "127.0.0.1", 82 Job: "job", 83 Frequency: 1000, 84 Timeout: 1000, 85 Probes: []int64{1}, 86 Settings: CheckSettings{ 87 Ping: &PingSettings{}, 88 }, 89 Labels: []Label{ 90 {Name: "name_1", Value: "1"}, 91 {Name: "name_2", Value: "1"}, 92 }, 93 }, 94 expectError: false, 95 }, 96 "multiple settings": { 97 input: Check{ 98 Id: 1, 99 TenantId: 1, 100 Target: "127.0.0.1", 101 Job: "job", 102 Frequency: 1000, 103 Timeout: 1000, 104 Probes: []int64{1}, 105 Settings: CheckSettings{ 106 Ping: &PingSettings{}, 107 Http: &HttpSettings{}, 108 }, 109 Labels: []Label{{Name: "name ", Value: "value"}}, 110 }, 111 expectError: true, 112 }, 113 "valid timeout & frequency": { // test for case when frequency > max timeout 114 input: Check{ 115 Id: 1, 116 TenantId: 1, 117 Target: "127.0.0.1", 118 Job: "job", 119 Frequency: 60000, // 60 seconds 120 Timeout: 5000, // 5 seconds 121 Probes: []int64{1}, 122 Settings: CheckSettings{ 123 Ping: &PingSettings{}, 124 }, 125 }, 126 expectError: false, 127 }, 128 "invalid timeout": { // issue #101 129 input: Check{ 130 Id: 1, 131 TenantId: 1, 132 Target: "127.0.0.1", 133 Job: "job", 134 Frequency: 1000, 135 Timeout: 1001, // timeout should be equal or less than frequency 136 Probes: []int64{1}, 137 Settings: CheckSettings{ 138 Ping: &PingSettings{}, 139 }, 140 }, 141 expectError: true, 142 }, 143 "invalid HTTP target": { 144 input: Check{ 145 Id: 1, 146 TenantId: 1, 147 Target: "ftp://example.org/", 148 Job: "job", 149 Frequency: 1000, 150 Timeout: 1000, 151 Probes: []int64{1}, 152 Settings: CheckSettings{ 153 Http: &HttpSettings{}, 154 }, 155 Labels: []Label{{Name: "name ", Value: "value"}}, 156 }, 157 expectError: true, 158 }, 159 "valid proxy URL": { 160 input: Check{ 161 Id: 1, 162 TenantId: 1, 163 Target: "http://example.org/", 164 Job: "job", 165 Frequency: 1000, 166 Timeout: 1000, 167 Probes: []int64{1}, 168 Settings: CheckSettings{ 169 Http: &HttpSettings{ 170 ProxyURL: "http://proxy.example.org/", 171 }, 172 }, 173 }, 174 expectError: false, 175 }, 176 "valid proxy URL and headers": { 177 input: Check{ 178 Id: 1, 179 TenantId: 1, 180 Target: "http://example.org/", 181 Job: "job", 182 Frequency: 1000, 183 Timeout: 1000, 184 Probes: []int64{1}, 185 Settings: CheckSettings{ 186 Http: &HttpSettings{ 187 ProxyURL: "http://proxy.example.org/", 188 ProxyConnectHeaders: []string{"h1: v1", "h2:v2"}, 189 }, 190 }, 191 }, 192 expectError: false, 193 }, 194 "proxy headers without url": { 195 input: Check{ 196 Id: 1, 197 TenantId: 1, 198 Target: "http://example.org/", 199 Job: "job", 200 Frequency: 1000, 201 Timeout: 1000, 202 Probes: []int64{1}, 203 Settings: CheckSettings{ 204 Http: &HttpSettings{ 205 ProxyConnectHeaders: []string{"h1: v1", "h2:v2"}, 206 }, 207 }, 208 }, 209 expectError: true, 210 }, 211 "valid HTTP check with long URL": { 212 input: Check{ 213 Id: 1, 214 TenantId: 1, 215 Target: "http://example.org/" + strings.Repeat("x", maxValidLabelValueLength-len("http://example.org/")), 216 Job: "job", 217 Frequency: 1000, 218 Timeout: 1000, 219 Probes: []int64{1}, 220 Settings: CheckSettings{ 221 Http: &HttpSettings{}, 222 }, 223 }, 224 expectError: false, 225 }, 226 "valid multihttp check": { 227 input: Check{ 228 Id: 1, 229 TenantId: 1, 230 Target: "https://example.org/", 231 Job: "job", 232 Frequency: 60000, 233 Timeout: 10000, 234 Probes: []int64{1}, 235 Settings: CheckSettings{ 236 Multihttp: &MultiHttpSettings{ 237 Entries: []*MultiHttpEntry{ 238 { 239 Request: &MultiHttpEntryRequest{ 240 Url: "https://example.org/", 241 }, 242 }, 243 }, 244 }, 245 }, 246 }, 247 expectError: false, 248 }, 249 "valid multihttp variable": { 250 input: Check{ 251 Id: 1, 252 TenantId: 1, 253 Target: "${variable}", 254 Job: "job", 255 Frequency: 60000, 256 Timeout: 10000, 257 Probes: []int64{1}, 258 Settings: CheckSettings{ 259 Multihttp: &MultiHttpSettings{ 260 Entries: []*MultiHttpEntry{ 261 { 262 Request: &MultiHttpEntryRequest{ 263 Url: "${variable}", 264 }, 265 }, 266 }, 267 }, 268 }, 269 }, 270 expectError: false, 271 }, 272 "valid multihttp with variable in second url": { 273 input: Check{ 274 Id: 1, 275 TenantId: 1, 276 Target: "https://www.example.com", 277 Job: "job", 278 Frequency: 60000, 279 Timeout: 10000, 280 Probes: []int64{1}, 281 Settings: CheckSettings{ 282 Multihttp: &MultiHttpSettings{ 283 Entries: []*MultiHttpEntry{ 284 { 285 Request: &MultiHttpEntryRequest{ 286 Url: "https://www.example.com", 287 }, 288 }, 289 { 290 Request: &MultiHttpEntryRequest{ 291 Url: "${variable}", 292 }, 293 }, 294 }, 295 }, 296 }, 297 }, 298 expectError: false, 299 }, 300 "empty multihttp check": { 301 input: Check{ 302 Id: 1, 303 TenantId: 1, 304 Target: "", 305 Job: "job", 306 Frequency: 60000, 307 Timeout: 10000, 308 Probes: []int64{1}, 309 Settings: CheckSettings{ 310 Multihttp: &MultiHttpSettings{ 311 Entries: []*MultiHttpEntry{ 312 { 313 Request: &MultiHttpEntryRequest{ 314 Url: "https://example.org/", 315 }, 316 }, 317 }, 318 }, 319 }, 320 }, 321 expectError: true, 322 }, 323 "invalid multihttp URL": { 324 input: Check{ 325 Id: 1, 326 TenantId: 1, 327 Target: "example.com", // this is fine 328 Job: "job", 329 Frequency: 60000, 330 Timeout: 10000, 331 Probes: []int64{1}, 332 Settings: CheckSettings{ 333 Multihttp: &MultiHttpSettings{ 334 Entries: []*MultiHttpEntry{ 335 { 336 Request: &MultiHttpEntryRequest{ 337 Url: "example.com", // this is the problem 338 }, 339 }, 340 }, 341 }, 342 }, 343 }, 344 expectError: true, 345 }, 346 "multihttp target must not be an URL": { 347 input: Check{ 348 Id: 1, 349 TenantId: 1, 350 Target: "example.com", // this is fine 351 Job: "job", 352 Frequency: 60000, 353 Timeout: 10000, 354 Probes: []int64{1}, 355 Settings: CheckSettings{ 356 Multihttp: &MultiHttpSettings{ 357 Entries: []*MultiHttpEntry{ 358 { 359 Request: &MultiHttpEntryRequest{ 360 Url: "http://example.com", 361 }, 362 }, 363 }, 364 }, 365 }, 366 }, 367 expectError: false, 368 }, 369 "invalid multihttp second target": { 370 input: Check{ 371 Id: 1, 372 TenantId: 1, 373 Target: "https://www.example.com", 374 Job: "job", 375 Frequency: 60000, 376 Timeout: 10000, 377 Probes: []int64{1}, 378 Settings: CheckSettings{ 379 Multihttp: &MultiHttpSettings{ 380 Entries: []*MultiHttpEntry{ 381 { 382 Request: &MultiHttpEntryRequest{ 383 Url: "https://www.example.com", 384 }, 385 }, 386 { 387 Request: &MultiHttpEntryRequest{ 388 Url: "notavalidurlatall", 389 }, 390 }, 391 }, 392 }, 393 }, 394 }, 395 expectError: true, 396 }, 397 "valid multihttp variables everywhere": { 398 input: Check{ 399 Id: 1, 400 TenantId: 1, 401 Target: "${variable}", 402 Job: "job", 403 Frequency: 60000, 404 Timeout: 10000, 405 Probes: []int64{1}, 406 Settings: CheckSettings{ 407 Multihttp: &MultiHttpSettings{ 408 Entries: []*MultiHttpEntry{ 409 { 410 Request: &MultiHttpEntryRequest{ 411 Url: "${variable}", 412 Headers: []*HttpHeader{ 413 { 414 Name: "Authorization", 415 Value: "Bearer ${variable}", 416 }, 417 }, 418 Body: &HttpRequestBody{ 419 ContentType: "application/json", 420 ContentEncoding: "gzip", 421 Payload: []byte("${variable}"), 422 }, 423 }, 424 }, 425 }, 426 }, 427 }, 428 }, 429 expectError: false, 430 }, 431 "invalid internal job": { 432 input: Check{ 433 Id: 1, 434 TenantId: 1, 435 Target: "http://example.org/", 436 Job: internalMarker, 437 Frequency: 1000, 438 Timeout: 1000, 439 Probes: []int64{1}, 440 Settings: CheckSettings{ 441 Http: &HttpSettings{}, 442 }, 443 }, 444 expectError: true, 445 }, 446 "invalid internal target": { 447 input: Check{ 448 Id: 1, 449 TenantId: 1, 450 Target: internalMarker, 451 Job: "valid-job", 452 Frequency: 1000, 453 Timeout: 1000, 454 Probes: []int64{1}, 455 Settings: CheckSettings{ 456 Scripted: &ScriptedSettings{}, 457 }, 458 }, 459 expectError: true, 460 }, 461 } 462 463 // add trivial cases for all check types 464 for _, checkType := range CheckTypeValues() { 465 testcases["valid "+checkType.String()] = TestCase{ 466 input: GetCheckInstance(checkType), 467 expectError: false, 468 } 469 } 470 471 for name, testcase := range testcases { 472 t.Run(name, func(t *testing.T) { 473 err := testcase.input.Validate() 474 checkError(t, testcase.expectError, err, testcase.input) 475 }) 476 } 477 } 478 479 func TestCheckType(t *testing.T) { 480 type testcase struct { 481 input Check 482 expected CheckType 483 } 484 485 testcases := make(map[string]testcase) 486 487 for _, checkType := range CheckTypeValues() { 488 testcases[checkType.String()] = testcase{ 489 input: GetCheckInstance(checkType), 490 expected: checkType, 491 } 492 } 493 494 for name, testcase := range testcases { 495 t.Run(name, func(t *testing.T) { 496 actual := testcase.input.Type() 497 require.Equal(t, testcase.expected, actual) 498 }) 499 } 500 } 501 502 func TestCheckClass(t *testing.T) { 503 testcases := map[string]struct { 504 input Check 505 expected CheckClass 506 }{ 507 CheckTypeDns.String(): { 508 input: GetCheckInstance(CheckTypeDns), 509 expected: CheckClass_PROTOCOL, 510 }, 511 CheckTypeHttp.String(): { 512 input: GetCheckInstance(CheckTypeHttp), 513 expected: CheckClass_PROTOCOL, 514 }, 515 CheckTypePing.String(): { 516 input: GetCheckInstance(CheckTypePing), 517 expected: CheckClass_PROTOCOL, 518 }, 519 CheckTypeTcp.String(): { 520 input: GetCheckInstance(CheckTypeTcp), 521 expected: CheckClass_PROTOCOL, 522 }, 523 CheckTypeTraceroute.String(): { 524 input: GetCheckInstance(CheckTypeTraceroute), 525 expected: CheckClass_PROTOCOL, 526 }, 527 CheckTypeScripted.String(): { 528 input: GetCheckInstance(CheckTypeScripted), 529 expected: CheckClass_SCRIPTED, 530 }, 531 CheckTypeMultiHttp.String(): { 532 input: GetCheckInstance(CheckTypeMultiHttp), 533 expected: CheckClass_SCRIPTED, 534 }, 535 CheckTypeGrpc.String(): { 536 input: GetCheckInstance(CheckTypeGrpc), 537 expected: CheckClass_PROTOCOL, 538 }, 539 CheckTypeBrowser.String(): { 540 input: GetCheckInstance(CheckTypeBrowser), 541 expected: CheckClass_BROWSER, 542 }, 543 } 544 545 for name, testcase := range testcases { 546 t.Run(name, func(t *testing.T) { 547 actual := testcase.input.Class() 548 require.Equal(t, testcase.expected, actual) 549 }) 550 } 551 552 hasTest := make(map[CheckType]bool) 553 for _, checkType := range CheckTypeValues() { 554 hasTest[checkType] = false 555 } 556 557 for _, testcase := range testcases { 558 hasTest[testcase.input.Type()] = true 559 } 560 561 for checkType, found := range hasTest { 562 require.True(t, found, "missing test for check type %s", checkType) 563 } 564 } 565 566 func TestCheckTypeString(t *testing.T) { 567 testcases := map[string]struct { 568 input CheckType 569 expected string 570 }{ 571 "dns": { 572 input: CheckTypeDns, 573 expected: "dns", 574 }, 575 "http": { 576 input: CheckTypeHttp, 577 expected: "http", 578 }, 579 "ping": { 580 input: CheckTypePing, 581 expected: "ping", 582 }, 583 "tcp": { 584 input: CheckTypeTcp, 585 expected: "tcp", 586 }, 587 "traceroute": { 588 input: CheckTypeTraceroute, 589 expected: "traceroute", 590 }, 591 "scripted": { 592 input: CheckTypeScripted, 593 expected: "scripted", 594 }, 595 "multihttp": { 596 input: CheckTypeMultiHttp, 597 expected: "multihttp", 598 }, 599 "browser": { 600 input: CheckTypeBrowser, 601 expected: "browser", 602 }, 603 } 604 605 for name, testcase := range testcases { 606 actual := testcase.input.String() 607 require.Equal(t, testcase.expected, actual, "testcase %s", name) 608 } 609 } 610 611 func TestCheckTypeClass(t *testing.T) { 612 testcases := map[string]struct { 613 input CheckType 614 expected CheckClass 615 }{ 616 CheckTypeDns.String(): { 617 input: CheckTypeDns, 618 expected: CheckClass_PROTOCOL, 619 }, 620 CheckTypeHttp.String(): { 621 input: CheckTypeHttp, 622 expected: CheckClass_PROTOCOL, 623 }, 624 CheckTypePing.String(): { 625 input: CheckTypePing, 626 expected: CheckClass_PROTOCOL, 627 }, 628 CheckTypeTcp.String(): { 629 input: CheckTypeTcp, 630 expected: CheckClass_PROTOCOL, 631 }, 632 CheckTypeTraceroute.String(): { 633 input: CheckTypeTraceroute, 634 expected: CheckClass_PROTOCOL, 635 }, 636 CheckTypeScripted.String(): { 637 input: CheckTypeScripted, 638 expected: CheckClass_SCRIPTED, 639 }, 640 CheckTypeMultiHttp.String(): { 641 input: CheckTypeMultiHttp, 642 expected: CheckClass_SCRIPTED, 643 }, 644 CheckTypeGrpc.String(): { 645 input: CheckTypeGrpc, 646 expected: CheckClass_PROTOCOL, 647 }, 648 CheckTypeBrowser.String(): { 649 input: CheckTypeBrowser, 650 expected: CheckClass_BROWSER, 651 }, 652 } 653 654 for name, testcase := range testcases { 655 t.Run(name, func(t *testing.T) { 656 actual := testcase.input.Class() 657 require.Equal(t, testcase.expected, actual) 658 }) 659 } 660 661 hasTest := make(map[CheckType]bool) 662 for _, checkType := range CheckTypeValues() { 663 hasTest[checkType] = false 664 } 665 666 for _, testcase := range testcases { 667 hasTest[testcase.input] = true 668 } 669 670 for checkType, found := range hasTest { 671 require.True(t, found, "missing test for check type %s", checkType) 672 } 673 } 674 675 func TestValidateHost(t *testing.T) { 676 testcases := map[string]struct { 677 input string 678 expectError bool 679 }{ 680 // valid hostnames 681 "hostname": { 682 input: "grafana.com", 683 expectError: false, 684 }, 685 686 // invalid hostnames 687 "invalid hostname": { 688 input: "grafana-com", 689 expectError: true, 690 }, 691 692 // valid IP addresses 693 "IPv4": { 694 input: "1.2.3.4", 695 expectError: false, 696 }, 697 "IPv4 loopback": { 698 input: "127.0.0.1", 699 expectError: false, 700 }, 701 "IPv4 local multicast": { 702 input: "224.0.0.1", 703 expectError: false, 704 }, 705 "IPv4 control multicast": { 706 input: "224.0.1.1", 707 expectError: false, 708 }, 709 "IPv6": { 710 input: "ABCD:EF01:2345:6789:ABCD:EF01:2345:6789", 711 expectError: false, 712 }, 713 "IPv6 unicast": { 714 input: "2001:DB8:0:0:8:800:200C:417A", 715 expectError: false, 716 }, 717 "IPv6 link-local unicast": { 718 input: "FE80::1", 719 expectError: false, 720 }, 721 "IPv6 multicast": { 722 input: "FF01:0:0:0:0:0:0:101", 723 expectError: false, 724 }, 725 "IPv6 multicast all local nodes": { 726 input: "FF02::1", 727 expectError: false, 728 }, 729 "IPv6 loopback": { 730 input: "0:0:0:0:0:0:0:1", 731 expectError: false, 732 }, 733 "IPv6 loopback short": { 734 input: "::1", 735 expectError: false, 736 }, 737 "IPv6 unespecified short": { 738 input: "::", 739 expectError: false, 740 }, 741 "IPv4 as IPv6": { 742 input: "0:0:0:0:0:0:13.1.68.3", 743 expectError: false, 744 }, 745 "IPv4 in IPv6": { 746 input: "0:0:0:0:FF:FF:13.1.68.3", 747 expectError: false, 748 }, 749 "IPv4 as IPv6 short": { 750 input: "::13.1.68.3", 751 expectError: false, 752 }, 753 "IPv4 in IPv6 short": { 754 input: "::FFFF:13.1.68.3", 755 expectError: false, 756 }, 757 758 // invalid IP addresses 759 "invalid IPv4": { 760 input: "0.0.0.256", 761 expectError: true, 762 }, 763 "invalid IPv6": { 764 input: "::10000", 765 expectError: true, 766 }, 767 } 768 769 for name, testcase := range testcases { 770 t.Run(name, func(t *testing.T) { 771 err := validateHost(testcase.input) 772 checkError(t, testcase.expectError, err, testcase.input) 773 }) 774 } 775 } 776 777 func TestValidateDnsTarget(t *testing.T) { 778 testcases := map[string]struct { 779 input string 780 expectError bool 781 }{ 782 // valid hostnames 783 "hostname": { 784 input: "grafana.com", 785 expectError: false, 786 }, 787 788 // localhost is valid 789 "localhost": { 790 input: "localhost", 791 expectError: false, 792 }, 793 794 // localhost. is valid 795 "localhost.": { 796 input: "localhost.", 797 expectError: false, 798 }, 799 800 // single label fully qualified dns name is valid 801 "org.": { 802 input: "org.", 803 expectError: false, 804 }, 805 806 // multi-label dns name is valid 807 "grafana.com.": { 808 input: "grafana.com.", 809 expectError: false, 810 }, 811 812 // single label is invalid 813 "org": { 814 input: "org", 815 expectError: true, 816 }, 817 818 // For DNS entries, IP address is valid 819 "127.0.0.1": { 820 input: "127.0.0.1", 821 expectError: false, 822 }, 823 824 // IP address disguised as multi-label fully qualified 825 // dns name are also valid 826 "127.0.0.1.": { 827 input: "127.0.0.1.", 828 expectError: false, 829 }, 830 831 // empty label 832 "foo..bar": { 833 input: "foo..bar", 834 expectError: true, 835 }, 836 837 // label too long 838 "foo.a62a.bar": { 839 input: "foo." + strings.Repeat("a", 64) + ".bar", 840 expectError: true, 841 }, 842 843 // zeroconf 844 "_srv._tcp.example.org": { 845 input: "_srv._tcp.example.org", 846 expectError: false, 847 }, 848 } 849 850 for name, testcase := range testcases { 851 t.Run(name, func(t *testing.T) { 852 err := validateDnsTarget(testcase.input) 853 checkError(t, testcase.expectError, err, testcase.input) 854 }) 855 } 856 } 857 858 func TestCheckFQHN(t *testing.T) { 859 genstr := func(n int) string { 860 var sb strings.Builder 861 sb.Grow(n) 862 863 for i := 1; i <= n; i++ { 864 _ = sb.WriteByte(byte('a' + (i % ('z' - 'a' + 1)))) 865 } 866 867 return sb.String() 868 } 869 870 testcases := map[string]struct { 871 input string 872 expectError bool 873 }{ 874 "empty": { 875 input: "", 876 expectError: true, 877 }, 878 "too long": { 879 input: genstr(256), 880 expectError: true, 881 }, 882 "start with .": { 883 input: ".x", 884 expectError: true, 885 }, 886 "end with . 1": { 887 input: "x.", 888 expectError: true, 889 }, 890 "end with . 2": { 891 input: "x.y.", 892 expectError: true, 893 }, 894 "must have at least two labels": { 895 input: "x", 896 expectError: true, 897 }, 898 "label must start with letter or digit 1": { 899 input: "0.x", 900 expectError: false, 901 }, 902 "label must start with letter or digit 2": { 903 input: "-.x", 904 expectError: true, 905 }, 906 "label must start with letter or digit 3": { 907 input: "x.y", 908 expectError: false, 909 }, 910 "label must start with letter or digit 4": { 911 input: "1x.y", 912 expectError: false, 913 }, 914 "label must end with a letter or digit 1": { 915 input: "-.x", 916 expectError: true, 917 }, 918 "label must end with a letter or digit 2": { 919 input: "x.y", 920 expectError: false, 921 }, 922 "label must end with a letter or digit 3": { 923 input: "xy.z", 924 expectError: false, 925 }, 926 "label must end with a letter or digit 4": { 927 input: "x1.y", 928 expectError: false, 929 }, 930 "label must contain only letters, digits or dash 1": { 931 input: "x=y.z", 932 expectError: true, 933 }, 934 "label must contain only letters, digits or dash 2": { 935 input: "x-0.y-z", 936 expectError: false, 937 }, 938 "labels must be 63 characters or less 1": { 939 input: genstr(64) + ".x", 940 expectError: true, 941 }, 942 "labels must be 63 characters or less 2": { 943 input: genstr(63) + "." + genstr(63), 944 expectError: false, 945 }, 946 "valid, all lowercase": { 947 input: "grafana.com", 948 expectError: false, 949 }, 950 "valid, all uppercase": { 951 input: "GRAFANA.COM", 952 expectError: false, 953 }, 954 "valid, mixed case": { 955 input: "gRaFaNa.CoM", 956 expectError: false, 957 }, 958 } 959 960 for name, testcase := range testcases { 961 t.Run(name, func(t *testing.T) { 962 err := checkFQHN(testcase.input) 963 checkError(t, testcase.expectError, err, testcase.input) 964 }) 965 } 966 } 967 968 func TestValidateHostPort(t *testing.T) { 969 testcases := map[string]struct { 970 input string 971 expectError bool 972 }{ 973 "trivial": { 974 input: "grafana.com:25", 975 expectError: false, 976 }, 977 "port 1": { 978 input: "grafana.com:1", 979 expectError: false, 980 }, 981 "port 65535": { 982 input: "grafana.com:65535", 983 expectError: false, 984 }, 985 986 "blank": { 987 input: "", 988 expectError: true, 989 }, 990 991 // invalid hosts 992 "no host": { 993 input: ":25", 994 expectError: true, 995 }, 996 "invalid domain": { 997 input: "x:25", 998 expectError: true, 999 }, 1000 "invalid host": { 1001 input: "-.x:25", 1002 expectError: true, 1003 }, 1004 1005 // invalid ports 1006 "no port": { 1007 input: "grafana.com", 1008 expectError: true, 1009 }, 1010 "empty port": { 1011 input: "grafana.com:", 1012 expectError: true, 1013 }, 1014 "port zero": { 1015 input: "grafana.com:0", 1016 expectError: true, 1017 }, 1018 "negative port": { 1019 input: "grafana.com:-1", 1020 expectError: true, 1021 }, 1022 "port too large": { 1023 input: "grafana.com:65536", 1024 expectError: true, 1025 }, 1026 } 1027 1028 for name, testcase := range testcases { 1029 t.Run(name, func(t *testing.T) { 1030 err := validateHostPort(testcase.input) 1031 checkError(t, testcase.expectError, err, testcase.input) 1032 }) 1033 } 1034 } 1035 1036 func TestValidateHttpUrl(t *testing.T) { 1037 testcases := map[string]struct { 1038 input string 1039 expectError bool 1040 }{ 1041 "http": { 1042 input: "http://example.org/", 1043 expectError: false, 1044 }, 1045 "https": { 1046 input: "https://example.org/", 1047 expectError: false, 1048 }, 1049 "http port": { 1050 input: "http://example.org:8000/", 1051 expectError: false, 1052 }, 1053 "https port": { 1054 input: "https://example.org:8443/", 1055 expectError: false, 1056 }, 1057 "ipv4": { 1058 input: "http://127.0.0.1/", 1059 expectError: false, 1060 }, 1061 "ipv6": { 1062 input: "http://[::1]/", 1063 expectError: false, 1064 }, 1065 "ipv4 port": { 1066 input: "http://127.0.0.1:80/", 1067 expectError: false, 1068 }, 1069 "ipv6 port": { 1070 input: "http://[::1]:80/", 1071 expectError: false, 1072 }, 1073 "invalid scheme": { 1074 input: "ftp://example.org/", 1075 expectError: true, 1076 }, 1077 "with username": { 1078 input: "http://user@example.org/", 1079 expectError: true, 1080 }, 1081 "with username and password": { 1082 input: "http://user:password@example.org/", 1083 expectError: true, 1084 }, 1085 1086 "blank": { 1087 input: "", 1088 expectError: true, 1089 }, 1090 "no host": { 1091 input: "http://", 1092 expectError: true, 1093 }, 1094 1095 // these are covered by TestValidateHostPort 1096 "bad host": { 1097 input: "http://test/", 1098 expectError: true, 1099 }, 1100 "port too large": { 1101 input: "http://example.org:65536/", 1102 expectError: true, 1103 }, 1104 "port zero": { 1105 input: "http://example.org:0/", 1106 expectError: true, 1107 }, 1108 "negative port": { 1109 input: "http://example.org:-1/", 1110 expectError: true, 1111 }, 1112 } 1113 1114 for name, testcase := range testcases { 1115 t.Run(name, func(t *testing.T) { 1116 err := validateHttpUrl(testcase.input) 1117 checkError(t, testcase.expectError, err, testcase.input) 1118 }) 1119 } 1120 } 1121 1122 func TestValidateLabel(t *testing.T) { 1123 genString := func(n int) string { 1124 var s strings.Builder 1125 s.Grow(n) 1126 1127 for range n { 1128 _ = s.WriteByte('x') 1129 } 1130 1131 return s.String() 1132 } 1133 1134 testcases := map[string]struct { 1135 input Label 1136 expectError bool 1137 }{ 1138 "trivial": { 1139 input: Label{Name: "label", Value: "value"}, 1140 expectError: false, 1141 }, 1142 "name with underscore": { 1143 input: Label{Name: "some_name", Value: "value"}, 1144 expectError: false, 1145 }, 1146 "name with leading underscore": { 1147 input: Label{Name: "_name", Value: "value"}, 1148 expectError: false, 1149 }, 1150 "empty name": { 1151 input: Label{Name: "", Value: "value"}, 1152 expectError: true, 1153 }, 1154 "invalid name": { 1155 input: Label{Name: "foo@bar", Value: "value"}, 1156 expectError: true, 1157 }, 1158 "name with trailing blank": { // issue #99 1159 input: Label{Name: "name ", Value: "value"}, 1160 expectError: true, 1161 }, 1162 "empty value": { 1163 input: Label{Name: "name", Value: ""}, 1164 expectError: true, 1165 }, 1166 "long value": { 1167 input: Label{Name: "name", Value: genString(MaxLabelValueLength)}, 1168 expectError: false, 1169 }, 1170 "value too long": { 1171 input: Label{Name: "name", Value: genString(MaxLabelValueLength + 1)}, 1172 expectError: true, 1173 }, 1174 } 1175 1176 for name, testcase := range testcases { 1177 t.Run(name, func(t *testing.T) { 1178 err := testcase.input.Validate() 1179 checkError(t, testcase.expectError, err, testcase.input) 1180 }) 1181 } 1182 } 1183 1184 func TestHttpSettingsValidate(t *testing.T) { 1185 testcases := map[string]struct { 1186 input HttpSettings 1187 expectError bool 1188 }{ 1189 "trivial": { 1190 input: HttpSettings{}, 1191 expectError: false, 1192 }, 1193 "valid headers": { 1194 input: HttpSettings{ 1195 Headers: []string{"header: value"}, 1196 }, 1197 expectError: false, 1198 }, 1199 "no value is OK": { 1200 input: HttpSettings{ 1201 Headers: []string{"header:"}, 1202 }, 1203 expectError: false, 1204 }, 1205 "empty header is not OK": { 1206 input: HttpSettings{ 1207 Headers: []string{": value"}, 1208 }, 1209 expectError: true, 1210 }, 1211 "empty": { 1212 input: HttpSettings{ 1213 Headers: []string{""}, 1214 }, 1215 expectError: true, 1216 }, 1217 "no colon": { 1218 input: HttpSettings{ 1219 Headers: []string{"header"}, 1220 }, 1221 expectError: true, 1222 }, 1223 "multiple colons": { 1224 input: HttpSettings{ 1225 Headers: []string{"origin:https://www.grafana.com"}, 1226 }, 1227 expectError: false, 1228 }, 1229 "invalid name": { 1230 input: HttpSettings{ 1231 Headers: []string{"hea;der: value"}, 1232 }, 1233 expectError: true, 1234 }, 1235 "empty header/value": { 1236 input: HttpSettings{ 1237 Headers: []string{":"}, 1238 }, 1239 expectError: true, 1240 }, 1241 "blank header/value": { 1242 input: HttpSettings{ 1243 Headers: []string{" : "}, 1244 }, 1245 expectError: true, 1246 }, 1247 "non-ascii value": { 1248 input: HttpSettings{ 1249 Headers: []string{"header: यूनिकोड टेक्स्ट"}, 1250 }, 1251 expectError: false, 1252 }, 1253 } 1254 1255 for name, testcase := range testcases { 1256 t.Run(name, func(t *testing.T) { 1257 err := testcase.input.Validate() 1258 checkError(t, testcase.expectError, err, testcase.input) 1259 }) 1260 } 1261 } 1262 1263 func TestCompressionAlgorithmMarshal(t *testing.T) { 1264 type testStruct struct { 1265 Compression CompressionAlgorithm `json:"compression,omitempty"` 1266 } 1267 1268 testcases := map[string]struct { 1269 unserialized testStruct 1270 serialized []byte 1271 }{ 1272 "none": { 1273 unserialized: testStruct{ 1274 Compression: CompressionAlgorithm_none, 1275 }, 1276 serialized: []byte(`{}`), 1277 }, 1278 "gzip": { 1279 unserialized: testStruct{ 1280 Compression: CompressionAlgorithm_gzip, 1281 }, 1282 serialized: []byte(`{"compression":"gzip"}`), 1283 }, 1284 "br": { 1285 unserialized: testStruct{ 1286 Compression: CompressionAlgorithm_br, 1287 }, 1288 serialized: []byte(`{"compression":"br"}`), 1289 }, 1290 "deflate": { 1291 unserialized: testStruct{ 1292 Compression: CompressionAlgorithm_deflate, 1293 }, 1294 serialized: []byte(`{"compression":"deflate"}`), 1295 }, 1296 "identity": { 1297 unserialized: testStruct{ 1298 Compression: CompressionAlgorithm_identity, 1299 }, 1300 serialized: []byte(`{"compression":"identity"}`), 1301 }, 1302 } 1303 1304 for name, tc := range testcases { 1305 t.Run(name, func(t *testing.T) { 1306 actual, err := json.Marshal(tc.unserialized) 1307 require.NoError(t, err) 1308 require.Equal(t, tc.serialized, actual) 1309 }) 1310 } 1311 } 1312 1313 func TestCompressionAlgorithmUnmarshal(t *testing.T) { 1314 type testStruct struct { 1315 Compression CompressionAlgorithm `json:"compression,omitempty"` 1316 } 1317 1318 testcases := map[string]struct { 1319 unserialized testStruct 1320 serialized []byte 1321 }{ 1322 "none": { 1323 unserialized: testStruct{ 1324 Compression: CompressionAlgorithm_none, 1325 }, 1326 serialized: []byte(`{}`), 1327 }, 1328 "empty": { 1329 unserialized: testStruct{ 1330 Compression: CompressionAlgorithm_none, 1331 }, 1332 serialized: []byte(`{"compression":""}`), 1333 }, 1334 "null": { 1335 unserialized: testStruct{ 1336 Compression: CompressionAlgorithm_none, 1337 }, 1338 serialized: []byte(`{"compression":null}`), 1339 }, 1340 "gzip": { 1341 unserialized: testStruct{ 1342 Compression: CompressionAlgorithm_gzip, 1343 }, 1344 serialized: []byte(`{"compression":"gzip"}`), 1345 }, 1346 "br": { 1347 unserialized: testStruct{ 1348 Compression: CompressionAlgorithm_br, 1349 }, 1350 serialized: []byte(`{"compression":"br"}`), 1351 }, 1352 "deflate": { 1353 unserialized: testStruct{ 1354 Compression: CompressionAlgorithm_deflate, 1355 }, 1356 serialized: []byte(`{"compression":"deflate"}`), 1357 }, 1358 "identity": { 1359 unserialized: testStruct{ 1360 Compression: CompressionAlgorithm_identity, 1361 }, 1362 serialized: []byte(`{"compression":"identity"}`), 1363 }, 1364 } 1365 1366 for name, tc := range testcases { 1367 t.Run(name, func(t *testing.T) { 1368 var actual testStruct 1369 1370 err := json.Unmarshal(tc.serialized, &actual) 1371 require.NoError(t, err) 1372 require.Equal(t, tc.unserialized, actual) 1373 }) 1374 } 1375 } 1376 1377 func checkError(t *testing.T, expectError bool, err error, input any) { 1378 t.Helper() 1379 1380 switch { 1381 case expectError && err == nil: 1382 // unexpected success 1383 t.Errorf("expecting failure for input %q, but got success", input) 1384 1385 case !expectError && err != nil: 1386 // unexpected failure 1387 t.Errorf("expecting success for input %q, but got failure: %s", input, err.Error()) 1388 1389 case expectError && err != nil: 1390 // expected failure 1391 if *testDebugOutput { 1392 t.Logf("expecting failure for input %q, got failure: %s", input, err.Error()) 1393 } 1394 1395 case !expectError && err == nil: 1396 // expected success 1397 if *testDebugOutput { 1398 t.Logf("expecting success for input %q, got success", input) 1399 } 1400 } 1401 } 1402 1403 type TestCases[T any] map[string]struct { 1404 input T 1405 expectError bool 1406 } 1407 1408 func testValidate[T validatable](t *testing.T, testcases TestCases[T]) { 1409 t.Helper() 1410 1411 for name, testcase := range testcases { 1412 t.Run(name, func(t *testing.T) { 1413 err := testcase.input.Validate() 1414 if testcase.expectError { 1415 require.Error(t, err) 1416 } else { 1417 require.NoError(t, err) 1418 } 1419 }) 1420 } 1421 } 1422 1423 type testValidatable struct { 1424 err error 1425 } 1426 1427 func (v testValidatable) Validate() error { 1428 return v.err 1429 } 1430 1431 // TestValidateCollection vefifies that validateCollection calls `Validate` on 1432 // each member of a collection of `Validatable` objects. 1433 func TestValidateCollection(t *testing.T) { 1434 invalid := errors.New("invalid") 1435 1436 testcases := map[string]struct { 1437 input []testValidatable 1438 expectError bool 1439 }{ 1440 "empty": { 1441 input: nil, 1442 expectError: false, 1443 }, 1444 "one valid": { 1445 input: []testValidatable{{err: nil}}, 1446 expectError: false, 1447 }, 1448 "one invalid": { 1449 input: []testValidatable{{err: errors.New("invalid")}}, 1450 expectError: true, 1451 }, 1452 "two valid": { 1453 input: []testValidatable{{err: nil}, {err: nil}}, 1454 expectError: false, 1455 }, 1456 "two invalid": { 1457 input: []testValidatable{{err: invalid}, {err: invalid}}, 1458 expectError: true, 1459 }, 1460 "mixed": { 1461 input: []testValidatable{{err: nil}, {err: invalid}}, 1462 expectError: true, 1463 }, 1464 } 1465 1466 for name, testcase := range testcases { 1467 t.Run(name, func(t *testing.T) { 1468 err := validateCollection(testcase.input) 1469 if testcase.expectError { 1470 require.Error(t, err) 1471 } else { 1472 require.NoError(t, err) 1473 } 1474 }) 1475 } 1476 } 1477 1478 func TestHttpHeaderValidate(t *testing.T) { 1479 testValidate(t, TestCases[HttpHeader]{ 1480 "valid": { 1481 input: HttpHeader{Name: "name", Value: "value"}, 1482 expectError: false, 1483 }, 1484 "an empty value is valid": { 1485 input: HttpHeader{Name: "xyz", Value: ""}, 1486 expectError: false, 1487 }, 1488 "an empty name is invalid": { 1489 input: HttpHeader{Name: "", Value: "xxx"}, 1490 expectError: true, 1491 }, 1492 "invalid header name": { 1493 input: HttpHeader{Name: "x y z", Value: ""}, 1494 expectError: true, 1495 }, 1496 }) 1497 } 1498 1499 func TestQueryFieldValidate(t *testing.T) { 1500 testValidate(t, TestCases[QueryField]{ 1501 "valid": { 1502 input: QueryField{Name: "name", Value: "value"}, 1503 expectError: false, 1504 }, 1505 "an empty value is valid": { 1506 input: QueryField{Name: "xyz", Value: ""}, 1507 expectError: false, 1508 }, 1509 "an empty name is invalid": { 1510 input: QueryField{Name: "", Value: "xxx"}, 1511 expectError: true, 1512 }, 1513 "a name with spaces is valid": { 1514 input: QueryField{Name: "x y z", Value: ""}, 1515 expectError: false, 1516 }, 1517 }) 1518 } 1519 1520 func TestHttpRequestBodyValidate(t *testing.T) { 1521 testValidate(t, TestCases[*HttpRequestBody]{ 1522 "nil is valid": { 1523 input: nil, 1524 expectError: false, 1525 }, 1526 "no content type is invalid": { 1527 input: &HttpRequestBody{ 1528 ContentType: "", 1529 ContentEncoding: "identity", 1530 Payload: []byte{42}, 1531 }, 1532 expectError: true, 1533 }, 1534 "empty content encoding is valid": { 1535 input: &HttpRequestBody{ 1536 ContentType: "text/plain", 1537 ContentEncoding: "", 1538 Payload: []byte{42}, 1539 }, 1540 expectError: false, 1541 }, 1542 "content type must be a valid media type": { 1543 input: &HttpRequestBody{ 1544 // media types should not have space before the / 1545 ContentType: "json file", 1546 ContentEncoding: "", 1547 Payload: []byte{42}, 1548 }, 1549 expectError: true, 1550 }, 1551 "empty payload is valid": { 1552 input: &HttpRequestBody{ 1553 ContentType: "text/plain", 1554 ContentEncoding: "identity", 1555 Payload: []byte{}, 1556 }, 1557 expectError: false, 1558 }, 1559 }) 1560 } 1561 1562 func TestMultiHttpEntryAssertionValidate(t *testing.T) { 1563 testValidate(t, TestCases[*MultiHttpEntryAssertion]{ 1564 "nil is valid": { 1565 input: nil, 1566 expectError: false, 1567 }, 1568 "zero value is invalid": { 1569 // The zero value is a text assertion with "body" as the 1570 // subject and "contains" as the condition. The 1571 // "value" is empty, and that is not allowed. 1572 input: &MultiHttpEntryAssertion{}, 1573 expectError: true, 1574 }, 1575 "text+body+contains with valid value": { 1576 input: &MultiHttpEntryAssertion{ 1577 Type: MultiHttpEntryAssertionType_TEXT, 1578 Subject: MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY, 1579 Condition: MultiHttpEntryAssertionConditionVariant_CONTAINS, 1580 Value: "foobar", 1581 }, 1582 expectError: false, 1583 }, 1584 "text does not allow expressions": { 1585 input: &MultiHttpEntryAssertion{ 1586 Type: MultiHttpEntryAssertionType_TEXT, 1587 Subject: MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY, 1588 Condition: MultiHttpEntryAssertionConditionVariant_CONTAINS, 1589 Expression: "foo", 1590 Value: "bar", 1591 }, 1592 expectError: true, 1593 }, 1594 "json path value with valid value": { 1595 input: &MultiHttpEntryAssertion{ 1596 Type: MultiHttpEntryAssertionType_JSON_PATH_VALUE, 1597 Condition: MultiHttpEntryAssertionConditionVariant_CONTAINS, 1598 Expression: "$.data", 1599 Value: "foo", 1600 }, 1601 expectError: false, 1602 }, 1603 "json path value does not allow for setting the subject": { 1604 input: &MultiHttpEntryAssertion{ 1605 Type: MultiHttpEntryAssertionType_JSON_PATH_VALUE, 1606 Subject: MultiHttpEntryAssertionSubjectVariant_RESPONSE_HEADERS, // invalid 1607 Condition: MultiHttpEntryAssertionConditionVariant_CONTAINS, 1608 Expression: "$.data", 1609 Value: "foo", 1610 }, 1611 expectError: true, 1612 }, 1613 "valid json path assertion": { 1614 input: &MultiHttpEntryAssertion{ 1615 Type: MultiHttpEntryAssertionType_JSON_PATH_ASSERTION, 1616 Expression: "$.foo", 1617 }, 1618 expectError: false, 1619 }, 1620 "json path assertion does not allow subject": { 1621 input: &MultiHttpEntryAssertion{ 1622 Type: MultiHttpEntryAssertionType_JSON_PATH_ASSERTION, 1623 Subject: MultiHttpEntryAssertionSubjectVariant_RESPONSE_HEADERS, // invalid 1624 Expression: "$.foo", 1625 }, 1626 expectError: true, 1627 }, 1628 "json path assertion does not allow condition": { 1629 input: &MultiHttpEntryAssertion{ 1630 Type: MultiHttpEntryAssertionType_JSON_PATH_ASSERTION, 1631 Condition: 1, // invalid 1632 Expression: "$.foo", 1633 }, 1634 expectError: true, 1635 }, 1636 "json path assertion does not allow value": { 1637 input: &MultiHttpEntryAssertion{ 1638 Type: MultiHttpEntryAssertionType_JSON_PATH_ASSERTION, 1639 Value: "bar", // invalid 1640 Expression: "$.foo", 1641 }, 1642 expectError: true, 1643 }, 1644 "valid regexp assertion": { 1645 input: &MultiHttpEntryAssertion{ 1646 Type: MultiHttpEntryAssertionType_REGEX_ASSERTION, 1647 Subject: MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY, 1648 Expression: "foo", 1649 }, 1650 expectError: false, 1651 }, 1652 "regexp assertion does not allow value": { 1653 input: &MultiHttpEntryAssertion{ 1654 Type: MultiHttpEntryAssertionType_REGEX_ASSERTION, 1655 Subject: MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY, 1656 Expression: "foo", 1657 Value: "bar", // invalid 1658 }, 1659 expectError: true, 1660 }, 1661 "regexp assertion does not allow condition": { 1662 input: &MultiHttpEntryAssertion{ 1663 Type: MultiHttpEntryAssertionType_REGEX_ASSERTION, 1664 Subject: MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY, 1665 Expression: "foo", 1666 Condition: 1, // invalid 1667 }, 1668 expectError: true, 1669 }, 1670 }) 1671 } 1672 1673 func TestHttpRegexFields(t *testing.T) { 1674 testValidate(t, TestCases[*HttpSettings]{ 1675 "body matches regexp parses": { 1676 input: &HttpSettings{ 1677 FailIfBodyMatchesRegexp: []string{".*good stuff.*"}, 1678 }, 1679 expectError: false, 1680 }, 1681 "body matches invalid regexp errors": { 1682 input: &HttpSettings{ 1683 FailIfBodyMatchesRegexp: []string{"*good stuff*"}, 1684 }, 1685 expectError: true, 1686 }, 1687 "body not matches regexp parses": { 1688 input: &HttpSettings{ 1689 FailIfBodyNotMatchesRegexp: []string{".*good stuff.*"}, 1690 }, 1691 expectError: false, 1692 }, 1693 "body not matches invalid regexp errors": { 1694 input: &HttpSettings{ 1695 FailIfBodyNotMatchesRegexp: []string{"*good stuff*"}, 1696 }, 1697 expectError: true, 1698 }, 1699 "header matches regexp parses": { 1700 input: &HttpSettings{ 1701 FailIfHeaderMatchesRegexp: []HeaderMatch{{Regexp: ".*good stuff.*"}}, 1702 }, 1703 expectError: false, 1704 }, 1705 "header matches invalid regexp errors": { 1706 input: &HttpSettings{ 1707 FailIfHeaderMatchesRegexp: []HeaderMatch{{Regexp: "*good stuff*"}}, 1708 }, 1709 expectError: true, 1710 }, 1711 "header not matches regexp parses": { 1712 input: &HttpSettings{ 1713 FailIfHeaderNotMatchesRegexp: []HeaderMatch{{Regexp: ".*good stuff.*"}}, 1714 }, 1715 expectError: false, 1716 }, 1717 "header not matches invalid regexp errors": { 1718 input: &HttpSettings{ 1719 FailIfHeaderNotMatchesRegexp: []HeaderMatch{{Regexp: "*good stuff*"}}, 1720 }, 1721 expectError: true, 1722 }, 1723 }) 1724 } 1725 1726 func TestMultiHttpEntryVariableValidate(t *testing.T) { 1727 testValidate(t, TestCases[*MultiHttpEntryVariable]{ 1728 "zero value": { 1729 // The zero value is invalid because a name and an 1730 // expression are required. 1731 input: &MultiHttpEntryVariable{}, 1732 expectError: true, 1733 }, 1734 "json path": { 1735 input: &MultiHttpEntryVariable{ 1736 Type: MultiHttpEntryVariableType_JSON_PATH, 1737 Name: "foo", 1738 Expression: "$.bar", 1739 }, 1740 expectError: false, 1741 }, 1742 "json path without a name is invalid": { 1743 input: &MultiHttpEntryVariable{ 1744 Type: MultiHttpEntryVariableType_JSON_PATH, 1745 Name: "", 1746 Expression: "$.bar", 1747 }, 1748 expectError: true, 1749 }, 1750 "json path without an expression is invalid": { 1751 input: &MultiHttpEntryVariable{ 1752 Type: MultiHttpEntryVariableType_JSON_PATH, 1753 Name: "foo", 1754 Expression: "", 1755 }, 1756 expectError: true, 1757 }, 1758 "json path with an attribute is invalid": { 1759 input: &MultiHttpEntryVariable{ 1760 Type: MultiHttpEntryVariableType_JSON_PATH, 1761 Name: "foo", 1762 Expression: "bar", 1763 Attribute: "baz", 1764 }, 1765 expectError: true, 1766 }, 1767 "regexp": { 1768 input: &MultiHttpEntryVariable{ 1769 Type: MultiHttpEntryVariableType_REGEX, 1770 Name: "foo", 1771 Expression: "bar", 1772 }, 1773 expectError: false, 1774 }, 1775 "regexp without a name is invalid": { 1776 input: &MultiHttpEntryVariable{ 1777 Type: MultiHttpEntryVariableType_REGEX, 1778 Name: "", 1779 Expression: "bar", 1780 }, 1781 expectError: true, 1782 }, 1783 "regexp without an expression is invalid": { 1784 input: &MultiHttpEntryVariable{ 1785 Type: MultiHttpEntryVariableType_REGEX, 1786 Name: "foo", 1787 Expression: "", 1788 }, 1789 expectError: true, 1790 }, 1791 "regexp with an attribute is invalid": { 1792 input: &MultiHttpEntryVariable{ 1793 Type: MultiHttpEntryVariableType_REGEX, 1794 Name: "foo", 1795 Expression: "bar", 1796 Attribute: "baz", 1797 }, 1798 expectError: true, 1799 }, 1800 "css selector": { 1801 input: &MultiHttpEntryVariable{ 1802 Type: MultiHttpEntryVariableType_CSS_SELECTOR, 1803 Name: "foo", 1804 Expression: "bar", 1805 }, 1806 expectError: false, 1807 }, 1808 "css selector without a name is invalid": { 1809 input: &MultiHttpEntryVariable{ 1810 Type: MultiHttpEntryVariableType_CSS_SELECTOR, 1811 Name: "", 1812 Expression: "bar", 1813 }, 1814 expectError: true, 1815 }, 1816 "css selector without an expression is invalid": { 1817 input: &MultiHttpEntryVariable{ 1818 Type: MultiHttpEntryVariableType_CSS_SELECTOR, 1819 Name: "foo", 1820 Expression: "", 1821 }, 1822 expectError: true, 1823 }, 1824 "css selector with attribute": { 1825 input: &MultiHttpEntryVariable{ 1826 Type: MultiHttpEntryVariableType_CSS_SELECTOR, 1827 Name: "foo", 1828 Expression: "bar", 1829 Attribute: "baz", 1830 }, 1831 expectError: false, 1832 }, 1833 }) 1834 } 1835 1836 func TestMultiHttpEntryRequestValidate(t *testing.T) { 1837 testValidate(t, TestCases[*MultiHttpEntryRequest]{ 1838 "nil": { 1839 input: nil, 1840 expectError: false, 1841 }, 1842 "zero value": { 1843 // The zero value is invalid because it doesn't have a URL 1844 input: &MultiHttpEntryRequest{}, 1845 expectError: true, 1846 }, 1847 "valid": { 1848 input: &MultiHttpEntryRequest{ 1849 Method: HttpMethod_GET, 1850 Url: "http://example.com", 1851 }, 1852 expectError: false, 1853 }, 1854 "invalid headers": { 1855 input: &MultiHttpEntryRequest{ 1856 Method: HttpMethod_GET, 1857 Url: "http://example.com", 1858 Headers: []*HttpHeader{ 1859 {Name: "", Value: ""}, 1860 }, 1861 }, 1862 expectError: true, 1863 }, 1864 "invalid query": { 1865 input: &MultiHttpEntryRequest{ 1866 Method: HttpMethod_GET, 1867 Url: "http://example.com", 1868 QueryFields: []*QueryField{ 1869 {Name: "", Value: ""}, 1870 }, 1871 }, 1872 expectError: true, 1873 }, 1874 "invalid body": { 1875 input: &MultiHttpEntryRequest{ 1876 Method: HttpMethod_GET, 1877 Url: "http://example.com", 1878 Body: &HttpRequestBody{ 1879 ContentType: "", // ContentType must not be empty 1880 ContentEncoding: "", 1881 Payload: []byte(""), 1882 }, 1883 }, 1884 expectError: true, 1885 }, 1886 }) 1887 } 1888 1889 // TestMultiHttpSettingsValidate verifies that MultiHttpSettings.Validate 1890 // performs the expected validations. 1891 // 1892 // Other tests validate various invalid values. This test verifies that a 1893 // single invalid value is reported in order to verify that the inner 1894 // validations are happening. 1895 func TestMultiHttpSettingsValidate(t *testing.T) { 1896 createEntries := func(n int, method HttpMethod, url string) []*MultiHttpEntry { 1897 entries := make([]*MultiHttpEntry, n) 1898 1899 for i := range n { 1900 entries[i] = &MultiHttpEntry{ 1901 Request: &MultiHttpEntryRequest{ 1902 Method: method, 1903 Url: url, 1904 }, 1905 } 1906 } 1907 1908 return entries 1909 } 1910 1911 testValidate(t, TestCases[*MultiHttpSettings]{ 1912 "zero value": { 1913 // the zero value for MultiHttpSettings is not usable 1914 // because at least one entry is required. 1915 input: &MultiHttpSettings{}, 1916 expectError: true, 1917 }, 1918 "valid": { 1919 input: &MultiHttpSettings{ 1920 Entries: createEntries(1, HttpMethod_GET, "http://example.com"), 1921 }, 1922 expectError: false, 1923 }, 1924 "many entries": { 1925 input: &MultiHttpSettings{ 1926 Entries: createEntries(MaxMultiHttpTargets, HttpMethod_GET, "http://example.com"), 1927 }, 1928 expectError: false, 1929 }, 1930 "too many entries": { 1931 input: &MultiHttpSettings{ 1932 Entries: createEntries(MaxMultiHttpTargets+1, HttpMethod_GET, "http://example.com"), 1933 }, 1934 expectError: true, 1935 }, 1936 "invalid request": { 1937 input: &MultiHttpSettings{ 1938 Entries: []*MultiHttpEntry{ 1939 { 1940 Request: &MultiHttpEntryRequest{ 1941 Method: HttpMethod_GET, 1942 Url: "", 1943 }, 1944 }, 1945 }, 1946 }, 1947 expectError: true, 1948 }, 1949 "invalid assertion": { 1950 input: &MultiHttpSettings{ 1951 Entries: []*MultiHttpEntry{ 1952 { 1953 Request: &MultiHttpEntryRequest{ 1954 Method: HttpMethod_GET, 1955 Url: "http://example.com", 1956 }, 1957 Assertions: []*MultiHttpEntryAssertion{ 1958 { 1959 Type: -1, 1960 }, 1961 }, 1962 }, 1963 }, 1964 }, 1965 expectError: true, 1966 }, 1967 "invalid variable": { 1968 input: &MultiHttpSettings{ 1969 Entries: []*MultiHttpEntry{ 1970 { 1971 Request: &MultiHttpEntryRequest{ 1972 Method: HttpMethod_GET, 1973 Url: "http://example.com", 1974 }, 1975 Variables: []*MultiHttpEntryVariable{ 1976 { 1977 Type: -1, 1978 }, 1979 }, 1980 }, 1981 }, 1982 }, 1983 expectError: true, 1984 }, 1985 }) 1986 } 1987 1988 func TestInClosedRange(t *testing.T) { 1989 testcases := map[string]struct { 1990 value int64 1991 lower int64 1992 upper int64 1993 expected bool 1994 }{ 1995 "too low": {value: 0, lower: 1, upper: 5, expected: false}, 1996 "lower bound": {value: 1, lower: 1, upper: 5, expected: true}, 1997 "in range": {value: 3, lower: 1, upper: 5, expected: true}, 1998 "upper bound": {value: 5, lower: 1, upper: 5, expected: true}, 1999 "too high": {value: 6, lower: 1, upper: 5, expected: false}, 2000 } 2001 2002 for name, tc := range testcases { 2003 actual := inClosedRange(tc.value, tc.lower, tc.upper) 2004 require.Equalf(t, tc.expected, actual, `%s`, name) 2005 } 2006 } 2007 2008 func TestGetCheckInstance(t *testing.T) { 2009 for _, checkType := range CheckTypeValues() { 2010 check := GetCheckInstance(checkType) 2011 require.NotNil(t, check) 2012 require.Equal(t, checkType, check.Type()) 2013 require.NoError(t, check.Validate()) 2014 } 2015 } 2016 2017 func requireRemoteInfoFields(t *testing.T) { 2018 t.Helper() 2019 2020 // The expected fields in the RemoteInfo struct. It's necessary to 2021 // assert this list so that if the implementation ever changes, we can 2022 // go update the MarshalZerologObject method accordingly. 2023 expectedFields := []string{ 2024 "Name", 2025 "Url", 2026 "Username", 2027 "Password", 2028 } 2029 2030 remoteInfoType := reflect.TypeOf(RemoteInfo{}) 2031 actualFields := make([]string, 0, remoteInfoType.NumField()) 2032 2033 for i := range remoteInfoType.NumField() { 2034 field := remoteInfoType.Field(i) 2035 if field.IsExported() { 2036 actualFields = append(actualFields, field.Name) 2037 } 2038 } 2039 2040 require.ElementsMatch(t, expectedFields, actualFields, 2041 "RemoteInfo struct fields have changed. Update the expected fields list and review any code that depends on the field order.") 2042 } 2043 2044 func TestRemoteInfoMarshalZerologObject(t *testing.T) { 2045 requireRemoteInfoFields(t) 2046 2047 remoteInfo := RemoteInfo{ 2048 Name: "the name", 2049 Url: "https://example.com", 2050 Username: "the username", 2051 Password: "the password", 2052 } 2053 2054 var buf bytes.Buffer 2055 2056 logger := zerolog.New(&buf) 2057 2058 logger.Info().Interface("remote_info", remoteInfo).Send() 2059 2060 // Note that the order of the expected fields is fixed. If the 2061 // implementation changes, this test will break. 2062 expected := `{"level":"info","remote_info":{"name":"the name","url":"https://example.com","username":"the username","password":"<encrypted>"}}` + "\n" 2063 actual := buf.String() 2064 2065 require.Equal(t, expected, actual) 2066 } 2067 2068 func TestRemoteInfoMarshalJSON(t *testing.T) { 2069 requireRemoteInfoFields(t) 2070 2071 remoteInfo := RemoteInfo{ 2072 Name: "the name", 2073 Url: "https://example.com", 2074 Username: "the username", 2075 Password: "the password", 2076 } 2077 2078 var buf bytes.Buffer 2079 2080 err := json.NewEncoder(&buf).Encode(&remoteInfo) 2081 require.NoError(t, err) 2082 2083 expected := `{"name":"the name","url":"https://example.com","username":"the username","password":"\u003cencrypted\u003e"}` 2084 actual := strings.TrimSpace(buf.String()) 2085 require.Equal(t, expected, actual, "JSON encoding of RemoteInfo did not match expected output") 2086 } 2087 2088 func TestSystemLabelsSliceIsSorted(t *testing.T) { 2089 systemLabelsSorted := slices.Clone(systemLabels) 2090 slices.Sort(systemLabelsSorted) 2091 2092 require.Equal(t, systemLabelsSorted, systemLabels, "systemLabels must be sorted") 2093 } 2094 2095 // BenchmarkIsSystemLabel pins down the performance of IsSystemLabel. 2096 // 2097 // The lookup inputs are weighted roughly 80% misses to 20% hits, which mirrors 2098 // the common case of querying user-defined labels that are not reserved. 2099 func BenchmarkIsSystemLabel(b *testing.B) { 2100 // hits: every reserved system label (~20% of the workload). 2101 hits := slices.Clone(systemLabels) 2102 2103 // misses: generated label names that are not reserved (~80% of the workload). 2104 missCount := 4 * len(hits) 2105 misses := make([]string, 0, missCount) 2106 2107 prefixes := []string{"alpha", "delta", "kilo", "november", "sierra", "zulu"} 2108 for i := range missCount { 2109 misses = append(misses, prefixes[i%len(prefixes)]+"_label_"+strconv.Itoa(i)) 2110 } 2111 2112 // Interleave one hit for every four misses so neither path runs in a long 2113 // uninterrupted block. 2114 names := make([]string, 0, len(hits)+len(misses)) 2115 for i, j := 0, 0; i < len(hits) || j < len(misses); { 2116 if i < len(hits) { 2117 names = append(names, hits[i]) 2118 i++ 2119 } 2120 2121 for k := 0; k < 4 && j < len(misses); k++ { 2122 names = append(names, misses[j]) 2123 j++ 2124 } 2125 } 2126 2127 b.ResetTimer() 2128 2129 var found bool 2130 2131 for i := 0; i < b.N; i++ { 2132 found = IsSystemLabel(names[i%len(names)]) 2133 } 2134 2135 _ = found 2136 }