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