k8s.io/client-go@v0.22.2/tools/clientcmd/validation_test.go (about)

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     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 clientcmd
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"strings"
    25  	"testing"
    26  
    27  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    28  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    29  )
    30  
    31  func TestConfirmUsableBadInfoButOkConfig(t *testing.T) {
    32  	config := clientcmdapi.NewConfig()
    33  	config.Clusters["missing ca"] = &clientcmdapi.Cluster{
    34  		Server:               "anything",
    35  		CertificateAuthority: "missing",
    36  	}
    37  	config.AuthInfos["error"] = &clientcmdapi.AuthInfo{
    38  		Username: "anything",
    39  		Token:    "here",
    40  	}
    41  	config.Contexts["dirty"] = &clientcmdapi.Context{
    42  		Cluster:  "missing ca",
    43  		AuthInfo: "error",
    44  	}
    45  	config.Clusters["clean"] = &clientcmdapi.Cluster{
    46  		Server: "anything",
    47  	}
    48  	config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
    49  		Token: "here",
    50  	}
    51  	config.Contexts["clean"] = &clientcmdapi.Context{
    52  		Cluster:  "clean",
    53  		AuthInfo: "clean",
    54  	}
    55  
    56  	badValidation := configValidationTest{
    57  		config:                 config,
    58  		expectedErrorSubstring: []string{"unable to read certificate-authority"},
    59  	}
    60  	okTest := configValidationTest{
    61  		config: config,
    62  	}
    63  
    64  	okTest.testConfirmUsable("clean", t)
    65  	badValidation.testConfig(t)
    66  }
    67  
    68  func TestConfirmUsableBadInfoConfig(t *testing.T) {
    69  	config := clientcmdapi.NewConfig()
    70  	config.Clusters["missing ca"] = &clientcmdapi.Cluster{
    71  		Server:               "anything",
    72  		CertificateAuthority: "missing",
    73  	}
    74  	config.AuthInfos["error"] = &clientcmdapi.AuthInfo{
    75  		Username: "anything",
    76  		Token:    "here",
    77  	}
    78  	config.Contexts["first"] = &clientcmdapi.Context{
    79  		Cluster:  "missing ca",
    80  		AuthInfo: "error",
    81  	}
    82  	test := configValidationTest{
    83  		config:                 config,
    84  		expectedErrorSubstring: []string{"unable to read certificate-authority"},
    85  	}
    86  
    87  	test.testConfirmUsable("first", t)
    88  }
    89  
    90  func TestConfirmUsableEmptyConfig(t *testing.T) {
    91  	config := clientcmdapi.NewConfig()
    92  	test := configValidationTest{
    93  		config:                 config,
    94  		expectedErrorSubstring: []string{"invalid configuration: no configuration has been provided"},
    95  	}
    96  
    97  	test.testConfirmUsable("", t)
    98  }
    99  
   100  func TestConfirmUsableMissingConfig(t *testing.T) {
   101  	config := clientcmdapi.NewConfig()
   102  	test := configValidationTest{
   103  		config:                 config,
   104  		expectedErrorSubstring: []string{"invalid configuration: no configuration has been provided"},
   105  	}
   106  
   107  	test.testConfirmUsable("not-here", t)
   108  }
   109  
   110  func TestValidateEmptyConfig(t *testing.T) {
   111  	config := clientcmdapi.NewConfig()
   112  	test := configValidationTest{
   113  		config:                 config,
   114  		expectedErrorSubstring: []string{"invalid configuration: no configuration has been provided"},
   115  	}
   116  
   117  	test.testConfig(t)
   118  }
   119  
   120  func TestValidateMissingCurrentContextConfig(t *testing.T) {
   121  	config := clientcmdapi.NewConfig()
   122  	config.CurrentContext = "anything"
   123  	test := configValidationTest{
   124  		config:                 config,
   125  		expectedErrorSubstring: []string{"context was not found for specified "},
   126  	}
   127  
   128  	test.testConfig(t)
   129  }
   130  
   131  func TestIsContextNotFound(t *testing.T) {
   132  	config := clientcmdapi.NewConfig()
   133  	config.CurrentContext = "anything"
   134  
   135  	err := Validate(*config)
   136  	if !IsContextNotFound(err) {
   137  		t.Errorf("Expected context not found, but got %v", err)
   138  	}
   139  	if !IsConfigurationInvalid(err) {
   140  		t.Errorf("Expected configuration invalid, but got %v", err)
   141  	}
   142  }
   143  
   144  func TestIsEmptyConfig(t *testing.T) {
   145  	config := clientcmdapi.NewConfig()
   146  
   147  	err := Validate(*config)
   148  	if !IsEmptyConfig(err) {
   149  		t.Errorf("Expected context not found, but got %v", err)
   150  	}
   151  	if !IsConfigurationInvalid(err) {
   152  		t.Errorf("Expected configuration invalid, but got %v", err)
   153  	}
   154  }
   155  
   156  func TestIsConfigurationInvalid(t *testing.T) {
   157  	if newErrConfigurationInvalid([]error{}) != nil {
   158  		t.Errorf("unexpected error")
   159  	}
   160  	if newErrConfigurationInvalid([]error{ErrNoContext}) == ErrNoContext {
   161  		t.Errorf("unexpected error")
   162  	}
   163  	if newErrConfigurationInvalid([]error{ErrNoContext, ErrNoContext}) == nil {
   164  		t.Errorf("unexpected error")
   165  	}
   166  	if !IsConfigurationInvalid(newErrConfigurationInvalid([]error{ErrNoContext, ErrNoContext})) {
   167  		t.Errorf("unexpected error")
   168  	}
   169  }
   170  
   171  func TestValidateMissingReferencesConfig(t *testing.T) {
   172  	config := clientcmdapi.NewConfig()
   173  	config.CurrentContext = "anything"
   174  	config.Contexts["anything"] = &clientcmdapi.Context{Cluster: "missing", AuthInfo: "missing"}
   175  	test := configValidationTest{
   176  		config:                 config,
   177  		expectedErrorSubstring: []string{"user \"missing\" was not found for context \"anything\"", "cluster \"missing\" was not found for context \"anything\""},
   178  	}
   179  
   180  	test.testContext("anything", t)
   181  	test.testConfig(t)
   182  }
   183  
   184  func TestValidateEmptyContext(t *testing.T) {
   185  	config := clientcmdapi.NewConfig()
   186  	config.CurrentContext = "anything"
   187  	config.Contexts["anything"] = &clientcmdapi.Context{}
   188  	test := configValidationTest{
   189  		config:                 config,
   190  		expectedErrorSubstring: []string{"user was not specified for context \"anything\"", "cluster was not specified for context \"anything\""},
   191  	}
   192  
   193  	test.testContext("anything", t)
   194  	test.testConfig(t)
   195  }
   196  
   197  func TestValidateEmptyContextName(t *testing.T) {
   198  	config := clientcmdapi.NewConfig()
   199  	config.CurrentContext = "anything"
   200  	config.Contexts[""] = &clientcmdapi.Context{Cluster: "missing", AuthInfo: "missing"}
   201  	test := configValidationTest{
   202  		config:                 config,
   203  		expectedErrorSubstring: []string{"empty context name", "is not allowed"},
   204  	}
   205  
   206  	test.testContext("", t)
   207  	test.testConfig(t)
   208  }
   209  
   210  func TestValidateEmptyClusterInfo(t *testing.T) {
   211  	config := clientcmdapi.NewConfig()
   212  	config.Clusters["empty"] = clientcmdapi.NewCluster()
   213  	test := configValidationTest{
   214  		config:                 config,
   215  		expectedErrorSubstring: []string{"cluster has no server defined"},
   216  	}
   217  
   218  	test.testCluster("empty", t)
   219  	test.testConfig(t)
   220  }
   221  
   222  func TestValidateClusterInfoErrEmptyCluster(t *testing.T) {
   223  	cluster := clientcmdapi.NewCluster()
   224  	errs := validateClusterInfo("", *cluster)
   225  
   226  	if len(errs) != 1 {
   227  		t.Fatalf("unexpected errors: %v", errs)
   228  	}
   229  	if errs[0] != ErrEmptyCluster {
   230  		t.Errorf("unexpected error: %v", errs[0])
   231  	}
   232  }
   233  
   234  func TestValidateMissingCAFileClusterInfo(t *testing.T) {
   235  	config := clientcmdapi.NewConfig()
   236  	config.Clusters["missing ca"] = &clientcmdapi.Cluster{
   237  		Server:               "anything",
   238  		CertificateAuthority: "missing",
   239  	}
   240  	test := configValidationTest{
   241  		config:                 config,
   242  		expectedErrorSubstring: []string{"unable to read certificate-authority"},
   243  	}
   244  
   245  	test.testCluster("missing ca", t)
   246  	test.testConfig(t)
   247  }
   248  
   249  func TestValidateCleanClusterInfo(t *testing.T) {
   250  	config := clientcmdapi.NewConfig()
   251  	config.Clusters["clean"] = &clientcmdapi.Cluster{
   252  		Server: "anything",
   253  	}
   254  	test := configValidationTest{
   255  		config: config,
   256  	}
   257  
   258  	test.testCluster("clean", t)
   259  	test.testConfig(t)
   260  }
   261  
   262  func TestValidateCleanWithCAClusterInfo(t *testing.T) {
   263  	tempFile, _ := ioutil.TempFile("", "")
   264  	defer os.Remove(tempFile.Name())
   265  
   266  	config := clientcmdapi.NewConfig()
   267  	config.Clusters["clean"] = &clientcmdapi.Cluster{
   268  		Server:               "anything",
   269  		CertificateAuthority: tempFile.Name(),
   270  	}
   271  	test := configValidationTest{
   272  		config: config,
   273  	}
   274  
   275  	test.testCluster("clean", t)
   276  	test.testConfig(t)
   277  }
   278  
   279  func TestValidateEmptyAuthInfo(t *testing.T) {
   280  	config := clientcmdapi.NewConfig()
   281  	config.AuthInfos["error"] = &clientcmdapi.AuthInfo{}
   282  	test := configValidationTest{
   283  		config: config,
   284  	}
   285  
   286  	test.testAuthInfo("error", t)
   287  	test.testConfig(t)
   288  }
   289  
   290  func TestValidateCertFilesNotFoundAuthInfo(t *testing.T) {
   291  	config := clientcmdapi.NewConfig()
   292  	config.AuthInfos["error"] = &clientcmdapi.AuthInfo{
   293  		ClientCertificate: "missing",
   294  		ClientKey:         "missing",
   295  	}
   296  	test := configValidationTest{
   297  		config:                 config,
   298  		expectedErrorSubstring: []string{"unable to read client-cert", "unable to read client-key"},
   299  	}
   300  
   301  	test.testAuthInfo("error", t)
   302  	test.testConfig(t)
   303  }
   304  
   305  func TestValidateCertDataOverridesFiles(t *testing.T) {
   306  	tempFile, _ := ioutil.TempFile("", "")
   307  	defer os.Remove(tempFile.Name())
   308  
   309  	config := clientcmdapi.NewConfig()
   310  	config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
   311  		ClientCertificate:     tempFile.Name(),
   312  		ClientCertificateData: []byte("certdata"),
   313  		ClientKey:             tempFile.Name(),
   314  		ClientKeyData:         []byte("keydata"),
   315  	}
   316  	test := configValidationTest{
   317  		config:                 config,
   318  		expectedErrorSubstring: []string{"client-cert-data and client-cert are both specified", "client-key-data and client-key are both specified"},
   319  	}
   320  
   321  	test.testAuthInfo("clean", t)
   322  	test.testConfig(t)
   323  }
   324  
   325  func TestValidateCleanCertFilesAuthInfo(t *testing.T) {
   326  	tempFile, _ := ioutil.TempFile("", "")
   327  	defer os.Remove(tempFile.Name())
   328  
   329  	config := clientcmdapi.NewConfig()
   330  	config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
   331  		ClientCertificate: tempFile.Name(),
   332  		ClientKey:         tempFile.Name(),
   333  	}
   334  	test := configValidationTest{
   335  		config: config,
   336  	}
   337  
   338  	test.testAuthInfo("clean", t)
   339  	test.testConfig(t)
   340  }
   341  
   342  func TestValidateCleanTokenAuthInfo(t *testing.T) {
   343  	config := clientcmdapi.NewConfig()
   344  	config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
   345  		Token: "any-value",
   346  	}
   347  	test := configValidationTest{
   348  		config: config,
   349  	}
   350  
   351  	test.testAuthInfo("clean", t)
   352  	test.testConfig(t)
   353  }
   354  
   355  func TestValidateMultipleMethodsAuthInfo(t *testing.T) {
   356  	config := clientcmdapi.NewConfig()
   357  	config.AuthInfos["error"] = &clientcmdapi.AuthInfo{
   358  		Token:    "token",
   359  		Username: "username",
   360  	}
   361  	test := configValidationTest{
   362  		config:                 config,
   363  		expectedErrorSubstring: []string{"more than one authentication method", "token", "basicAuth"},
   364  	}
   365  
   366  	test.testAuthInfo("error", t)
   367  	test.testConfig(t)
   368  }
   369  
   370  func TestValidateAuthInfoExec(t *testing.T) {
   371  	config := clientcmdapi.NewConfig()
   372  	config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
   373  		Exec: &clientcmdapi.ExecConfig{
   374  			Command:    "/bin/example",
   375  			APIVersion: "clientauthentication.k8s.io/v1alpha1",
   376  			Args:       []string{"hello", "world"},
   377  			Env: []clientcmdapi.ExecEnvVar{
   378  				{Name: "foo", Value: "bar"},
   379  			},
   380  			InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
   381  		},
   382  	}
   383  	test := configValidationTest{
   384  		config: config,
   385  	}
   386  
   387  	test.testAuthInfo("user", t)
   388  	test.testConfig(t)
   389  }
   390  
   391  func TestValidateAuthInfoExecNoVersion(t *testing.T) {
   392  	config := clientcmdapi.NewConfig()
   393  	config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
   394  		Exec: &clientcmdapi.ExecConfig{
   395  			Command:         "/bin/example",
   396  			InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
   397  		},
   398  	}
   399  	test := configValidationTest{
   400  		config: config,
   401  		expectedErrorSubstring: []string{
   402  			"apiVersion must be specified for user to use exec authentication plugin",
   403  		},
   404  	}
   405  
   406  	test.testAuthInfo("user", t)
   407  	test.testConfig(t)
   408  }
   409  
   410  func TestValidateAuthInfoExecNoCommand(t *testing.T) {
   411  	config := clientcmdapi.NewConfig()
   412  	config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
   413  		Exec: &clientcmdapi.ExecConfig{
   414  			APIVersion:      "clientauthentication.k8s.io/v1alpha1",
   415  			InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
   416  		},
   417  	}
   418  	test := configValidationTest{
   419  		config: config,
   420  		expectedErrorSubstring: []string{
   421  			"command must be specified for user to use exec authentication plugin",
   422  		},
   423  	}
   424  
   425  	test.testAuthInfo("user", t)
   426  	test.testConfig(t)
   427  }
   428  
   429  func TestValidateAuthInfoExecWithAuthProvider(t *testing.T) {
   430  	config := clientcmdapi.NewConfig()
   431  	config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
   432  		AuthProvider: &clientcmdapi.AuthProviderConfig{
   433  			Name: "oidc",
   434  		},
   435  		Exec: &clientcmdapi.ExecConfig{
   436  			Command:         "/bin/example",
   437  			APIVersion:      "clientauthentication.k8s.io/v1alpha1",
   438  			InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
   439  		},
   440  	}
   441  	test := configValidationTest{
   442  		config: config,
   443  		expectedErrorSubstring: []string{
   444  			"authProvider cannot be provided in combination with an exec plugin for user",
   445  		},
   446  	}
   447  
   448  	test.testAuthInfo("user", t)
   449  	test.testConfig(t)
   450  }
   451  
   452  func TestValidateAuthInfoExecNoEnv(t *testing.T) {
   453  	config := clientcmdapi.NewConfig()
   454  	config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
   455  		Exec: &clientcmdapi.ExecConfig{
   456  			Command:    "/bin/example",
   457  			APIVersion: "clientauthentication.k8s.io/v1alpha1",
   458  			Env: []clientcmdapi.ExecEnvVar{
   459  				{Name: "foo", Value: ""},
   460  			},
   461  			InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
   462  		},
   463  	}
   464  	test := configValidationTest{
   465  		config: config,
   466  	}
   467  
   468  	test.testAuthInfo("user", t)
   469  	test.testConfig(t)
   470  }
   471  
   472  func TestValidateAuthInfoExecInteractiveModeMissing(t *testing.T) {
   473  	config := clientcmdapi.NewConfig()
   474  	config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
   475  		Exec: &clientcmdapi.ExecConfig{
   476  			Command:    "/bin/example",
   477  			APIVersion: "clientauthentication.k8s.io/v1alpha1",
   478  		},
   479  	}
   480  	test := configValidationTest{
   481  		config: config,
   482  		expectedErrorSubstring: []string{
   483  			"interactiveMode must be specified for user to use exec authentication plugin",
   484  		},
   485  	}
   486  
   487  	test.testAuthInfo("user", t)
   488  	test.testConfig(t)
   489  }
   490  
   491  func TestValidateAuthInfoExecInteractiveModeInvalid(t *testing.T) {
   492  	config := clientcmdapi.NewConfig()
   493  	config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
   494  		Exec: &clientcmdapi.ExecConfig{
   495  			Command:         "/bin/example",
   496  			APIVersion:      "clientauthentication.k8s.io/v1alpha1",
   497  			InteractiveMode: "invalid",
   498  		},
   499  	}
   500  	test := configValidationTest{
   501  		config: config,
   502  		expectedErrorSubstring: []string{
   503  			`invalid interactiveMode for user: "invalid"`,
   504  		},
   505  	}
   506  
   507  	test.testAuthInfo("user", t)
   508  	test.testConfig(t)
   509  }
   510  
   511  type configValidationTest struct {
   512  	config                 *clientcmdapi.Config
   513  	expectedErrorSubstring []string
   514  }
   515  
   516  func (c configValidationTest) testContext(contextName string, t *testing.T) {
   517  	errs := validateContext(contextName, *c.config.Contexts[contextName], *c.config)
   518  
   519  	if len(c.expectedErrorSubstring) != 0 {
   520  		if len(errs) == 0 {
   521  			t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
   522  		}
   523  		for _, curr := range c.expectedErrorSubstring {
   524  			if len(errs) != 0 && !strings.Contains(utilerrors.NewAggregate(errs).Error(), curr) {
   525  				t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, utilerrors.NewAggregate(errs))
   526  			}
   527  		}
   528  
   529  	} else {
   530  		if len(errs) != 0 {
   531  			t.Errorf("Unexpected error: %v", utilerrors.NewAggregate(errs))
   532  		}
   533  	}
   534  }
   535  
   536  func (c configValidationTest) testConfirmUsable(contextName string, t *testing.T) {
   537  	err := ConfirmUsable(*c.config, contextName)
   538  
   539  	if len(c.expectedErrorSubstring) != 0 {
   540  		if err == nil {
   541  			t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
   542  		} else {
   543  			for _, curr := range c.expectedErrorSubstring {
   544  				if err != nil && !strings.Contains(err.Error(), curr) {
   545  					t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, err)
   546  				}
   547  			}
   548  		}
   549  	} else {
   550  		if err != nil {
   551  			t.Errorf("Unexpected error: %v", err)
   552  		}
   553  	}
   554  }
   555  
   556  func (c configValidationTest) testConfig(t *testing.T) {
   557  	err := Validate(*c.config)
   558  
   559  	if len(c.expectedErrorSubstring) != 0 {
   560  		if err == nil {
   561  			t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
   562  		} else {
   563  			for _, curr := range c.expectedErrorSubstring {
   564  				if err != nil && !strings.Contains(err.Error(), curr) {
   565  					t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, err)
   566  				}
   567  			}
   568  			if !IsConfigurationInvalid(err) {
   569  				t.Errorf("all errors should be configuration invalid: %v", err)
   570  			}
   571  		}
   572  	} else {
   573  		if err != nil {
   574  			t.Errorf("Unexpected error: %v", err)
   575  		}
   576  	}
   577  }
   578  
   579  func (c configValidationTest) testCluster(clusterName string, t *testing.T) {
   580  	errs := validateClusterInfo(clusterName, *c.config.Clusters[clusterName])
   581  
   582  	if len(c.expectedErrorSubstring) != 0 {
   583  		if len(errs) == 0 {
   584  			t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
   585  		}
   586  		for _, curr := range c.expectedErrorSubstring {
   587  			if len(errs) != 0 && !strings.Contains(utilerrors.NewAggregate(errs).Error(), curr) {
   588  				t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, utilerrors.NewAggregate(errs))
   589  			}
   590  		}
   591  
   592  	} else {
   593  		if len(errs) != 0 {
   594  			t.Errorf("Unexpected error: %v", utilerrors.NewAggregate(errs))
   595  		}
   596  	}
   597  }
   598  
   599  func (c configValidationTest) testAuthInfo(authInfoName string, t *testing.T) {
   600  	errs := validateAuthInfo(authInfoName, *c.config.AuthInfos[authInfoName])
   601  
   602  	if len(c.expectedErrorSubstring) != 0 {
   603  		if len(errs) == 0 {
   604  			t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
   605  		}
   606  		for _, curr := range c.expectedErrorSubstring {
   607  			if len(errs) != 0 && !strings.Contains(utilerrors.NewAggregate(errs).Error(), curr) {
   608  				t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, utilerrors.NewAggregate(errs))
   609  			}
   610  		}
   611  
   612  	} else {
   613  		if len(errs) != 0 {
   614  			t.Errorf("Unexpected error: %v", utilerrors.NewAggregate(errs))
   615  		}
   616  	}
   617  }
   618  
   619  type alwaysMatchingError struct{}
   620  
   621  func (_ alwaysMatchingError) Error() string {
   622  	return "error"
   623  }
   624  
   625  func (_ alwaysMatchingError) Is(_ error) bool {
   626  	return true
   627  }
   628  
   629  type someError struct{ msg string }
   630  
   631  func (se someError) Error() string {
   632  	if se.msg != "" {
   633  		return se.msg
   634  	}
   635  	return "err"
   636  }
   637  
   638  func TestErrConfigurationInvalidWithErrorsIs(t *testing.T) {
   639  	testCases := []struct {
   640  		name         string
   641  		err          error
   642  		matchAgainst error
   643  		expectMatch  bool
   644  	}{
   645  		{
   646  			name:         "no match",
   647  			err:          errConfigurationInvalid{errors.New("my-error"), errors.New("my-other-error")},
   648  			matchAgainst: fmt.Errorf("no entry %s", "here"),
   649  		},
   650  		{
   651  			name:         "match via .Is()",
   652  			err:          errConfigurationInvalid{errors.New("forbidden"), alwaysMatchingError{}},
   653  			matchAgainst: errors.New("unauthorized"),
   654  			expectMatch:  true,
   655  		},
   656  		{
   657  			name:         "match via equality",
   658  			err:          errConfigurationInvalid{errors.New("err"), someError{}},
   659  			matchAgainst: someError{},
   660  			expectMatch:  true,
   661  		},
   662  		{
   663  			name:         "match via nested aggregate",
   664  			err:          errConfigurationInvalid{errors.New("closed today"), errConfigurationInvalid{errConfigurationInvalid{someError{}}}},
   665  			matchAgainst: someError{},
   666  			expectMatch:  true,
   667  		},
   668  		{
   669  			name:         "match via wrapped aggregate",
   670  			err:          fmt.Errorf("wrap: %w", errConfigurationInvalid{errors.New("err"), someError{}}),
   671  			matchAgainst: someError{},
   672  			expectMatch:  true,
   673  		},
   674  	}
   675  
   676  	for _, tc := range testCases {
   677  		t.Run(tc.name, func(t *testing.T) {
   678  			result := errors.Is(tc.err, tc.matchAgainst)
   679  			if result != tc.expectMatch {
   680  				t.Errorf("expected match: %t, got match: %t", tc.expectMatch, result)
   681  			}
   682  		})
   683  	}
   684  }
   685  
   686  type accessTrackingError struct {
   687  	wasAccessed bool
   688  }
   689  
   690  func (accessTrackingError) Error() string {
   691  	return "err"
   692  }
   693  
   694  func (ate *accessTrackingError) Is(_ error) bool {
   695  	ate.wasAccessed = true
   696  	return true
   697  }
   698  
   699  var _ error = &accessTrackingError{}
   700  
   701  func TestErrConfigurationInvalidWithErrorsIsShortCircuitsOnFirstMatch(t *testing.T) {
   702  	errC := errConfigurationInvalid{&accessTrackingError{}, &accessTrackingError{}}
   703  	_ = errors.Is(errC, &accessTrackingError{})
   704  
   705  	var numAccessed int
   706  	for _, err := range errC {
   707  		if ate := err.(*accessTrackingError); ate.wasAccessed {
   708  			numAccessed++
   709  		}
   710  	}
   711  	if numAccessed != 1 {
   712  		t.Errorf("expected exactly one error to get accessed, got %d", numAccessed)
   713  	}
   714  }