Skip to content

Commit c831e6b

Browse files
test: add structural compatibility tests for optional types
Includes tests for optional primitive types, optional lists, maps, and struct types to validate structural compatibility scenarios. Signed-off-by: Jakob Möller <[email protected]>
1 parent 1fd231a commit c831e6b

File tree

1 file changed

+219
-0
lines changed

1 file changed

+219
-0
lines changed

pkg/cel/compatibility_test.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,222 @@ func TestNestedTypes(t *testing.T) {
437437
})
438438
}
439439
}
440+
441+
func TestOptionalPrimitive(t *testing.T) {
442+
optionalString := cel.OpaqueType("optional_type", cel.StringType)
443+
optionalInt := cel.OpaqueType("optional_type", cel.IntType)
444+
445+
tests := []struct {
446+
name string
447+
output *cel.Type
448+
expected *cel.Type
449+
compatible bool
450+
errContains string
451+
}{
452+
{
453+
name: "optional<string> to string",
454+
output: optionalString,
455+
expected: cel.StringType,
456+
compatible: true,
457+
},
458+
{
459+
name: "optional<string> to int",
460+
output: optionalString,
461+
expected: cel.IntType,
462+
compatible: false,
463+
errContains: "kind mismatch",
464+
},
465+
{
466+
name: "optional<int> to string",
467+
output: optionalInt,
468+
expected: cel.StringType,
469+
compatible: false,
470+
errContains: "kind mismatch",
471+
},
472+
{
473+
name: "optional(optional(string)) to string",
474+
output: cel.OpaqueType("optional_type", optionalString),
475+
expected: cel.StringType,
476+
compatible: true,
477+
},
478+
}
479+
480+
for _, tt := range tests {
481+
t.Run(tt.name, func(t *testing.T) {
482+
compatible, err := AreTypesStructurallyCompatible(tt.output, tt.expected, nil)
483+
assert.Equal(t, tt.compatible, compatible)
484+
if tt.compatible {
485+
assert.NoError(t, err)
486+
} else {
487+
require.Error(t, err)
488+
assert.Contains(t, err.Error(), tt.errContains)
489+
}
490+
})
491+
}
492+
}
493+
494+
func TestOptionalListTypes(t *testing.T) {
495+
optionalString := cel.OpaqueType("optional_type", cel.StringType)
496+
optionalListString := cel.OpaqueType("optional_type", cel.ListType(cel.StringType))
497+
498+
tests := []struct {
499+
name string
500+
output *cel.Type
501+
expected *cel.Type
502+
compatible bool
503+
errContains string
504+
}{
505+
{
506+
name: "optional<string> list element → list<string>",
507+
output: cel.ListType(optionalString),
508+
expected: cel.ListType(cel.StringType),
509+
compatible: true,
510+
},
511+
{
512+
name: "optional<list<string>> → list<string>",
513+
output: optionalListString,
514+
expected: cel.ListType(cel.StringType),
515+
compatible: true,
516+
},
517+
{
518+
name: "list<optional<string>> → list<int>",
519+
output: cel.ListType(optionalString),
520+
expected: cel.ListType(cel.IntType),
521+
compatible: false,
522+
errContains: "list element type incompatible",
523+
},
524+
}
525+
526+
for _, tt := range tests {
527+
t.Run(tt.name, func(t *testing.T) {
528+
compatible, err := AreTypesStructurallyCompatible(tt.output, tt.expected, nil)
529+
assert.Equal(t, tt.compatible, compatible)
530+
if tt.compatible {
531+
assert.NoError(t, err)
532+
} else {
533+
require.Error(t, err)
534+
assert.Contains(t, err.Error(), tt.errContains)
535+
}
536+
})
537+
}
538+
}
539+
540+
func TestOptionalMapTypes(t *testing.T) {
541+
optionalString := cel.OpaqueType("optional_type", cel.StringType)
542+
543+
tests := []struct {
544+
name string
545+
output *cel.Type
546+
expected *cel.Type
547+
compatible bool
548+
errContains string
549+
}{
550+
{
551+
name: "map[string]optional<string> → map[string]string",
552+
output: cel.MapType(cel.StringType, optionalString),
553+
expected: cel.MapType(cel.StringType, cel.StringType),
554+
compatible: true,
555+
},
556+
{
557+
name: "map[string]optional<string> → map[string]int",
558+
output: cel.MapType(cel.StringType, optionalString),
559+
expected: cel.MapType(cel.StringType, cel.IntType),
560+
compatible: false,
561+
errContains: "map value type incompatible",
562+
},
563+
{
564+
name: "map[optional<string>]string → map[string]string (key)",
565+
output: cel.MapType(optionalString, cel.StringType),
566+
expected: cel.MapType(cel.StringType, cel.StringType),
567+
compatible: true, // optional<string> → string ok
568+
},
569+
{
570+
name: "map[optional<string>]string → map[int]string",
571+
output: cel.MapType(optionalString, cel.StringType),
572+
expected: cel.MapType(cel.IntType, cel.StringType),
573+
compatible: false,
574+
errContains: "map key type incompatible",
575+
},
576+
}
577+
578+
for _, tt := range tests {
579+
t.Run(tt.name, func(t *testing.T) {
580+
compatible, err := AreTypesStructurallyCompatible(tt.output, tt.expected, nil)
581+
assert.Equal(t, tt.compatible, compatible)
582+
if tt.compatible {
583+
assert.NoError(t, err)
584+
} else {
585+
require.Error(t, err)
586+
assert.Contains(t, err.Error(), tt.errContains)
587+
}
588+
})
589+
}
590+
}
591+
592+
func TestOptionalStructTypes(t *testing.T) {
593+
fields := map[string]*apiservercel.DeclField{
594+
"name": apiservercel.NewDeclField("name", apiservercel.StringType, true, nil, nil),
595+
}
596+
personType := apiservercel.NewObjectType(TypeNamePrefix+"person", fields)
597+
otherType := apiservercel.NewObjectType(TypeNamePrefix+"other", fields)
598+
599+
unknownFieldsType := apiservercel.NewObjectType(TypeNamePrefix+"unknownFields", map[string]*apiservercel.DeclField{})
600+
unknownFieldsType.Metadata = map[string]string{XKubernetesPreserveUnknownFields: "true"}
601+
602+
optionalString := cel.OptionalType(cel.StringType)
603+
optionalPerson := cel.OptionalType(personType.CelType())
604+
605+
provider := NewDeclTypeProvider(personType, otherType, unknownFieldsType)
606+
607+
tests := []struct {
608+
name string
609+
output *cel.Type
610+
expected *cel.Type
611+
compatible bool
612+
errContains string
613+
}{
614+
{
615+
name: "optional<string> field inside struct",
616+
output: cel.ListType(optionalString), // simulate a struct field → list<string>
617+
expected: cel.ListType(cel.StringType),
618+
compatible: true,
619+
},
620+
{
621+
name: "optional<person> → person",
622+
output: optionalPerson,
623+
expected: personType.CelType(),
624+
compatible: true,
625+
},
626+
{
627+
name: "optional<person> → empty struct ok (permissive right now, could be rejected as well)",
628+
output: optionalPerson,
629+
expected: apiservercel.NewObjectType(TypeNamePrefix+"noFields", nil).CelType(),
630+
compatible: true,
631+
},
632+
{
633+
name: "optional<person> → matching struct",
634+
output: optionalPerson,
635+
expected: otherType.CelType(),
636+
compatible: true,
637+
},
638+
{
639+
name: "optional<person> → unknown field allowing type",
640+
output: optionalPerson,
641+
expected: unknownFieldsType.CelType(),
642+
compatible: true,
643+
},
644+
}
645+
646+
for _, tt := range tests {
647+
t.Run(tt.name, func(t *testing.T) {
648+
compatible, err := AreTypesStructurallyCompatible(tt.output, tt.expected, provider)
649+
assert.Equal(t, tt.compatible, compatible)
650+
if tt.compatible {
651+
assert.NoError(t, err)
652+
} else {
653+
require.Error(t, err)
654+
assert.Contains(t, err.Error(), tt.errContains)
655+
}
656+
})
657+
}
658+
}

0 commit comments

Comments
 (0)