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  }