Test Failed
Push — main ( 9f7a12...05cfc8 )
by Igor
01:08 queued 13s
created

validation_test.TestPropertyPath_String   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
package validation_test
2
3
import (
4
	"encoding/json"
5
	"fmt"
6
	"math"
7
	"regexp"
8
	"strconv"
9
	"strings"
10
	"testing"
11
12
	"github.com/muonsoft/validation"
13
	"github.com/stretchr/testify/assert"
14
	"github.com/stretchr/testify/require"
15
)
16
17
func TestPropertyPath_With(t *testing.T) {
18
	path := validation.NewPropertyPath(validation.PropertyName("top"), validation.ArrayIndex(0))
19
20
	path = path.With(
21
		validation.PropertyName("low"),
22
		validation.ArrayIndex(1),
23
		validation.PropertyName("property"),
24
	)
25
26
	assert.Equal(t, "top[0].low[1].property", path.String())
27
}
28
29
func TestPropertyPath_Elements(t *testing.T) {
30
	want := []validation.PropertyPathElement{
31
		validation.PropertyName("top"),
32
		validation.ArrayIndex(0),
33
		validation.PropertyName("low"),
34
		validation.ArrayIndex(1),
35
		validation.PropertyName("property"),
36
	}
37
38
	got := validation.NewPropertyPath(want...).Elements()
39
40
	assert.Equal(t, want, got)
41
}
42
43
func TestPropertyPath_String(t *testing.T) {
44
	tests := []struct {
45
		path *validation.PropertyPath
46
		want string
47
	}{
48
		{path: nil, want: ""},
49
		{path: validation.NewPropertyPath(), want: ""},
50
		{path: validation.NewPropertyPath(validation.PropertyName(" ")), want: "[' ']"},
51
		{path: validation.NewPropertyPath(validation.PropertyName("$")), want: "$"},
52
		{path: validation.NewPropertyPath(validation.PropertyName("_")), want: "_"},
53
		{path: validation.NewPropertyPath(validation.PropertyName("id$_")), want: "id$_"},
54
		{
55
			path: validation.NewPropertyPath().WithProperty("array").WithIndex(1).WithProperty("property"),
56
			want: "array[1].property",
57
		},
58
		{
59
			path: validation.NewPropertyPath().WithProperty("@foo").WithProperty("bar"),
60
			want: "['@foo'].bar",
61
		},
62
		{
63
			path: validation.NewPropertyPath().WithProperty("@foo").WithIndex(0),
64
			want: "['@foo'][0]",
65
		},
66
		{
67
			path: validation.NewPropertyPath().WithProperty("foo.bar").WithProperty("baz"),
68
			want: "['foo.bar'].baz",
69
		},
70
		{
71
			path: validation.NewPropertyPath().WithProperty("foo.'bar'").WithProperty("baz"),
72
			want: `['foo.\'bar\''].baz`,
73
		},
74
		{
75
			path: validation.NewPropertyPath().WithProperty(`0`).WithProperty("baz"),
76
			want: `['0'].baz`,
77
		},
78
		{
79
			path: validation.NewPropertyPath().WithProperty(`foo[0]`).WithProperty("baz"),
80
			want: `['foo[0]'].baz`,
81
		},
82
		{
83
			path: validation.NewPropertyPath().WithProperty(``).WithProperty("baz"),
84
			want: `[''].baz`,
85
		},
86
		{
87
			path: validation.NewPropertyPath().WithProperty("foo").WithProperty(""),
88
			want: `foo['']`,
89
		},
90
		{
91
			path: validation.NewPropertyPath().WithProperty(`'`).WithProperty("baz"),
92
			want: `['\''].baz`,
93
		},
94
		{
95
			path: validation.NewPropertyPath().WithProperty(`\`).WithProperty("baz"),
96
			want: `['\\'].baz`,
97
		},
98
		{
99
			path: validation.NewPropertyPath().WithProperty(`\'foo`).WithProperty("baz"),
100
			want: `['\\\'foo'].baz`,
101
		},
102
		{
103
			path: validation.NewPropertyPath().WithProperty(`фу`).WithProperty("baz"),
104
			want: `фу.baz`,
105
		},
106
	}
107
	for _, test := range tests {
108
		t.Run(test.want, func(t *testing.T) {
109
			got := test.path.String()
110
111
			assert.Equal(t, test.want, got)
112
		})
113
	}
114
}
115
116
func TestPropertyPath_UnmarshalText(t *testing.T) {
117
	tests := []struct {
118
		pathString string
119
		want       []validation.PropertyPathElement
120
		wantError  string
121
	}{
122
		{pathString: "[", wantError: "parsing path element #0: incomplete array index"},
123
		{pathString: "]", wantError: "parsing path element #0 at char #0 ']': unexpected close bracket"},
124
		{pathString: "[]", wantError: "parsing path element #0 at char #1 ']': unexpected close bracket"},
125
		{pathString: "'", wantError: `parsing path element #0 at char #0 '\'': unexpected quote`},
126
		{pathString: ".", wantError: "parsing path element #0 at char #0 '.': unexpected point"},
127
		{pathString: "\\", wantError: `parsing path element #0 at char #0 '\\': unexpected backslash`},
128
		{pathString: "[[", wantError: "parsing path element #0 at char #1 '[': unexpected char"},
129
		{pathString: "0", wantError: "parsing path element #0 at char #0 '0': unexpected identifier character"},
130
		{pathString: "[0", wantError: "parsing path element #0: incomplete array index"},
131
		{pathString: "[0[", wantError: "parsing path element #0 at char #2 '[': unexpected char"},
132
		{pathString: "[0[]", wantError: "parsing path element #0 at char #2 '[': unexpected char"},
133
		{pathString: "[[0]", wantError: "parsing path element #0 at char #1 '[': unexpected char"},
134
		{pathString: "['property", wantError: "parsing path element #0: incomplete bracketed property name"},
135
		{pathString: "['property'", wantError: "parsing path element #0: incomplete bracketed property name"},
136
		{pathString: "['property]", wantError: "parsing path element #0: incomplete bracketed property name"},
137
		{pathString: "['property'][", wantError: "parsing path element #1: incomplete array index"},
138
		{pathString: "[0][1][invalid]", wantError: "parsing path element #2 at char #7 'i': unexpected array index character"},
139
		{pathString: "[0][1][0invalid]", wantError: "parsing path element #2 at char #8 'i': unexpected array index character"},
140
		{pathString: "[0][1][012345678901234567890123456789]", wantError: "parsing path element #2: value out of range: 012345678901234567890123456789"},
141
		{pathString: "[9227000000000000000]", wantError: "parsing path element #0: value out of range: 9227000000000000000"},
142
		{pathString: " ", wantError: "parsing path element #0 at char #0 ' ': unexpected identifier char"},
143
		{pathString: "A ", wantError: "parsing path element #0 at char #1 ' ': unexpected identifier char"},
144
		{pathString: "[0]A", wantError: "parsing path element #1 at char #3 'A': unexpected char"},
145
		{pathString: "A.", wantError: "parsing path element #1: incomplete property name"},
146
		{pathString: "A.[0]", wantError: "parsing path element #1 at char #2 '[': unexpected char"},
147
		{pathString: "[''A]", wantError: "parsing path element #0 at char #3 'A': unexpected char"},
148
		{pathString: "[''[]", wantError: "parsing path element #0 at char #3 '[': unexpected char"},
149
		{pathString: "", want: nil},
150
		{
151
			pathString: "[0]",
152
			want: []validation.PropertyPathElement{
153
				validation.ArrayIndex(0),
154
			},
155
		},
156
		{
157
			pathString: "[' ']",
158
			want: []validation.PropertyPathElement{
159
				validation.PropertyName(" "),
160
			},
161
		},
162
		{
163
			pathString: "[0][1][2][3][4][5][6][7][8][9][10]",
164
			want: []validation.PropertyPathElement{
165
				validation.ArrayIndex(0),
166
				validation.ArrayIndex(1),
167
				validation.ArrayIndex(2),
168
				validation.ArrayIndex(3),
169
				validation.ArrayIndex(4),
170
				validation.ArrayIndex(5),
171
				validation.ArrayIndex(6),
172
				validation.ArrayIndex(7),
173
				validation.ArrayIndex(8),
174
				validation.ArrayIndex(9),
175
				validation.ArrayIndex(10),
176
			},
177
		},
178
		{
179
			pathString: "array[1].property",
180
			want: []validation.PropertyPathElement{
181
				validation.PropertyName("array"),
182
				validation.ArrayIndex(1),
183
				validation.PropertyName("property"),
184
			},
185
		},
186
		{
187
			pathString: "foo1.bar",
188
			want: []validation.PropertyPathElement{
189
				validation.PropertyName("foo1"),
190
				validation.PropertyName("bar"),
191
			},
192
		},
193
		{
194
			pathString: "$foo.bar",
195
			want: []validation.PropertyPathElement{
196
				validation.PropertyName("$foo"),
197
				validation.PropertyName("bar"),
198
			},
199
		},
200
		{
201
			pathString: "_foo.bar",
202
			want: []validation.PropertyPathElement{
203
				validation.PropertyName("_foo"),
204
				validation.PropertyName("bar"),
205
			},
206
		},
207
		{
208
			pathString: "['@foo'].bar",
209
			want: []validation.PropertyPathElement{
210
				validation.PropertyName("@foo"),
211
				validation.PropertyName("bar"),
212
			},
213
		},
214
		{
215
			pathString: "['@foo'][0]",
216
			want: []validation.PropertyPathElement{
217
				validation.PropertyName("@foo"),
218
				validation.ArrayIndex(0),
219
			},
220
		},
221
		{
222
			pathString: "['foo.bar'].baz",
223
			want: []validation.PropertyPathElement{
224
				validation.PropertyName("foo.bar"),
225
				validation.PropertyName("baz"),
226
			},
227
		},
228
		{
229
			pathString: `['foo.\'bar\''].baz`,
230
			want: []validation.PropertyPathElement{
231
				validation.PropertyName("foo.'bar'"),
232
				validation.PropertyName("baz"),
233
			},
234
		},
235
		{
236
			pathString: `['0'].baz`,
237
			want: []validation.PropertyPathElement{
238
				validation.PropertyName(`0`),
239
				validation.PropertyName("baz"),
240
			},
241
		},
242
		{
243
			pathString: `['foo[0]'].baz`,
244
			want: []validation.PropertyPathElement{
245
				validation.PropertyName(`foo[0]`),
246
				validation.PropertyName("baz"),
247
			},
248
		},
249
		{
250
			pathString: `[''].baz`,
251
			want: []validation.PropertyPathElement{
252
				validation.PropertyName(``),
253
				validation.PropertyName("baz"),
254
			},
255
		},
256
		{
257
			pathString: `foo['']`,
258
			want: []validation.PropertyPathElement{
259
				validation.PropertyName("foo"),
260
				validation.PropertyName(""),
261
			},
262
		},
263
		{
264
			pathString: `['\''].baz`,
265
			want: []validation.PropertyPathElement{
266
				validation.PropertyName(`'`),
267
				validation.PropertyName("baz"),
268
			},
269
		},
270
		{
271
			pathString: `['\\'].baz`,
272
			want: []validation.PropertyPathElement{
273
				validation.PropertyName(`\`),
274
				validation.PropertyName("baz"),
275
			},
276
		},
277
		{
278
			pathString: `['\\\'foo'].baz`,
279
			want: []validation.PropertyPathElement{
280
				validation.PropertyName(`\'foo`),
281
				validation.PropertyName("baz"),
282
			},
283
		},
284
		{
285
			pathString: `фу.baz`,
286
			want: []validation.PropertyPathElement{
287
				validation.PropertyName(`фу`),
288
				validation.PropertyName("baz"),
289
			},
290
		},
291
	}
292
	for _, test := range tests {
293
		t.Run(test.pathString, func(t *testing.T) {
294
			var got validation.PropertyPath
295
			err := got.UnmarshalText([]byte(test.pathString))
296
297
			if test.wantError == "" {
298
				assert.NoError(t, err)
299
				assert.Equal(t, test.want, got.Elements())
300
			} else {
301
				assert.Nil(t, got.Elements())
302
				assert.EqualError(t, err, test.wantError)
303
			}
304
		})
305
	}
306
}
307
308
func TestPropertyPath_UnmarshalJSON(t *testing.T) {
309
	jsonData := `"array[1].property"`
310
311
	var path validation.PropertyPath
312
	err := json.Unmarshal([]byte(jsonData), &path)
313
314
	require.NoError(t, err)
315
	assert.Equal(t,
316
		[]validation.PropertyPathElement{
317
			validation.PropertyName("array"),
318
			validation.ArrayIndex(1),
319
			validation.PropertyName("property"),
320
		},
321
		path.Elements(),
322
	)
323
}
324
325
func BenchmarkPropertyPath_String(b *testing.B) {
326
	// cpu: Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz
327
	// BenchmarkPropertyPath_String
328
	// BenchmarkPropertyPath_String-16    	  225926	      7175 ns/op	    4352 B/op	       5 allocs/op
329
	path := validation.NewPropertyPath(
330
		validation.PropertyName("array"),
331
		validation.ArrayIndex(1234567890),
332
		validation.PropertyName("@foo"),
333
		validation.ArrayIndex(1234567890),
334
		validation.PropertyName("foo.bar"),
335
		validation.PropertyName("foo.'bar'"),
336
		validation.PropertyName(`0123456789`),
337
		validation.PropertyName(`foo[0][1][2][3][4][5][6][7][8][9]`),
338
		validation.PropertyName(``),
339
		validation.PropertyName(`'`),
340
		validation.PropertyName(`\`),
341
		validation.PropertyName(`\'foo`),
342
		validation.PropertyName(`фу`),
343
		validation.PropertyName(strings.Repeat(`@foo.'bar'.[baz]`, 100)),
344
	)
345
346
	b.ReportAllocs()
347
	b.ResetTimer()
348
	for i := 0; i < b.N; i++ {
349
		_ = path.String()
350
	}
351
}
352
353
//nolint:stylecheck
354
func FuzzPropertyPath_UnmarshalText(f *testing.F) {
355
	f.Add("array[1].property")
356
	f.Add(" ")
357
	f.Add(fmt.Sprintf("[%d]", math.MaxInt))
358
	f.Add(fmt.Sprintf("[%d]", uint(math.MaxInt)+1))
359
	f.Add("A0000[0000].A000000") // valid path, but not symmetric
360
	f.Add("['A']")               // valid path, but not symmetric
361
362
	assertion := NewPropertyPathAssertion()
363
364
	f.Fuzz(func(t *testing.T, sourcePath string) {
365
		var path validation.PropertyPath
366
		err := path.UnmarshalText([]byte(sourcePath))
367
		if err != nil {
368
			return
369
		}
370
		assertion.Equal(t, sourcePath, path)
371
	})
372
}
373
374
type PropertyPathAssertion struct {
375
	zerosTrimmer *regexp.Regexp
376
}
377
378
func NewPropertyPathAssertion() *PropertyPathAssertion {
379
	return &PropertyPathAssertion{
380
		zerosTrimmer: regexp.MustCompile(`\[\d+]`),
381
	}
382
}
383
384
func (a *PropertyPathAssertion) Equal(t *testing.T, sourcePath string, parsedPath validation.PropertyPath) {
385
	t.Helper()
386
387
	encodedPathBytes, err := parsedPath.MarshalText()
388
	require.NoError(t, err)
389
390
	encodedPath := string(encodedPathBytes)
391
	bracketedPath := a.bracketPath(parsedPath)
392
	trimmedPath := a.trimIndexZeros(sourcePath)
393
	reencodedPath := a.reencodePath(trimmedPath)
394
395
	if trimmedPath != encodedPath && trimmedPath != bracketedPath && reencodedPath != encodedPath {
396
		assert.Fail(t, fmt.Sprintf(`paths not equal: source "%s", encoded "%s"`, sourcePath, encodedPath))
397
	}
398
}
399
400
func (a *PropertyPathAssertion) reencodePath(trimmedPath string) string {
401
	s := strings.Builder{}
402
	for _, c := range trimmedPath {
403
		s.WriteRune(c)
404
	}
405
	return s.String()
406
}
407
408
func (a *PropertyPathAssertion) trimIndexZeros(path string) string {
409
	return a.zerosTrimmer.ReplaceAllStringFunc(path, func(s string) string {
410
		i, err := strconv.Atoi(strings.Trim(s, "[]"))
411
		if err != nil {
412
			return s
413
		}
414
		return "[" + strconv.Itoa(i) + "]"
415
	})
416
}
417
418
func (a *PropertyPathAssertion) bracketPath(path validation.PropertyPath) string {
419
	s := strings.Builder{}
420
421
	for _, e := range path.Elements() {
422
		s.WriteString("[")
423
		if e.IsIndex() {
424
			s.WriteString(e.String())
425
		} else {
426
			s.WriteString("'")
427
			p := e.String()
428
			for _, c := range p {
429
				if c == '\'' || c == '\\' {
430
					s.WriteRune('\\')
431
				}
432
				s.WriteRune(c)
433
			}
434
			s.WriteString("'")
435
		}
436
		s.WriteString("]")
437
	}
438
439
	return s.String()
440
}
441