Files
tasks/chasm/fields_iterator_test.go
Alan WuandGitHub 18b007c44f Add BusinessID search attribute option (#8803)
## What changed?
Add BusinessID search attribute option. CHASM archetype developers that
require Visibility will need to call `WithBusinessIDAlias` as a required
RegistrableComponent option.

Adds validation if BusinessIDAlias option is not set, but contains
Visibility as a child component, registration to the CHASM Registry will
fail.

## Why?
CHASM archetypes define separate aliases for BusinessID, eg.
`ActivityID`, `ScheduleID`, etc. End users of these archetypes will
query Visibility using a filter such as `ActivityID = my-activity-id`,
and should be able to map to the underlying shared `system` visibility
storage column.

## How did you test it?
- [X] built
- [X] run locally and tested manually
- [X] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)
2026-01-05 13:46:33 -05:00

246 lines
6.8 KiB
Go

package chasm
import (
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"go.uber.org/mock/gomock"
)
type fieldsIteratorSuite struct {
suite.Suite
*require.Assertions
controller *gomock.Controller
}
func TestFieldsIteratorSuite(t *testing.T) {
suite.Run(t, new(fieldsIteratorSuite))
}
func (s *fieldsIteratorSuite) SetupTest() {
s.initAssertions()
s.controller = gomock.NewController(s.T())
}
func (s *fieldsIteratorSuite) SetupSubTest() {
s.initAssertions()
}
func (s *fieldsIteratorSuite) initAssertions() {
// `s.Assertions` (as well as other test helpers which depends on `s.T()`) must be initialized on
// both test and subtest levels (but not suite level, where `s.T()` is `nil`).
//
// If these helpers are not reinitialized on subtest level, any failed `assert` in
// subtest will fail the entire test (not subtest) immediately without running other subtests.
s.Assertions = require.New(s.T())
}
func (s *fieldsIteratorSuite) TestGenericTypePrefix() {
tests := []struct {
name string
input any
expected string
}{
{
name: "Field type",
input: Field[string]{},
expected: chasmFieldTypePrefix,
},
{
name: "Map type",
input: Map[string, int]{},
expected: chasmMapTypePrefix,
},
{
name: "Non-generic type",
input: 0,
expected: "",
},
{
name: "Map type",
input: map[string]int{},
expected: "map[",
},
}
for _, tt := range tests {
s.Run(tt.name, func() {
typ := reflect.TypeOf(tt.input)
result := genericTypePrefix(typ)
s.Equal(tt.expected, result)
})
}
}
func (s *fieldsIteratorSuite) TestChasmFieldTypePrefix() {
f := Field[any]{}
fT := reflect.TypeOf(f)
s.True(strings.HasPrefix(fT.String(), chasmFieldTypePrefix))
}
func (s *fieldsIteratorSuite) TestChasmMapTypePrefix() {
c := Map[string, any]{}
cT := reflect.TypeOf(c)
s.True(strings.HasPrefix(cT.String(), chasmMapTypePrefix))
}
func (s *fieldsIteratorSuite) TestFieldsOf() {
type fieldPointer struct {
DataField *protoMessageType
InvalidField *Field[string]
}
type noDataField struct {
SubField Field[string]
SubMap Map[string, int]
}
type twoDataFields struct {
DataField *protoMessageType
AnotherDataField *protoMessageType
}
type unimplementedComponentOnly struct {
UnimplementedComponent
}
tests := []struct {
name string
input any
expectedKinds []fieldKind
expectedNames []string
expectedTypes []string
expectedErrors []string
}{
{
name: "Valid component with one data field",
input: &struct {
UnimplementedComponent
DataField *protoMessageType
SubField Field[string]
SubMap Map[string, int]
ignored *struct{}
}{},
expectedKinds: []fieldKind{fieldKindData, fieldKindSubField, fieldKindSubMap},
expectedNames: []string{"DataField", "SubField", "SubMap"},
expectedTypes: []string{"*persistence.WorkflowExecutionState", "chasm.Field[string]", "chasm.Map[string,int]"},
expectedErrors: []string{"", "", ""},
},
{
name: "Component with no data field",
input: &noDataField{},
expectedKinds: []fieldKind{fieldKindSubField, fieldKindSubMap, fieldKindUnspecified},
expectedNames: []string{"SubField", "SubMap", ""},
expectedTypes: []string{"chasm.Field[string]", "chasm.Map[string,int]", ""},
expectedErrors: []string{"", "", "*chasm.noDataField: no data field (implements proto.Message) found"},
},
{
name: "Component with *Field",
input: &fieldPointer{},
expectedKinds: []fieldKind{fieldKindData, fieldKindUnspecified},
expectedNames: []string{"DataField", "InvalidField"},
expectedTypes: []string{"*persistence.WorkflowExecutionState", "*chasm.Field[string]"},
expectedErrors: []string{"", "*chasm.fieldPointer.InvalidField: CHASM fields must not be pointers"},
},
{
name: "Component with multiple data fields",
input: &twoDataFields{},
expectedKinds: []fieldKind{fieldKindData, fieldKindData},
expectedNames: []string{"DataField", "AnotherDataField"},
expectedTypes: []string{"*persistence.WorkflowExecutionState", "*persistence.WorkflowExecutionState"},
expectedErrors: []string{"", "*chasm.twoDataFields.AnotherDataField: only one data field DataField (implements proto.Message) allowed in component"},
},
{
name: "Component with UnimplementedComponent only",
input: &unimplementedComponentOnly{},
expectedKinds: []fieldKind{fieldKindUnspecified},
expectedNames: []string{""},
expectedTypes: []string{""},
expectedErrors: []string{"*chasm.unimplementedComponentOnly: no data field (implements proto.Message) found"},
},
}
for _, tt := range tests {
s.Run(tt.name, func() {
valueV := reflect.ValueOf(tt.input)
var actualKinds []fieldKind
var actualNames []string
var actualTypes []string
var actualErrors []string
for field := range fieldsOf(valueV) {
actualKinds = append(actualKinds, field.kind)
actualNames = append(actualNames, field.name)
if field.typ != nil {
actualTypes = append(actualTypes, field.typ.String())
} else {
actualTypes = append(actualTypes, "")
}
if field.err != nil {
actualErrors = append(actualErrors, field.err.Error())
} else {
actualErrors = append(actualErrors, "")
}
}
s.Equal(tt.expectedKinds, actualKinds)
s.Equal(tt.expectedNames, actualNames)
s.Equal(tt.expectedTypes, actualTypes)
s.Equal(tt.expectedErrors, actualErrors)
})
}
}
func (s *fieldsIteratorSuite) TestUnmanagedFieldsOf() {
type unmanagedFields struct {
UnimplementedComponent
DataField *protoMessageType
SomeField Field[string]
unmanaged struct{}
anotherPtr *struct{}
}
var result []string
for r := range unmanagedFieldsOf(reflect.TypeFor[unmanagedFields]()) {
result = append(result, r.name)
}
s.Equal(2, len(result))
s.ElementsMatch([]string{"unmanaged", "anotherPtr"}, result)
}
func (s *fieldsIteratorSuite) TestHasVisibilityField() {
type componentWithVisibility struct {
UnimplementedComponent
DataField *protoMessageType
Visibility Field[*Visibility]
}
type componentWithoutVisibility struct {
UnimplementedComponent
DataField *protoMessageType
SomeField Field[string]
}
s.Run("component with Visibility field", func() {
s.True(hasVisibilityField(reflect.TypeFor[componentWithVisibility]()))
s.True(hasVisibilityField(reflect.TypeFor[*componentWithVisibility]()))
})
s.Run("component without Visibility field", func() {
s.False(hasVisibilityField(reflect.TypeFor[componentWithoutVisibility]()))
s.False(hasVisibilityField(reflect.TypeFor[*componentWithoutVisibility]()))
})
s.Run("non-struct type", func() {
s.False(hasVisibilityField(reflect.TypeFor[string]()))
s.False(hasVisibilityField(reflect.TypeFor[int]()))
})
}