1 package fieldbehavior
2
3 import (
4 "testing"
5
6 examplefreightv1 "go.einride.tech/aip/proto/gen/einride/example/freight/v1"
7 syntaxv1 "go.einride.tech/aip/proto/gen/einride/example/syntax/v1"
8 "google.golang.org/genproto/googleapis/api/annotations"
9 "google.golang.org/genproto/googleapis/example/library/v1"
10 "google.golang.org/protobuf/testing/protocmp"
11 "google.golang.org/protobuf/types/known/fieldmaskpb"
12 "google.golang.org/protobuf/types/known/timestamppb"
13 "gotest.tools/v3/assert"
14 "gotest.tools/v3/assert/cmp"
15 )
16
17 func TestClearFields(t *testing.T) {
18 t.Parallel()
19 t.Run("clear fields with set field_behavior", func(t *testing.T) {
20 t.Parallel()
21 site := &examplefreightv1.Site{
22 Name: "site1",
23 CreateTime: timestamppb.Now(),
24 DisplayName: "site one",
25 }
26 ClearFields(site, annotations.FieldBehavior_OUTPUT_ONLY)
27 assert.Equal(t, site.GetCreateTime(), (*timestamppb.Timestamp)(nil))
28 assert.Equal(t, site.GetDisplayName(), "site one")
29 assert.Equal(t, site.GetName(), "site1")
30 })
31 t.Run("clear field with set field_behavior on nested message", func(t *testing.T) {
32 t.Parallel()
33 input := &syntaxv1.FieldBehaviorMessage{
34 Field: "field",
35 OptionalField: "optional",
36 MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{
37 Field: "field",
38 OptionalField: "optional",
39 OutputOnlyField: "output_only",
40 },
41 }
42
43 expected := &syntaxv1.FieldBehaviorMessage{
44 Field: "field",
45 OptionalField: "optional",
46 MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{
47 Field: "field",
48 OptionalField: "optional",
49 },
50 }
51
52 ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
53 assert.DeepEqual(t, input, expected, protocmp.Transform())
54 })
55
56 t.Run("clear field with set field_behavior on multiple levels of nested messages", func(t *testing.T) {
57 t.Parallel()
58 input := &syntaxv1.FieldBehaviorMessage{
59 Field: "field",
60 OptionalField: "optional",
61 MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{
62 Field: "field",
63 OptionalField: "optional",
64 OutputOnlyField: "output_only",
65 MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{
66 Field: "field",
67 OptionalField: "optional",
68 OutputOnlyField: "output_only",
69 },
70 },
71 }
72
73 expected := &syntaxv1.FieldBehaviorMessage{
74 Field: "field",
75 OptionalField: "optional",
76 MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{
77 Field: "field",
78 OptionalField: "optional",
79 MessageWithoutFieldBehavior: &syntaxv1.FieldBehaviorMessage{
80 Field: "field",
81 OptionalField: "optional",
82 },
83 },
84 }
85
86 ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
87 assert.DeepEqual(t, input, expected, protocmp.Transform())
88 })
89
90 t.Run("clear fields with set field_behavior on repeated message", func(t *testing.T) {
91 t.Parallel()
92 input := &syntaxv1.FieldBehaviorMessage{
93 Field: "field",
94 OptionalField: "optional",
95 RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{
96 {
97 Field: "field",
98 OptionalField: "optional",
99 OutputOnlyField: "output_only",
100 },
101 },
102 StringList: []string{
103 "string",
104 },
105 }
106
107 expected := &syntaxv1.FieldBehaviorMessage{
108 Field: "field",
109 OptionalField: "optional",
110 RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{
111 {
112 Field: "field",
113 OptionalField: "optional",
114 },
115 },
116 StringList: []string{
117 "string",
118 },
119 }
120
121 ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
122 assert.DeepEqual(t, input, expected, protocmp.Transform())
123 })
124
125 t.Run("clear fields with set field_behavior on multiple levels of repeated messages", func(t *testing.T) {
126 t.Parallel()
127 input := &syntaxv1.FieldBehaviorMessage{
128 Field: "field",
129 OptionalField: "optional",
130 RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{
131 {
132 Field: "field",
133 OptionalField: "optional",
134 OutputOnlyField: "output_only",
135 RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{
136 {
137 Field: "field",
138 OptionalField: "optional",
139 OutputOnlyField: "output_only",
140 },
141 },
142 },
143 },
144 StringList: []string{
145 "string",
146 },
147 }
148
149 expected := &syntaxv1.FieldBehaviorMessage{
150 Field: "field",
151 OptionalField: "optional",
152 RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{
153 {
154 Field: "field",
155 OptionalField: "optional",
156 RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{
157 {
158 Field: "field",
159 OptionalField: "optional",
160 },
161 },
162 },
163 },
164 StringList: []string{
165 "string",
166 },
167 }
168
169 ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
170 assert.DeepEqual(t, input, expected, protocmp.Transform())
171 })
172
173 t.Run("clear repeated field with set field_behavior", func(t *testing.T) {
174 t.Parallel()
175 input := &syntaxv1.FieldBehaviorMessage{
176 Field: "field",
177 OptionalField: "optional",
178 RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{
179 {
180 Field: "field",
181 OptionalField: "optional",
182 },
183 },
184 RepeatedOutputOnlyMessage: []*syntaxv1.FieldBehaviorMessage{
185 {
186 Field: "field",
187 OptionalField: "optional",
188 OutputOnlyField: "output_only",
189 },
190 },
191 }
192
193 expected := &syntaxv1.FieldBehaviorMessage{
194 Field: "field",
195 OptionalField: "optional",
196 RepeatedMessage: []*syntaxv1.FieldBehaviorMessage{
197 {
198 Field: "field",
199 OptionalField: "optional",
200 },
201 },
202 }
203
204 ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
205 assert.DeepEqual(t, input, expected, protocmp.Transform())
206 })
207
208 t.Run("clear fields with set field_behavior on message in map", func(t *testing.T) {
209 t.Parallel()
210 input := &syntaxv1.FieldBehaviorMessage{
211 OptionalField: "optional",
212 MapOptionalMessage: map[string]*syntaxv1.FieldBehaviorMessage{
213 "key_1": {
214 OptionalField: "optional",
215 OutputOnlyField: "output_only",
216 },
217 },
218 StringMap: map[string]string{
219 "string_key": "string",
220 },
221 }
222
223 expected := &syntaxv1.FieldBehaviorMessage{
224 OptionalField: "optional",
225 MapOptionalMessage: map[string]*syntaxv1.FieldBehaviorMessage{
226 "key_1": {
227 OptionalField: "optional",
228 },
229 },
230 StringMap: map[string]string{
231 "string_key": "string",
232 },
233 }
234
235 ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
236 assert.DeepEqual(
237 t,
238 input,
239 expected,
240 protocmp.Transform(),
241 )
242 })
243
244 t.Run("clear map field with set field_behavior", func(t *testing.T) {
245 t.Parallel()
246 input := &syntaxv1.FieldBehaviorMessage{
247 OptionalField: "optional",
248
249 MapOutputOnlyMessage: map[string]*syntaxv1.FieldBehaviorMessage{
250 "key_1": {
251 OutputOnlyField: "output_only",
252 },
253 },
254 }
255
256 expected := &syntaxv1.FieldBehaviorMessage{
257 OptionalField: "optional",
258 }
259
260 ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
261 assert.DeepEqual(
262 t,
263 input,
264 expected,
265 protocmp.Transform(),
266 )
267 })
268
269 t.Run("clear field with set field_behavior on oneof message", func(t *testing.T) {
270 t.Parallel()
271 input := &syntaxv1.FieldBehaviorMessage{
272 Field: "field",
273 OptionalField: "optional",
274 Oneof: &syntaxv1.FieldBehaviorMessage_FieldBehaviorMessage{
275 FieldBehaviorMessage: &syntaxv1.FieldBehaviorMessage{
276 Field: "field",
277 OptionalField: "optional",
278 OutputOnlyField: "output_only",
279 },
280 },
281 }
282
283 expected := &syntaxv1.FieldBehaviorMessage{
284 Field: "field",
285 OptionalField: "optional",
286 Oneof: &syntaxv1.FieldBehaviorMessage_FieldBehaviorMessage{
287 FieldBehaviorMessage: &syntaxv1.FieldBehaviorMessage{
288 Field: "field",
289 OptionalField: "optional",
290 },
291 },
292 }
293
294 ClearFields(input, annotations.FieldBehavior_OUTPUT_ONLY)
295 assert.DeepEqual(t, input, expected, protocmp.Transform())
296 })
297 }
298
299 func TestCopyFields(t *testing.T) {
300 t.Parallel()
301 t.Run("different types", func(t *testing.T) {
302 t.Parallel()
303 assert.Assert(t, cmp.Panics(func() {
304 CopyFields(&library.Book{}, &library.Shelf{}, annotations.FieldBehavior_REQUIRED)
305 }))
306 })
307 }
308
309 func TestValidateRequiredFields(t *testing.T) {
310 t.Parallel()
311 assert.NilError(t, ValidateRequiredFields(&examplefreightv1.GetShipmentRequest{Name: "testbook"}))
312 assert.Error(t, ValidateRequiredFields(&examplefreightv1.GetShipmentRequest{}), "missing required field: name")
313 }
314
315 func TestValidateRequiredFieldsWithMask(t *testing.T) {
316 t.Parallel()
317 t.Run("ok", func(t *testing.T) {
318 t.Parallel()
319 assert.NilError(
320 t,
321 ValidateRequiredFieldsWithMask(
322 &library.Book{Name: "testbook"},
323 nil,
324 ),
325 )
326 })
327 t.Run("ok - empty mask", func(t *testing.T) {
328 t.Parallel()
329 assert.NilError(
330 t,
331 ValidateRequiredFieldsWithMask(
332 &library.Book{},
333 nil,
334 ),
335 )
336 })
337 t.Run("missing field", func(t *testing.T) {
338 t.Parallel()
339 assert.Error(
340 t,
341 ValidateRequiredFieldsWithMask(
342 &examplefreightv1.GetShipmentRequest{},
343 &fieldmaskpb.FieldMask{Paths: []string{"*"}},
344 ),
345 "missing required field: name",
346 )
347 })
348 t.Run("missing but not in mask", func(t *testing.T) {
349 t.Parallel()
350 assert.NilError(
351 t,
352 ValidateRequiredFieldsWithMask(
353 &library.Book{},
354 &fieldmaskpb.FieldMask{Paths: []string{"author"}},
355 ),
356 )
357 })
358 t.Run("missing nested", func(t *testing.T) {
359 t.Parallel()
360 assert.Error(
361 t,
362 ValidateRequiredFieldsWithMask(
363 &examplefreightv1.UpdateShipmentRequest{
364 Shipment: &examplefreightv1.Shipment{},
365 },
366 &fieldmaskpb.FieldMask{Paths: []string{"shipment.origin_site"}},
367 ),
368 "missing required field: shipment.origin_site",
369 )
370 })
371 t.Run("missing nested not in mask", func(t *testing.T) {
372 t.Parallel()
373 assert.NilError(
374 t,
375 ValidateRequiredFieldsWithMask(
376 &library.UpdateBookRequest{
377 Book: &library.Book{},
378 },
379 &fieldmaskpb.FieldMask{Paths: []string{"book.author"}},
380 ),
381 )
382 })
383 t.Run("support maps", func(t *testing.T) {
384 t.Parallel()
385 assert.NilError(
386 t,
387 ValidateRequiredFieldsWithMask(
388 &examplefreightv1.Shipment{
389 Annotations: map[string]string{
390 "x": "y",
391 },
392 },
393 &fieldmaskpb.FieldMask{Paths: []string{"annotations"}},
394 ),
395 )
396 })
397 }
398
399 func TestValidateImmutableFieldsWithMask(t *testing.T) {
400 t.Parallel()
401 t.Run("no error when immutable field not set", func(t *testing.T) {
402 t.Parallel()
403 req := &examplefreightv1.UpdateShipmentRequest{
404 Shipment: &examplefreightv1.Shipment{},
405 UpdateMask: &fieldmaskpb.FieldMask{
406 Paths: []string{""},
407 },
408 }
409 err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
410 assert.NilError(t, err)
411 })
412 t.Run("no error when immutable field not part of fieldmask", func(t *testing.T) {
413 t.Parallel()
414 req := &examplefreightv1.UpdateShipmentRequest{
415 Shipment: &examplefreightv1.Shipment{
416 ExternalReferenceId: "external-reference-id",
417 OriginSite: "shippers/shipper1/sites/site1",
418 },
419 UpdateMask: &fieldmaskpb.FieldMask{
420 Paths: []string{"shipment.origin_site"},
421 },
422 }
423 err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
424 assert.NilError(t, err)
425 })
426 t.Run("errors when wildcard fieldmask used", func(t *testing.T) {
427 t.Parallel()
428 req := &examplefreightv1.UpdateShipmentRequest{
429 Shipment: &examplefreightv1.Shipment{},
430 UpdateMask: &fieldmaskpb.FieldMask{
431 Paths: []string{"*"},
432 },
433 }
434 err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
435 assert.ErrorContains(t, err, "field is immutable")
436 })
437 t.Run("errors when immutable field set in fieldmask", func(t *testing.T) {
438 t.Parallel()
439 req := &examplefreightv1.UpdateShipmentRequest{
440 Shipment: &examplefreightv1.Shipment{},
441 UpdateMask: &fieldmaskpb.FieldMask{
442 Paths: []string{"shipment.external_reference_id"},
443 },
444 }
445 err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
446 assert.ErrorContains(t, err, "field is immutable")
447 })
448 t.Run("errors when immutable field set in message", func(t *testing.T) {
449 t.Parallel()
450 req := &examplefreightv1.UpdateShipmentRequest{
451 Shipment: &examplefreightv1.Shipment{
452 ExternalReferenceId: "I am immutable!",
453 },
454 UpdateMask: &fieldmaskpb.FieldMask{
455 Paths: []string{},
456 },
457 }
458 err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
459 assert.ErrorContains(t, err, "field is immutable")
460 })
461 t.Run("errors when immutable field set in nested field", func(t *testing.T) {
462 t.Parallel()
463 req := &examplefreightv1.UpdateShipmentRequest{
464 Shipment: &examplefreightv1.Shipment{
465 LineItems: []*examplefreightv1.LineItem{
466 {
467 ExternalReferenceId: "I am immutable",
468 },
469 },
470 },
471 UpdateMask: &fieldmaskpb.FieldMask{
472 Paths: []string{},
473 },
474 }
475 err := ValidateImmutableFieldsWithMask(req, req.GetUpdateMask())
476 assert.ErrorContains(t, err, "field is immutable")
477 })
478 }
479
View as plain text