github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/types/resource_test.go (about) 1 /* 2 * Copyright 2022 Gravitational, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package types 18 19 import ( 20 "testing" 21 "time" 22 23 "github.com/stretchr/testify/require" 24 ) 25 26 func TestMatchSearch(t *testing.T) { 27 t.Parallel() 28 29 cases := []struct { 30 name string 31 expectMatch require.BoolAssertionFunc 32 fieldVals []string 33 searchVals []string 34 customFn func(v string) bool 35 }{ 36 { 37 name: "no match", 38 expectMatch: require.False, 39 fieldVals: []string{"foo", "bar", "baz"}, 40 searchVals: []string{"cat"}, 41 customFn: func(v string) bool { 42 return false 43 }, 44 }, 45 { 46 name: "no match for partial match", 47 expectMatch: require.False, 48 fieldVals: []string{"foo"}, 49 searchVals: []string{"foo", "dog"}, 50 }, 51 { 52 name: "no match for partial custom match", 53 expectMatch: require.False, 54 fieldVals: []string{"foo", "bar", "baz"}, 55 searchVals: []string{"foo", "bee", "rat"}, 56 customFn: func(v string) bool { 57 return v == "bee" 58 }, 59 }, 60 { 61 name: "no match for search phrase", 62 expectMatch: require.False, 63 fieldVals: []string{"foo", "dog", "dog foo", "foodog"}, 64 searchVals: []string{"foo dog"}, 65 }, 66 { 67 name: "match", 68 expectMatch: require.True, 69 fieldVals: []string{"foo", "bar", "baz"}, 70 searchVals: []string{"baz"}, 71 }, 72 { 73 name: "match with nil search values", 74 expectMatch: require.True, 75 }, 76 { 77 name: "match with repeat search vals", 78 expectMatch: require.True, 79 fieldVals: []string{"foo", "bar", "baz"}, 80 searchVals: []string{"foo", "foo", "baz"}, 81 }, 82 { 83 name: "match for a list of search vals contained within one field value", 84 expectMatch: require.True, 85 fieldVals: []string{"foo barbaz"}, 86 searchVals: []string{"baz", "foo", "bar"}, 87 }, 88 { 89 name: "match with mix of single vals and phrases", 90 expectMatch: require.True, 91 fieldVals: []string{"foo baz", "bar"}, 92 searchVals: []string{"baz", "foo", "foo baz", "bar"}, 93 }, 94 { 95 name: "match ignore case", 96 expectMatch: require.True, 97 fieldVals: []string{"FOO barBaz"}, 98 searchVals: []string{"baZ", "foo", "BaR"}, 99 }, 100 { 101 name: "match with custom match", 102 expectMatch: require.True, 103 fieldVals: []string{"foo", "bar", "baz"}, 104 searchVals: []string{"foo", "bar", "tunnel"}, 105 customFn: func(v string) bool { 106 return v == "tunnel" 107 }, 108 }, 109 } 110 111 for _, tc := range cases { 112 tc := tc 113 t.Run(tc.name, func(t *testing.T) { 114 t.Parallel() 115 116 matched := MatchSearch(tc.fieldVals, tc.searchVals, tc.customFn) 117 tc.expectMatch(t, matched) 118 }) 119 } 120 } 121 122 func TestUnifiedNameCompare(t *testing.T) { 123 t.Parallel() 124 testCases := []struct { 125 name string 126 resourceA func(*testing.T) ResourceWithLabels 127 resourceB func(*testing.T) ResourceWithLabels 128 isDesc bool 129 expect bool 130 }{ 131 { 132 name: "sort by same kind", 133 resourceA: func(t *testing.T) ResourceWithLabels { 134 server, err := NewServer("node-cloud", KindNode, ServerSpecV2{ 135 Hostname: "node-cloud", 136 }) 137 require.NoError(t, err) 138 return server 139 }, 140 resourceB: func(t *testing.T) ResourceWithLabels { 141 server, err := NewServer("node-strawberry", KindNode, ServerSpecV2{ 142 Hostname: "node-strawberry", 143 }) 144 require.NoError(t, err) 145 return server 146 }, 147 isDesc: true, 148 expect: false, 149 }, 150 { 151 name: "sort by different kind", 152 resourceA: func(t *testing.T) ResourceWithLabels { 153 server := newAppServer(t, "app-cloud") 154 return server 155 }, 156 resourceB: func(t *testing.T) ResourceWithLabels { 157 server, err := NewServer("node-strawberry", KindNode, ServerSpecV2{ 158 Hostname: "node-strawberry", 159 }) 160 require.NoError(t, err) 161 return server 162 }, 163 isDesc: true, 164 expect: false, 165 }, 166 { 167 name: "sort with different cases", 168 resourceA: func(t *testing.T) ResourceWithLabels { 169 server := newAppServer(t, "app-cloud") 170 return server 171 }, 172 resourceB: func(t *testing.T) ResourceWithLabels { 173 server, err := NewServer("Node-strawberry", KindNode, ServerSpecV2{ 174 Hostname: "node-strawberry", 175 }) 176 require.NoError(t, err) 177 return server 178 }, 179 isDesc: true, 180 expect: false, 181 }, 182 } 183 184 for _, tc := range testCases { 185 tc := tc 186 resourceA := tc.resourceA(t) 187 resourceB := tc.resourceB(t) 188 t.Run(tc.name, func(t *testing.T) { 189 t.Parallel() 190 191 actual := unifiedNameCompare(resourceA, resourceB, tc.isDesc) 192 if actual != tc.expect { 193 t.Errorf("Expected %v, but got %v for %+v and %+v with isDesc=%v", tc.expect, actual, resourceA, resourceB, tc.isDesc) 194 } 195 }) 196 } 197 } 198 199 func TestMatchSearch_ResourceSpecific(t *testing.T) { 200 t.Parallel() 201 202 labels := map[string]string{"env": "prod", "os": "mac"} 203 204 cases := []struct { 205 name string 206 // searchNotDefined refers to resources where the searcheable field values are not defined. 207 searchNotDefined bool 208 matchingSearchVals []string 209 newResource func(*testing.T) ResourceWithLabels 210 }{ 211 { 212 name: "node", 213 matchingSearchVals: []string{"foo", "bar", "prod", "os"}, 214 newResource: func(t *testing.T) ResourceWithLabels { 215 server, err := NewServerWithLabels("_", KindNode, ServerSpecV2{ 216 Hostname: "foo", 217 Addr: "bar", 218 }, labels) 219 require.NoError(t, err) 220 221 return server 222 }, 223 }, 224 { 225 name: "node using tunnel", 226 matchingSearchVals: []string{"tunnel"}, 227 newResource: func(t *testing.T) ResourceWithLabels { 228 server, err := NewServer("_", KindNode, ServerSpecV2{ 229 UseTunnel: true, 230 }) 231 require.NoError(t, err) 232 233 return server 234 }, 235 }, 236 { 237 name: "windows desktop", 238 matchingSearchVals: []string{"foo", "bar", "env", "prod", "os"}, 239 newResource: func(t *testing.T) ResourceWithLabels { 240 desktop, err := NewWindowsDesktopV3("foo", labels, WindowsDesktopSpecV3{ 241 Addr: "bar", 242 }) 243 require.NoError(t, err) 244 245 return desktop 246 }, 247 }, 248 { 249 name: "application", 250 matchingSearchVals: []string{"foo", "bar", "baz", "mac"}, 251 newResource: func(t *testing.T) ResourceWithLabels { 252 app, err := NewAppV3(Metadata{ 253 Name: "foo", 254 Description: "bar", 255 Labels: labels, 256 }, AppSpecV3{ 257 PublicAddr: "baz", 258 URI: "_", 259 }) 260 require.NoError(t, err) 261 262 return app 263 }, 264 }, 265 { 266 name: "kube cluster", 267 matchingSearchVals: []string{"foo", "prod", "env"}, 268 newResource: func(t *testing.T) ResourceWithLabels { 269 kc, err := NewKubernetesClusterV3FromLegacyCluster("_", &KubernetesCluster{ 270 Name: "foo", 271 StaticLabels: labels, 272 }) 273 require.NoError(t, err) 274 275 return kc 276 }, 277 }, 278 { 279 name: "database", 280 matchingSearchVals: []string{"foo", "bar", "baz", "prod", DatabaseTypeRedshift}, 281 newResource: func(t *testing.T) ResourceWithLabels { 282 db, err := NewDatabaseV3(Metadata{ 283 Name: "foo", 284 Description: "bar", 285 Labels: labels, 286 }, DatabaseSpecV3{ 287 Protocol: "baz", 288 URI: "_", 289 AWS: AWS{ 290 Redshift: Redshift{ 291 ClusterID: "_", 292 }, 293 }, 294 }) 295 require.NoError(t, err) 296 297 return db 298 }, 299 }, 300 { 301 name: "database with gcp keywords", 302 matchingSearchVals: []string{"cloud", "cloud sql"}, 303 newResource: func(t *testing.T) ResourceWithLabels { 304 db, err := NewDatabaseV3(Metadata{ 305 Name: "foo", 306 Labels: labels, 307 }, DatabaseSpecV3{ 308 Protocol: "_", 309 URI: "_", 310 GCP: GCPCloudSQL{ 311 ProjectID: "_", 312 InstanceID: "_", 313 }, 314 }) 315 require.NoError(t, err) 316 317 return db 318 }, 319 }, 320 { 321 name: "app server", 322 searchNotDefined: true, 323 newResource: func(t *testing.T) ResourceWithLabels { 324 appServer, err := NewAppServerV3(Metadata{ 325 Name: "foo", 326 }, AppServerSpecV3{ 327 HostID: "_", 328 App: &AppV3{Metadata: Metadata{Name: "_"}, Spec: AppSpecV3{URI: "_"}}, 329 }) 330 require.NoError(t, err) 331 332 return appServer 333 }, 334 }, 335 { 336 name: "db server", 337 searchNotDefined: true, 338 newResource: func(t *testing.T) ResourceWithLabels { 339 db, err := NewDatabaseV3(Metadata{ 340 Name: "foo", 341 Description: "bar", 342 Labels: labels, 343 }, DatabaseSpecV3{ 344 Protocol: "baz", 345 URI: "_", 346 AWS: AWS{ 347 Redshift: Redshift{ 348 ClusterID: "_", 349 }, 350 }, 351 }) 352 require.NoError(t, err) 353 dbServer, err := NewDatabaseServerV3(Metadata{ 354 Name: "foo", 355 }, DatabaseServerSpecV3{ 356 HostID: "_", 357 Hostname: "_", 358 Database: db, 359 }) 360 require.NoError(t, err) 361 362 return dbServer 363 }, 364 }, 365 { 366 name: "kube server", 367 searchNotDefined: true, 368 newResource: func(t *testing.T) ResourceWithLabels { 369 kubeServer, err := NewKubernetesServerV3( 370 Metadata{ 371 Name: "foo", 372 }, KubernetesServerSpecV3{ 373 HostID: "_", 374 Hostname: "_", 375 Cluster: &KubernetesClusterV3{ 376 Metadata: Metadata{ 377 Name: "_", 378 }, 379 }, 380 }) 381 require.NoError(t, err) 382 383 return kubeServer 384 }, 385 }, 386 { 387 name: "desktop service", 388 searchNotDefined: true, 389 newResource: func(t *testing.T) ResourceWithLabels { 390 desktopService, err := NewWindowsDesktopServiceV3(Metadata{ 391 Name: "foo", 392 }, WindowsDesktopServiceSpecV3{ 393 Addr: "_", 394 TeleportVersion: "_", 395 }) 396 require.NoError(t, err) 397 398 return desktopService 399 }, 400 }, 401 } 402 403 for _, tc := range cases { 404 tc := tc 405 t.Run(tc.name, func(t *testing.T) { 406 t.Parallel() 407 408 resource := tc.newResource(t) 409 410 // Nil search values, should always return true 411 match := resource.MatchSearch(nil) 412 require.True(t, match) 413 414 switch { 415 case tc.searchNotDefined: 416 // Trying to search something in resources without search field values defined 417 // should always return false. 418 match := resource.MatchSearch([]string{"_"}) 419 require.False(t, match) 420 default: 421 // Test no match. 422 match := resource.MatchSearch([]string{"foo", "llama"}) 423 require.False(t, match) 424 425 // Test match. 426 match = resource.MatchSearch(tc.matchingSearchVals) 427 require.True(t, match) 428 } 429 }) 430 } 431 } 432 433 func TestResourcesWithLabels_ToMap(t *testing.T) { 434 mkServerHost := func(name string, hostname string) ResourceWithLabels { 435 server, err := NewServerWithLabels(name, KindNode, ServerSpecV2{ 436 Hostname: hostname + ".example.com", 437 Addr: name + ".example.com", 438 }, nil) 439 require.NoError(t, err) 440 441 return server 442 } 443 444 mkServer := func(name string) ResourceWithLabels { 445 return mkServerHost(name, name) 446 } 447 448 tests := []struct { 449 name string 450 r ResourcesWithLabels 451 want ResourcesWithLabelsMap 452 }{ 453 { 454 name: "empty", 455 r: nil, 456 want: map[string]ResourceWithLabels{}, 457 }, 458 { 459 name: "simple list", 460 r: []ResourceWithLabels{mkServer("a"), mkServer("b"), mkServer("c")}, 461 want: map[string]ResourceWithLabels{ 462 "a": mkServer("a"), 463 "b": mkServer("b"), 464 "c": mkServer("c"), 465 }, 466 }, 467 { 468 name: "first duplicate wins", 469 r: []ResourceWithLabels{mkServerHost("a", "a1"), mkServerHost("a", "a2"), mkServerHost("a", "a3")}, 470 want: map[string]ResourceWithLabels{ 471 "a": mkServerHost("a", "a1"), 472 }, 473 }, 474 } 475 476 for _, tt := range tests { 477 t.Run(tt.name, func(t *testing.T) { 478 require.Equal(t, tt.want, tt.r.ToMap()) 479 }) 480 } 481 } 482 483 func TestValidLabelKey(t *testing.T) { 484 for _, tc := range []struct { 485 label string 486 valid bool 487 }{ 488 { 489 label: "1x/Y*_-", 490 valid: true, 491 }, 492 { 493 label: "x:y", 494 valid: true, 495 }, 496 { 497 label: "x\\y", 498 valid: false, 499 }, 500 } { 501 isValid := IsValidLabelKey(tc.label) 502 require.Equal(t, tc.valid, isValid) 503 } 504 } 505 506 func TestFriendlyName(t *testing.T) { 507 newApp := func(t *testing.T, name, description string, labels map[string]string) Application { 508 app, err := NewAppV3(Metadata{ 509 Name: name, 510 Description: description, 511 Labels: labels, 512 }, AppSpecV3{ 513 URI: "https://some-uri.com", 514 }) 515 require.NoError(t, err) 516 517 return app 518 } 519 520 newGroup := func(t *testing.T, name, description string, labels map[string]string) UserGroup { 521 group, err := NewUserGroup(Metadata{ 522 Name: name, 523 Description: description, 524 Labels: labels, 525 }, UserGroupSpecV1{}) 526 require.NoError(t, err) 527 528 return group 529 } 530 531 newRole := func(t *testing.T, name string, labels map[string]string) Role { 532 role, err := NewRole(name, RoleSpecV6{}) 533 require.NoError(t, err) 534 metadata := role.GetMetadata() 535 metadata.Labels = labels 536 role.SetMetadata(metadata) 537 return role 538 } 539 540 node, err := NewServer("node", KindNode, ServerSpecV2{ 541 Hostname: "friendly hostname", 542 }) 543 require.NoError(t, err) 544 545 tests := []struct { 546 name string 547 resource ResourceWithLabels 548 expected string 549 }{ 550 { 551 name: "no friendly name", 552 resource: newApp(t, "no friendly", "no friendly", map[string]string{}), 553 expected: "", 554 }, 555 { 556 name: "friendly app name (uses description)", 557 resource: newApp(t, "friendly", "friendly name", map[string]string{ 558 OriginLabel: OriginOkta, 559 }), 560 expected: "friendly name", 561 }, 562 { 563 name: "friendly app name (uses label)", 564 resource: newApp(t, "friendly", "friendly name", map[string]string{ 565 OriginLabel: OriginOkta, 566 OktaAppNameLabel: "label friendly name", 567 }), 568 expected: "label friendly name", 569 }, 570 { 571 name: "friendly group name (uses description)", 572 resource: newGroup(t, "friendly", "friendly name", map[string]string{ 573 OriginLabel: OriginOkta, 574 }), 575 expected: "friendly name", 576 }, 577 { 578 name: "friendly group name (uses label)", 579 resource: newGroup(t, "friendly", "friendly name", map[string]string{ 580 OriginLabel: OriginOkta, 581 OktaGroupNameLabel: "label friendly name", 582 }), 583 expected: "label friendly name", 584 }, 585 { 586 name: "friendly role name (uses label)", 587 resource: newRole(t, "friendly", map[string]string{ 588 OriginLabel: OriginOkta, 589 OktaRoleNameLabel: "label friendly name", 590 }), 591 expected: "label friendly name", 592 }, 593 { 594 name: "friendly node name", 595 resource: node, 596 expected: "friendly hostname", 597 }, 598 } 599 600 for _, test := range tests { 601 test := test 602 t.Run(test.name, func(t *testing.T) { 603 require.Equal(t, test.expected, FriendlyName(test.resource)) 604 }) 605 } 606 } 607 608 func TestMetadataIsEqual(t *testing.T) { 609 newMetadata := func(changeFns ...func(*Metadata)) *Metadata { 610 metadata := &Metadata{ 611 Name: "name", 612 Namespace: "namespace", 613 Description: "description", 614 Labels: map[string]string{"label1": "value1"}, 615 Expires: &time.Time{}, 616 ID: 1234, 617 Revision: "aaaa", 618 } 619 620 for _, fn := range changeFns { 621 fn(metadata) 622 } 623 624 return metadata 625 } 626 tests := []struct { 627 name string 628 m1 *Metadata 629 m2 *Metadata 630 expected bool 631 }{ 632 { 633 name: "empty equals", 634 m1: &Metadata{}, 635 m2: &Metadata{}, 636 expected: true, 637 }, 638 { 639 name: "nil equals", 640 m1: nil, 641 m2: (*Metadata)(nil), 642 expected: true, 643 }, 644 { 645 name: "one is nil", 646 m1: &Metadata{}, 647 m2: (*Metadata)(nil), 648 expected: false, 649 }, 650 { 651 name: "populated equals", 652 m1: newMetadata(), 653 m2: newMetadata(), 654 expected: true, 655 }, 656 { 657 name: "id and revision have no effect", 658 m1: newMetadata(), 659 m2: newMetadata(func(m *Metadata) { 660 m.ID = 7890 661 m.Revision = "bbbb" 662 }), 663 expected: true, 664 }, 665 { 666 name: "name is different", 667 m1: newMetadata(), 668 m2: newMetadata(func(m *Metadata) { 669 m.Name = "different-name" 670 }), 671 expected: false, 672 }, 673 { 674 name: "namespace is different", 675 m1: newMetadata(), 676 m2: newMetadata(func(m *Metadata) { 677 m.Namespace = "different-namespace" 678 }), 679 expected: false, 680 }, 681 { 682 name: "description is different", 683 m1: newMetadata(), 684 m2: newMetadata(func(m *Metadata) { 685 m.Description = "different-description" 686 }), 687 expected: false, 688 }, 689 { 690 name: "labels is different", 691 m1: newMetadata(), 692 m2: newMetadata(func(m *Metadata) { 693 m.Labels = map[string]string{"label2": "value2"} 694 }), 695 expected: false, 696 }, 697 { 698 name: "expires is different", 699 m1: newMetadata(), 700 m2: newMetadata(func(m *Metadata) { 701 newTime := time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC) 702 m.Expires = &newTime 703 }), 704 expected: false, 705 }, 706 { 707 name: "expires both nil", 708 m1: newMetadata(func(m *Metadata) { m.Expires = nil }), 709 m2: newMetadata(func(m *Metadata) { m.Expires = nil }), 710 expected: true, 711 }, 712 { 713 name: "expires m1 nil", 714 m1: newMetadata(func(m *Metadata) { m.Expires = nil }), 715 m2: newMetadata(), 716 expected: false, 717 }, 718 { 719 name: "expires m2 nil", 720 m1: newMetadata(), 721 m2: newMetadata(func(m *Metadata) { m.Expires = nil }), 722 expected: false, 723 }, 724 } 725 726 for _, test := range tests { 727 t.Run(test.name, func(t *testing.T) { 728 require.Equal(t, test.expected, test.m1.IsEqual(test.m2)) 729 }) 730 } 731 } 732 733 func TestResourceHeaderIsEqual(t *testing.T) { 734 newHeader := func(changeFns ...func(*ResourceHeader)) *ResourceHeader { 735 header := &ResourceHeader{ 736 Kind: "kind", 737 SubKind: "subkind", 738 Version: "v1", 739 Metadata: Metadata{ 740 Name: "name", 741 Namespace: "namespace", 742 Description: "description", 743 Labels: map[string]string{"label1": "value1"}, 744 Expires: &time.Time{}, 745 ID: 1234, 746 Revision: "aaaa", 747 }, 748 } 749 750 for _, fn := range changeFns { 751 fn(header) 752 } 753 754 return header 755 } 756 tests := []struct { 757 name string 758 h1 *ResourceHeader 759 h2 *ResourceHeader 760 expected bool 761 }{ 762 { 763 name: "empty equals", 764 h1: &ResourceHeader{}, 765 h2: &ResourceHeader{}, 766 expected: true, 767 }, 768 { 769 name: "nil equals", 770 h1: nil, 771 h2: (*ResourceHeader)(nil), 772 expected: true, 773 }, 774 { 775 name: "one is nil", 776 h1: &ResourceHeader{}, 777 h2: (*ResourceHeader)(nil), 778 expected: false, 779 }, 780 { 781 name: "populated equals", 782 h1: newHeader(), 783 h2: newHeader(), 784 expected: true, 785 }, 786 { 787 name: "kind is different", 788 h1: newHeader(), 789 h2: newHeader(func(h *ResourceHeader) { 790 h.Kind = "different-kind" 791 }), 792 expected: false, 793 }, 794 { 795 name: "subkind is different", 796 h1: newHeader(), 797 h2: newHeader(func(h *ResourceHeader) { 798 h.SubKind = "different-subkind" 799 }), 800 expected: false, 801 }, 802 { 803 name: "metadata is different", 804 h1: newHeader(), 805 h2: newHeader(func(h *ResourceHeader) { 806 h.Metadata = Metadata{} 807 }), 808 expected: false, 809 }, 810 { 811 name: "version is different", 812 h1: newHeader(), 813 h2: newHeader(func(h *ResourceHeader) { 814 h.Version = "different-version" 815 }), 816 expected: false, 817 }, 818 } 819 820 for _, test := range tests { 821 t.Run(test.name, func(t *testing.T) { 822 require.Equal(t, test.expected, test.h1.IsEqual(test.h2)) 823 }) 824 } 825 }