knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/apis/field_error_test.go (about)

     1  /*
     2  Copyright 2017 The Knative 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 apis
    18  
    19  import (
    20  	"strconv"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/google/go-cmp/cmp/cmpopts"
    27  )
    28  
    29  type testStruct struct {
    30  	Name string `json:"name"`
    31  }
    32  
    33  type unexported struct {
    34  	unexportedField int
    35  }
    36  
    37  func TestFieldError(t *testing.T) {
    38  	tests := []struct {
    39  		name     string
    40  		err      *FieldError
    41  		prefixes [][]string
    42  		want     string
    43  	}{{
    44  		name: "simple single no propagation",
    45  		err: &FieldError{
    46  			Message: "hear me roar",
    47  			Paths:   []string{"foo.bar"},
    48  		},
    49  		want: "hear me roar: foo.bar",
    50  	}, {
    51  		name: "simple single propagation",
    52  		err: &FieldError{
    53  			Message: `invalid value "blah"`,
    54  			Paths:   []string{"foo"},
    55  		},
    56  		prefixes: [][]string{{"bar"}, {"baz", "ugh"}, {"hoola"}},
    57  		want:     `invalid value "blah": hoola.baz.ugh.bar.foo`,
    58  	}, {
    59  		name: "simple multiple propagation",
    60  		err: &FieldError{
    61  			Message: "invalid field(s)",
    62  			Paths:   []string{"foo", "bar"},
    63  		},
    64  		prefixes: [][]string{{"baz", "ugh"}},
    65  		want:     "invalid field(s): baz.ugh.bar, baz.ugh.foo",
    66  	}, {
    67  		name: "multiple propagation with details",
    68  		err: &FieldError{
    69  			Message: "invalid field(s)",
    70  			Paths:   []string{"foo", "bar"},
    71  			Details: `I am a long
    72  long
    73  loooong
    74  Body.`,
    75  		},
    76  		prefixes: [][]string{{"baz", "ugh"}},
    77  		want: `invalid field(s): baz.ugh.bar, baz.ugh.foo
    78  I am a long
    79  long
    80  loooong
    81  Body.`,
    82  	}, {
    83  		name: "single propagation, empty start",
    84  		err: &FieldError{
    85  			Message: "invalid field(s)",
    86  			// We might see this validating a scalar leaf.
    87  			Paths: []string{CurrentField},
    88  		},
    89  		prefixes: [][]string{{"baz", "ugh"}},
    90  		want:     "invalid field(s): baz.ugh",
    91  	}, {
    92  		name: "single propagation, no paths",
    93  		err: &FieldError{
    94  			Message: "invalid field(s)",
    95  			Paths:   nil,
    96  		},
    97  		prefixes: [][]string{{"baz", "ugh"}},
    98  		want:     "invalid field(s): ",
    99  	}, {
   100  		name:     "nil propagation",
   101  		err:      nil,
   102  		prefixes: [][]string{{"baz", "ugh"}},
   103  	}, {
   104  		name:     "missing field propagation",
   105  		err:      ErrMissingField("foo", "bar"),
   106  		prefixes: [][]string{{"baz"}},
   107  		want:     "missing field(s): baz.bar, baz.foo",
   108  	}, {
   109  		name: "check disallowed - none found",
   110  		err: CheckDisallowedFields(
   111  			testStruct{
   112  				Name: "foo",
   113  			},
   114  			testStruct{
   115  				Name: "foo",
   116  			}),
   117  		prefixes: [][]string{{"baz"}},
   118  		want:     "",
   119  	}, {
   120  		name: "check disallowed internal error",
   121  		err: CheckDisallowedFields(
   122  			unexported{
   123  				unexportedField: 2,
   124  			},
   125  			unexported{
   126  				unexportedField: 0,
   127  			}),
   128  		prefixes: [][]string{{"baz"}},
   129  		want:     "Internal Error: baz",
   130  	}, {
   131  		name: "check disallowed propagation",
   132  		err: CheckDisallowedFields(
   133  			testStruct{
   134  				Name: "foo",
   135  			},
   136  			testStruct{
   137  				Name: "",
   138  			}),
   139  		prefixes: [][]string{{"baz"}},
   140  		want:     "must not set the field(s): baz.name",
   141  	}, {
   142  		name:     "missing disallowed propagation",
   143  		err:      ErrDisallowedFields("foo", "bar"),
   144  		prefixes: [][]string{{"baz"}},
   145  		want:     "must not set the field(s): baz.bar, baz.foo",
   146  	}, {
   147  		name:     "invalid value propagation",
   148  		err:      ErrInvalidValue("foo", "bar"),
   149  		prefixes: [][]string{{"baz"}},
   150  		want:     `invalid value: foo: baz.bar`,
   151  	}, {
   152  		name:     "invalid value propagation (int)",
   153  		err:      ErrInvalidValue(5, "bar"),
   154  		prefixes: [][]string{{"baz"}},
   155  		want:     `invalid value: 5: baz.bar`,
   156  	}, {
   157  		name:     "invalid value propagation (duration)",
   158  		err:      ErrInvalidValue(5*time.Second, "bar"),
   159  		prefixes: [][]string{{"baz"}},
   160  		want:     `invalid value: 5s: baz.bar`,
   161  	}, {
   162  		name:     "invalid value details propagation",
   163  		err:      ErrInvalidValue("foo", "bar", "qux"),
   164  		prefixes: [][]string{{"baz"}},
   165  		want:     "invalid value: foo: baz.bar\nqux",
   166  	}, {
   167  		name:     "invalid value propagation",
   168  		err:      ErrGeneric("this is a generic error", "foo", "bar"),
   169  		prefixes: [][]string{{"baz"}},
   170  		want:     `this is a generic error: baz.bar, baz.foo`,
   171  	}, {
   172  		name:     "missing mutually exclusive fields",
   173  		err:      ErrMissingOneOf("foo", "bar"),
   174  		prefixes: [][]string{{"baz"}},
   175  		want:     `expected exactly one, got neither: baz.bar, baz.foo`,
   176  	}, {
   177  		name:     "multiple mutually exclusive fields",
   178  		err:      ErrMultipleOneOf("foo", "bar"),
   179  		prefixes: [][]string{{"baz"}},
   180  		want:     `expected exactly one, got both: baz.bar, baz.foo`,
   181  	}, {
   182  		name: "invalid key name",
   183  		err: ErrInvalidKeyName("b@r", "foo[0].name",
   184  			"can not use @", "do not try"),
   185  		prefixes: [][]string{{"baz"}},
   186  		want: `invalid key name "b@r": baz.foo[0].name
   187  can not use @, do not try`,
   188  	}, {
   189  		name: "invalid key name with details array",
   190  		err: ErrInvalidKeyName("b@r", "foo[0].name",
   191  			[]string{"can not use @", "do not try"}...),
   192  		prefixes: [][]string{{"baz"}},
   193  		want: `invalid key name "b@r": baz.foo[0].name
   194  can not use @, do not try`,
   195  	}, {
   196  		name:     "disallowed update deprecated fields",
   197  		err:      ErrDisallowedUpdateDeprecatedFields("foo", "bar"),
   198  		prefixes: [][]string{{"baz"}},
   199  		want:     `must not update deprecated field(s): baz.bar, baz.foo`,
   200  	}, {
   201  		name: "very complex to simple",
   202  		err: func() *FieldError {
   203  			fe := &FieldError{
   204  				Message: "First",
   205  				Paths:   []string{"A", "B", "C"},
   206  			}
   207  
   208  			fe = fe.Also(fe).Also(fe).Also(fe).Also(fe)
   209  
   210  			fe = fe.Also(&FieldError{
   211  				Message: "Second",
   212  				Paths:   []string{"Z", "X", "Y"},
   213  			})
   214  
   215  			fe = fe.Also(fe).Also(fe).Also(fe).Also(fe)
   216  
   217  			return fe
   218  		}(),
   219  		want: `First: A, B, C
   220  Second: X, Y, Z`,
   221  	}, {
   222  		name: "exponentially grows",
   223  		err: func() *FieldError {
   224  			fe := &FieldError{
   225  				Message: "Top",
   226  				Paths:   []string{"A", "B", "C"},
   227  			}
   228  
   229  			for _, p := range []string{"3", "2", "1"} {
   230  				for range 3 {
   231  					fe = fe.Also(fe)
   232  				}
   233  				fe = fe.ViaField(p)
   234  			}
   235  
   236  			return fe
   237  		}(),
   238  		want: `Top: 1.2.3.A, 1.2.3.B, 1.2.3.C`,
   239  	}, {
   240  		name: "path grows but details are different",
   241  		err: func() *FieldError {
   242  			fe := &FieldError{
   243  				Message: "Top",
   244  				Paths:   []string{"A", "B", "C"},
   245  			}
   246  
   247  			for _, p := range []string{"3", "2", "1"} {
   248  				e := fe.ViaField(p)
   249  				e.Details = "here at " + p
   250  				for range 3 {
   251  					fe = fe.Also(e)
   252  				}
   253  			}
   254  
   255  			return fe
   256  		}(),
   257  		want: `Top: A, B, C
   258  Top: 1.A, 1.B, 1.C
   259  here at 1
   260  Top: 1.2.A, 1.2.B, 1.2.C, 2.A, 2.B, 2.C
   261  here at 2
   262  Top: 1.2.3.A, 1.2.3.B, 1.2.3.C, 1.3.A, 1.3.B, 1.3.C, 2.3.A, 2.3.B, 2.3.C, 3.A, 3.B, 3.C
   263  here at 3`,
   264  	}, {
   265  		name: "very complex to complex",
   266  		err: func() *FieldError {
   267  			fe := &FieldError{
   268  				Message: "First",
   269  				Paths:   []string{"A", "B", "C"},
   270  			}
   271  
   272  			fe = fe.ViaField("one").Also(fe).ViaField("two").Also(fe).ViaField("three").Also(fe)
   273  
   274  			fe = fe.Also(&FieldError{
   275  				Message: "Second",
   276  				Paths:   []string{"Z", "X", "Y"},
   277  			})
   278  
   279  			return fe
   280  		}(),
   281  		want: `First: A, B, C, three.A, three.B, three.C, three.two.A, three.two.B, three.two.C, three.two.one.A, three.two.one.B, three.two.one.C
   282  Second: X, Y, Z`,
   283  	}, {
   284  		name:     "out of bound value",
   285  		err:      ErrOutOfBoundsValue("a", "b", "c", "string"),
   286  		prefixes: [][]string{{"spec"}},
   287  		want:     `expected b <= a <= c: spec.string`,
   288  	}, {
   289  		name:     "out of bound value (int)",
   290  		err:      ErrOutOfBoundsValue(-1, 0, 5, "timeout"),
   291  		prefixes: [][]string{{"spec"}},
   292  		want:     `expected 0 <= -1 <= 5: spec.timeout`,
   293  	}, {
   294  		name:     "out of bound value (time.Duration)",
   295  		err:      ErrOutOfBoundsValue(1*time.Second, 2*time.Second, 5*time.Second, "timeout"),
   296  		prefixes: [][]string{{"spec"}},
   297  		want:     `expected 2s <= 1s <= 5s: spec.timeout`,
   298  	}, {
   299  		name: "Explicit Error",
   300  		err:  ErrMissingField("foo", "bar").At(ErrorLevel),
   301  		want: `missing field(s): bar, foo`,
   302  	}, {
   303  		name: "Warning",
   304  		err:  ErrMissingField("foo", "bar").At(WarningLevel),
   305  		want: `missing field(s): bar, foo`,
   306  	}, {
   307  		name: "Explicit Error isn't filtered",
   308  		err:  ErrMissingField("foo", "bar").At(ErrorLevel).Filter(ErrorLevel),
   309  		want: `missing field(s): bar, foo`,
   310  	}, {
   311  		name: "Implicit Error isn't filtered",
   312  		err:  ErrMissingField("foo", "bar").Filter(ErrorLevel),
   313  		want: `missing field(s): bar, foo`,
   314  	}, {
   315  		name: "Warning isn't filtered",
   316  		err:  ErrMissingField("foo", "bar").At(WarningLevel).Filter(WarningLevel),
   317  		want: `missing field(s): bar, foo`,
   318  	}, {
   319  		name: "Explicit Error is filtered",
   320  		err:  ErrMissingField("foo", "bar").At(ErrorLevel).Filter(WarningLevel),
   321  	}, {
   322  		name: "Warning is filtered",
   323  		err:  ErrMissingField("foo", "bar").At(WarningLevel).Filter(ErrorLevel),
   324  	}, {
   325  		name: "Mix of Errors and Warnings",
   326  		err:  ErrMissingField("foo").Also(ErrMissingField("bar").At(WarningLevel)),
   327  		want: `missing field(s): foo
   328  missing field(s): bar`,
   329  	}, {
   330  		name: "Mix of Errors and Warnings - Filter Warning",
   331  		err:  ErrMissingField("foo").Also(ErrMissingField("bar").At(WarningLevel)).Filter(WarningLevel),
   332  		want: `missing field(s): bar`,
   333  	}, {
   334  		name: "Mix of Errors and Warnings - Filter Error",
   335  		err:  ErrMissingField("foo").Also(ErrMissingField("bar").At(WarningLevel)).Filter(ErrorLevel),
   336  		want: `missing field(s): foo`,
   337  	}, {
   338  		name: "Mix of Errors and Warnings turned to Warnings",
   339  		err:  ErrMissingField("foo").Also(ErrMissingField("bar").At(WarningLevel)).At(WarningLevel).Filter(WarningLevel),
   340  		want: `missing field(s): bar, foo`,
   341  	}, {
   342  		name: "Mix of Errors and Warnings turned to Errors",
   343  		err:  ErrMissingField("foo").Also(ErrMissingField("bar").At(WarningLevel)).At(ErrorLevel).Filter(ErrorLevel),
   344  		want: `missing field(s): bar, foo`,
   345  	}}
   346  
   347  	for _, test := range tests {
   348  		t.Run(test.name, func(t *testing.T) {
   349  			fe := test.err
   350  			// Simulate propagation up a call stack.
   351  			for _, prefix := range test.prefixes {
   352  				fe = fe.ViaField(prefix...)
   353  			}
   354  			if test.want != "" {
   355  				if got, want := fe.Error(), test.want; got != want {
   356  					t.Errorf("%s: Error() = %v, wanted %v", test.name, got, want)
   357  				}
   358  			} else if fe != nil {
   359  				t.Errorf("%s: ViaField() = %v, wanted nil", test.name, fe)
   360  			}
   361  		})
   362  	}
   363  }
   364  
   365  func TestWrappedErrors(t *testing.T) {
   366  	for _, test := range []struct {
   367  		name string
   368  		err  *FieldError
   369  		want []*FieldError
   370  	}{{
   371  		name: "nil",
   372  		err:  nil,
   373  		want: nil,
   374  	}, {
   375  		name: "empty",
   376  		err:  &FieldError{},
   377  		want: nil,
   378  	}, {
   379  		name: "has errors",
   380  		err: (&FieldError{}).Also(
   381  			ErrInvalidValue("42", "answer"),
   382  			ErrGeneric("generic", "error"),
   383  		),
   384  		want: []*FieldError{
   385  			ErrGeneric("generic", "error"),
   386  			ErrInvalidValue("42", "answer"),
   387  		},
   388  	}} {
   389  		t.Run(test.name, func(t *testing.T) {
   390  			if diff := cmp.Diff(test.want, test.err.WrappedErrors(), cmp.AllowUnexported(FieldError{}), cmpopts.EquateEmpty()); diff != "" {
   391  				t.Errorf("unexpected GetErrors() -want,+got: %s", diff)
   392  			}
   393  		})
   394  	}
   395  }
   396  
   397  func TestViaIndexOrKeyFieldError(t *testing.T) {
   398  	tests := []struct {
   399  		name     string
   400  		err      *FieldError
   401  		prefixes [][]string
   402  		want     string
   403  	}{{
   404  		name: "nil",
   405  		err:  nil,
   406  		want: "",
   407  	}, {
   408  		name:     "nil with prefix",
   409  		err:      nil,
   410  		prefixes: [][]string{{"INDEX:2"}, {"KEY:B"}, {"FIELDINDEX:6,AAA"}, {"FIELDKEY:bee,AAA"}},
   411  		want:     "",
   412  	}, {
   413  		name: "simple single no propagation",
   414  		err: &FieldError{
   415  			Message: "hear me roar",
   416  			Paths:   []string{"bar"},
   417  		},
   418  		prefixes: [][]string{{"INDEX:3", "INDEX:2", "INDEX:1", "foo"}},
   419  		want:     "hear me roar: foo[1][2][3].bar",
   420  	}, {
   421  		name: "simple key",
   422  		err: &FieldError{
   423  			Message: "hear me roar",
   424  			Paths:   []string{"bar"},
   425  		},
   426  		prefixes: [][]string{{"KEY:C", "KEY:B", "KEY:A", "foo"}},
   427  		want:     "hear me roar: foo[A][B][C].bar",
   428  	}, {
   429  		name:     "missing field propagation",
   430  		err:      ErrMissingField("foo", "bar"),
   431  		prefixes: [][]string{{"[2]", "baz"}},
   432  		want:     "missing field(s): baz[2].bar, baz[2].foo",
   433  	}, {
   434  		name: "invalid key name",
   435  		err: ErrInvalidKeyName("b@r", "name",
   436  			"can not use @", "do not try"),
   437  		prefixes: [][]string{{"baz", "INDEX:0", "foo"}},
   438  		want: `invalid key name "b@r": foo[0].baz.name
   439  can not use @, do not try`,
   440  	}, {
   441  		name: "invalid key name with keys",
   442  		err: ErrInvalidKeyName("b@r", "name",
   443  			"can not use @", "do not try"),
   444  		prefixes: [][]string{{"baz", "INDEX:0", "foo"}, {"bar", "KEY:A", "boo"}},
   445  		want: `invalid key name "b@r": boo[A].bar.foo[0].baz.name
   446  can not use @, do not try`,
   447  	}, {
   448  		name: "multi prefixes provided",
   449  		err: &FieldError{
   450  			Message: "invalid field(s)",
   451  			Paths:   []string{"foo"},
   452  		},
   453  		prefixes: [][]string{{"INDEX:2"}, {"bee"}, {"INDEX:0"}, {"baa", "baz", "ugh"}},
   454  		want:     "invalid field(s): ugh.baz.baa[0].bee[2].foo",
   455  	}, {
   456  		name: "use helper viaFieldIndex",
   457  		err: &FieldError{
   458  			Message: "invalid field(s)",
   459  			Paths:   []string{"foo"},
   460  		},
   461  		prefixes: [][]string{{"FIELDINDEX:bee,2"}, {"FIELDINDEX:baa,0"}, {"baz", "ugh"}},
   462  		want:     "invalid field(s): ugh.baz.baa[0].bee[2].foo",
   463  	}, {
   464  		name: "use helper viaFieldKey",
   465  		err: &FieldError{
   466  			Message: "invalid field(s)",
   467  			Paths:   []string{"foo"},
   468  		},
   469  		prefixes: [][]string{{"FIELDKEY:bee,AAA"}, {"FIELDKEY:baa,BBB"}, {"baz", "ugh"}},
   470  		want:     "invalid field(s): ugh.baz.baa[BBB].bee[AAA].foo",
   471  	}, {
   472  		name: "bypass helpers",
   473  		err: &FieldError{
   474  			Message: "invalid field(s)",
   475  			Paths:   []string{"foo"},
   476  		},
   477  		prefixes: [][]string{{"[2]"}, {"[1]"}, {"bar"}},
   478  		want:     "invalid field(s): bar[1][2].foo",
   479  	}, {
   480  		name: "multi paths provided",
   481  		err: &FieldError{
   482  			Message: "invalid field(s)",
   483  			Paths:   []string{"foo", "bar"},
   484  		},
   485  		prefixes: [][]string{{"INDEX:0"}, {"index"}, {"KEY:A"}, {"map"}},
   486  		want:     "invalid field(s): map[A].index[0].bar, map[A].index[0].foo",
   487  	}, {
   488  		name: "manual index",
   489  		err: func() *FieldError {
   490  			// Example, return an error in a loop:
   491  			// for i, item := spec.myList {
   492  			//   err := item.validate().ViaIndex(i).ViaField("myList")
   493  			//   if err != nil {
   494  			// 		return err
   495  			//   }
   496  			// }
   497  			// --> I expect path to be myList[i].foo
   498  
   499  			err := &FieldError{
   500  				Message: "invalid field(s)",
   501  				Paths:   []string{"foo"},
   502  			}
   503  
   504  			err = err.ViaIndex(0).ViaField("bar")
   505  			err = err.ViaIndex(2).ViaIndex(1).ViaField("baz")
   506  			err = err.ViaIndex(3).ViaIndex(4).ViaField("boof")
   507  			return err
   508  		}(),
   509  		want: "invalid field(s): boof[4][3].baz[1][2].bar[0].foo",
   510  	}, {
   511  		name: "manual multiple index",
   512  		err: func() *FieldError {
   513  			err := &FieldError{
   514  				Message: "invalid field(s)",
   515  				Paths:   []string{"foo"},
   516  			}
   517  
   518  			err = err.ViaField("bear", "[1]", "[2]", "[3]", "baz", "]xxx[").ViaField("bar")
   519  			return err
   520  		}(),
   521  		want: "invalid field(s): bar.bear[1][2][3].baz.]xxx[.foo",
   522  	}, {
   523  		name: "manual keys",
   524  		err: func() *FieldError {
   525  			err := &FieldError{
   526  				Message: "invalid field(s)",
   527  				Paths:   []string{"foo"},
   528  			}
   529  
   530  			err = err.ViaKey("A").ViaField("bar")
   531  			err = err.ViaKey("CCC").ViaKey("BB").ViaField("baz")
   532  			err = err.ViaKey("E").ViaKey("F").ViaField("jar")
   533  			return err
   534  		}(),
   535  		want: "invalid field(s): jar[F][E].baz[BB][CCC].bar[A].foo",
   536  	}, {
   537  		name: "manual index and keys",
   538  		err: func() *FieldError {
   539  			err := &FieldError{
   540  				Message: "invalid field(s)",
   541  				Paths:   []string{"foo", "faa"},
   542  			}
   543  			err = err.ViaKey("A").ViaField("bar")
   544  			err = err.ViaIndex(1).ViaField("baz")
   545  			err = err.ViaKey("E").ViaIndex(0).ViaField("jar")
   546  			return err
   547  		}(),
   548  		want: "invalid field(s): jar[0][E].baz[1].bar[A].faa, jar[0][E].baz[1].bar[A].foo",
   549  	}, {
   550  		name: "leaf field error with index",
   551  		err: func() *FieldError {
   552  			return ErrInvalidArrayValue("kapot", "indexed", 5)
   553  		}(),
   554  		want: `invalid value: kapot: indexed[5]`,
   555  	}, {
   556  		name: "leaf field error with index (int)",
   557  		err: func() *FieldError {
   558  			return ErrInvalidArrayValue(42, "indexed", 5)
   559  		}(),
   560  		want: `invalid value: 42: indexed[5]`,
   561  	}, {
   562  		name:     "nil propagation",
   563  		err:      nil,
   564  		prefixes: [][]string{{"baz", "ugh", "INDEX:0", "KEY:A"}},
   565  	}}
   566  
   567  	for _, test := range tests {
   568  		t.Run(test.name, func(t *testing.T) {
   569  			fe := test.err
   570  			// Simulate propagation up a call stack.
   571  			for _, prefix := range test.prefixes {
   572  				for _, p := range prefix {
   573  					if strings.HasPrefix(p, "INDEX") {
   574  						index := strings.Split(p, ":")
   575  						fe = fe.ViaIndex(makeIndex(index[1]))
   576  					} else if strings.HasPrefix(p, "FIELDINDEX") {
   577  						index := strings.Split(p, ":")
   578  						fe = fe.ViaFieldIndex(makeFieldIndex(index[1]))
   579  					} else if strings.HasPrefix(p, "KEY") {
   580  						key := strings.Split(p, ":")
   581  						fe = fe.ViaKey(makeKey(key[1]))
   582  					} else if strings.HasPrefix(p, "FIELDKEY") {
   583  						index := strings.Split(p, ":")
   584  						fe = fe.ViaFieldKey(makeFieldKey(index[1]))
   585  					} else {
   586  						fe = fe.ViaField(p)
   587  					}
   588  				}
   589  			}
   590  
   591  			if test.want != "" {
   592  				if got, want := fe.Error(), test.want; got != want {
   593  					t.Errorf("%s: Error() = %q, wanted %q, diff: %s", test.name, got, want, cmp.Diff(got, want))
   594  				}
   595  			} else if fe != nil {
   596  				t.Errorf("%s: ViaField() = %v, wanted nil", test.name, fe)
   597  			}
   598  		})
   599  	}
   600  }
   601  
   602  func TestNilError(t *testing.T) {
   603  	var err *FieldError
   604  	if got, want := err.Error(), ""; got != want {
   605  		t.Errorf("got %v, wanted %v", got, want)
   606  	}
   607  }
   608  
   609  func TestAlso(t *testing.T) {
   610  	tests := []struct {
   611  		name     string
   612  		err      *FieldError
   613  		also     []FieldError
   614  		prefixes [][]string
   615  		want     string
   616  	}{{
   617  		name: "nil",
   618  		err:  nil,
   619  		also: []FieldError{{
   620  			Message: "also this",
   621  			Paths:   []string{"woo"},
   622  		}},
   623  		prefixes: [][]string{{"foo"}},
   624  		want:     "also this: foo.woo",
   625  	}, {
   626  		name: "nil all the way",
   627  		err:  nil,
   628  		also: []FieldError{{}},
   629  		want: "",
   630  	}, {
   631  		name: "simple",
   632  		err: &FieldError{
   633  			Message: "hear me roar",
   634  			Paths:   []string{"bar"},
   635  		},
   636  		also: []FieldError{{
   637  			Message: "also this",
   638  			Paths:   []string{"woo"},
   639  		}},
   640  		prefixes: [][]string{{"foo", "[A]", "[B]", "[C]"}},
   641  		want: `also this: foo[A][B][C].woo
   642  hear me roar: foo[A][B][C].bar`,
   643  	}, {
   644  		name: "lots of also",
   645  		err: &FieldError{
   646  			Message: "knock knock",
   647  			Paths:   []string{"foo"},
   648  		},
   649  		also: []FieldError{{
   650  			Message: "also this",
   651  			Paths:   []string{"A"},
   652  		}, {
   653  			Message: "and this",
   654  			Paths:   []string{"B"},
   655  		}, {
   656  			Message: "not without this",
   657  			Paths:   []string{"C"},
   658  		}},
   659  		prefixes: [][]string{{"bar"}},
   660  		want: `also this: bar.A
   661  and this: bar.B
   662  knock knock: bar.foo
   663  not without this: bar.C`,
   664  	}}
   665  
   666  	for _, test := range tests {
   667  		t.Run(test.name, func(t *testing.T) {
   668  			fe := test.err
   669  
   670  			for _, err := range test.also {
   671  				fe = fe.Also(&err)
   672  			}
   673  
   674  			// Simulate propagation up a call stack.
   675  			for _, prefix := range test.prefixes {
   676  				fe = fe.ViaField(prefix...)
   677  			}
   678  
   679  			if test.want != "" {
   680  				if got, want := fe.Error(), test.want; got != want {
   681  					t.Errorf("\nGot:  %v,\nwant: %v diff(-want,+got)\n%s", got, want, cmp.Diff(want, got))
   682  				}
   683  			} else if fe != nil {
   684  				t.Errorf("ViaField() = %v, wanted nil", fe)
   685  			}
   686  		})
   687  	}
   688  }
   689  
   690  func TestAlsoNil(t *testing.T) {
   691  	errs := &FieldError{
   692  		Message: "original",
   693  		Paths:   []string{"foo"},
   694  	}
   695  	errs = errs.Also(nil)
   696  
   697  	if got, want := errs.Error(), "original: foo"; got != want {
   698  		t.Errorf("TestAlsoNil: Also(nil).Error() = %v, wanted %v", got, want)
   699  	}
   700  
   701  	errs = errs.Also([]*FieldError{nil}...)
   702  	if got, want := errs.Error(), "original: foo"; got != want {
   703  		t.Errorf("TestAlsoNil: Also(nil).Error() = %v, wanted %v", got, want)
   704  	}
   705  }
   706  
   707  func TestMergeFieldErrors(t *testing.T) {
   708  	tests := []struct {
   709  		name     string
   710  		err      *FieldError
   711  		also     []FieldError
   712  		prefixes [][]string
   713  		want     string
   714  	}{{
   715  		name: "simple",
   716  		err: &FieldError{
   717  			Message: "A simple error message",
   718  			Paths:   []string{"bar"},
   719  		},
   720  		also: []FieldError{{
   721  			Message: "A simple error message",
   722  			Paths:   []string{"foo"},
   723  		}},
   724  		want: `A simple error message: bar, foo`,
   725  	}, {
   726  		name: "conflict",
   727  		err: &FieldError{
   728  			Message: "A simple error message",
   729  			Paths:   []string{"bar", "foo"},
   730  		},
   731  		also: []FieldError{{
   732  			Message: "A simple error message",
   733  			Paths:   []string{"foo"},
   734  		}},
   735  		want: `A simple error message: bar, foo`,
   736  	}, {
   737  		name: "lots of also",
   738  		err: (&FieldError{
   739  			Message: "this error",
   740  			Paths:   []string{"bar", "foo"},
   741  		}).Also(&FieldError{
   742  			Message: "another",
   743  			Paths:   []string{"right", "left"},
   744  		}).ViaField("head"),
   745  		also: []FieldError{{
   746  			Message: "An alpha error message",
   747  			Paths:   []string{"A"},
   748  		}, {
   749  			Message: "An alpha error message",
   750  			Paths:   []string{"B"},
   751  		}, {
   752  			Message: "An alpha error message",
   753  			Paths:   []string{"B"},
   754  		}, {
   755  			Message: "An alpha error message",
   756  			Paths:   []string{"B"},
   757  		}, {
   758  			Message: "An alpha error message",
   759  			Paths:   []string{"B"},
   760  		}, {
   761  			Message: "An alpha error message",
   762  			Paths:   []string{"B"},
   763  		}, {
   764  			Message: "An alpha error message",
   765  			Paths:   []string{"B"},
   766  		}, {
   767  			Message: "An alpha error message",
   768  			Paths:   []string{"C"},
   769  		}, {
   770  			Message: "An alpha error message",
   771  			Paths:   []string{"D"},
   772  		}, {
   773  			Message: "this error",
   774  			Paths:   []string{"foo"},
   775  			Details: "devil is in the details",
   776  		}, {
   777  			Message: "this error",
   778  			Paths:   []string{"foo"},
   779  			Details: "more details",
   780  		}},
   781  		prefixes: [][]string{{"this"}},
   782  		want: `An alpha error message: this.A, this.B, this.C, this.D
   783  another: this.head.left, this.head.right
   784  this error: this.head.bar, this.head.foo
   785  this error: this.foo
   786  devil is in the details
   787  this error: this.foo
   788  more details`,
   789  	}}
   790  	for _, test := range tests {
   791  		t.Run(test.name, func(t *testing.T) {
   792  			fe := test.err
   793  			for _, err := range test.also {
   794  				fe = fe.Also(&err)
   795  			}
   796  			// Simulate propagation up a call stack.
   797  			for _, prefix := range test.prefixes {
   798  				fe = fe.ViaField(prefix...)
   799  			}
   800  			if test.want != "" {
   801  				got := fe.Error()
   802  				if got != test.want {
   803  					t.Errorf("%s: Error() = %v, wanted %v", test.name, got, test.want)
   804  				}
   805  			} else if fe != nil {
   806  				t.Errorf("%s: ViaField() = %v, wanted nil", test.name, fe)
   807  			}
   808  		})
   809  	}
   810  }
   811  
   812  func TestAlsoStaysNil(t *testing.T) {
   813  	var err *FieldError
   814  
   815  	err = err.Also(nil)
   816  	if err != nil {
   817  		t.Errorf("Got: %v, want nil", err)
   818  	}
   819  
   820  	err = err.ViaField("nil").Also(nil)
   821  	if err != nil {
   822  		t.Errorf("Got: %v, want nil", err)
   823  	}
   824  
   825  	err = err.Also(&FieldError{})
   826  	if err != nil {
   827  		t.Errorf("expected nil, got %v, wanted nil", err)
   828  	}
   829  }
   830  
   831  func TestAlsoMultiple(t *testing.T) {
   832  	var err *FieldError
   833  
   834  	errs := []*FieldError{{
   835  		Message: "1",
   836  		Paths:   []string{"bar"},
   837  	}, {
   838  		Message: "2",
   839  		Paths:   []string{"baz"},
   840  	}}
   841  	err = err.Also(errs...)
   842  	const want = "1: bar\n2: baz"
   843  	if got := err.Error(); got != want {
   844  		t.Errorf("Got = %q, want: %q", got, want)
   845  	}
   846  
   847  	// This shows the order of Also calls does not matter.
   848  	err = nil
   849  	err = err.Also(errs[0]).Also(errs[1])
   850  	if got := err.Error(); got != want {
   851  		t.Errorf("Got = %q, want: %q", got, want)
   852  	}
   853  }
   854  
   855  func TestFlatten(t *testing.T) {
   856  	tests := []struct {
   857  		name    string
   858  		indices []string
   859  		want    string
   860  	}{{
   861  		name:    "simple",
   862  		indices: strings.Split("foo.[1]", "."),
   863  		want:    "foo[1]",
   864  	}, {
   865  		name:    "no brackets",
   866  		indices: strings.Split("foo.bar", "."),
   867  		want:    "foo.bar",
   868  	}, {
   869  		name:    "err([0]).ViaField(bar).ViaField(foo)",
   870  		indices: strings.Split("foo.bar.[0]", "."),
   871  		want:    "foo.bar[0]",
   872  	}, {
   873  		name:    "err(bar).ViaIndex(0).ViaField(foo)",
   874  		indices: strings.Split("foo.[0].bar", "."),
   875  		want:    "foo[0].bar",
   876  	}, {
   877  		name:    "err(bar).ViaField(foo).ViaIndex(0)",
   878  		indices: strings.Split("[0].foo.bar", "."),
   879  		want:    "[0].foo.bar",
   880  	}, {
   881  		name:    "err(bar).ViaIndex(0).ViaIndex[1].ViaField(foo)",
   882  		indices: strings.Split("foo.[1].[0].bar", "."),
   883  		want:    "foo[1][0].bar",
   884  	}, {
   885  		name:    "err(foo).ViaField(bar).ViaIndex[0].ViaField(baz)",
   886  		indices: []string{"foo", "bar.[0].baz"},
   887  		want:    "foo.bar[0].baz",
   888  	}}
   889  
   890  	for _, test := range tests {
   891  		t.Run(test.name, func(t *testing.T) {
   892  			if got, want := flatten(test.indices), test.want; got != want {
   893  				t.Errorf("got: %q, want %q", got, want)
   894  			}
   895  		})
   896  	}
   897  }
   898  
   899  func makeIndex(index string) int {
   900  	all := strings.Split(index, ",")
   901  	if i, err := strconv.Atoi(all[0]); err == nil {
   902  		return i
   903  	}
   904  	return -1
   905  }
   906  
   907  func makeFieldIndex(fi string) (string, int) {
   908  	all := strings.Split(fi, ",")
   909  	if i, err := strconv.Atoi(all[1]); err == nil {
   910  		return all[0], i
   911  	}
   912  	return "error", -1
   913  }
   914  
   915  func makeKey(key string) string {
   916  	all := strings.Split(key, ",")
   917  	return all[0]
   918  }
   919  
   920  func makeFieldKey(fk string) (string, string) {
   921  	all := strings.Split(fk, ",")
   922  	return all[0], all[1]
   923  }