diff --git a/mdl/catalog/builder_microflows_test.go b/mdl/catalog/builder_microflows_test.go new file mode 100644 index 00000000..073bbf60 --- /dev/null +++ b/mdl/catalog/builder_microflows_test.go @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: Apache-2.0 + +package catalog + +import ( + "testing" + + "github.com/mendixlabs/mxcli/sdk/microflows" + "github.com/mendixlabs/mxcli/model" +) + +// unknownMicroflowObject satisfies MicroflowObject but is not in the type switch. +type unknownMicroflowObject struct{} + +func (u *unknownMicroflowObject) GetID() model.ID { return "" } +func (u *unknownMicroflowObject) GetPosition() model.Point { return model.Point{} } +func (u *unknownMicroflowObject) SetPosition(model.Point) {} + +func TestGetMicroflowObjectType(t *testing.T) { + tests := []struct { + name string + obj microflows.MicroflowObject + want string + }{ + {"ActionActivity", µflows.ActionActivity{}, "ActionActivity"}, + {"StartEvent", µflows.StartEvent{}, "StartEvent"}, + {"EndEvent", µflows.EndEvent{}, "EndEvent"}, + {"ExclusiveSplit", µflows.ExclusiveSplit{}, "ExclusiveSplit"}, + {"InheritanceSplit", µflows.InheritanceSplit{}, "InheritanceSplit"}, + {"ExclusiveMerge", µflows.ExclusiveMerge{}, "ExclusiveMerge"}, + {"LoopedActivity", µflows.LoopedActivity{}, "LoopedActivity"}, + {"Annotation", µflows.Annotation{}, "Annotation"}, + {"BreakEvent", µflows.BreakEvent{}, "BreakEvent"}, + {"ContinueEvent", µflows.ContinueEvent{}, "ContinueEvent"}, + {"ErrorEvent", µflows.ErrorEvent{}, "ErrorEvent"}, + {"unknown object falls to default", &unknownMicroflowObject{}, "MicroflowObject"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getMicroflowObjectType(tt.obj); got != tt.want { + t.Errorf("getMicroflowObjectType() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestGetMicroflowActionType(t *testing.T) { + tests := []struct { + name string + action microflows.MicroflowAction + want string + }{ + {"CreateObjectAction", µflows.CreateObjectAction{}, "CreateObjectAction"}, + {"ChangeObjectAction", µflows.ChangeObjectAction{}, "ChangeObjectAction"}, + {"RetrieveAction", µflows.RetrieveAction{}, "RetrieveAction"}, + {"MicroflowCallAction", µflows.MicroflowCallAction{}, "MicroflowCallAction"}, + {"JavaActionCallAction", µflows.JavaActionCallAction{}, "JavaActionCallAction"}, + {"ShowMessageAction", µflows.ShowMessageAction{}, "ShowMessageAction"}, + {"LogMessageAction", µflows.LogMessageAction{}, "LogMessageAction"}, + {"ValidationFeedbackAction", µflows.ValidationFeedbackAction{}, "ValidationFeedbackAction"}, + {"ChangeVariableAction", µflows.ChangeVariableAction{}, "ChangeVariableAction"}, + {"CreateVariableAction", µflows.CreateVariableAction{}, "CreateVariableAction"}, + {"AggregateListAction", µflows.AggregateListAction{}, "AggregateListAction"}, + {"ListOperationAction", µflows.ListOperationAction{}, "ListOperationAction"}, + {"CastAction", µflows.CastAction{}, "CastAction"}, + {"DownloadFileAction", µflows.DownloadFileAction{}, "DownloadFileAction"}, + {"ClosePageAction", µflows.ClosePageAction{}, "ClosePageAction"}, + {"ShowPageAction", µflows.ShowPageAction{}, "ShowPageAction"}, + {"CallExternalAction", µflows.CallExternalAction{}, "CallExternalAction"}, + {"unknown action falls to default", µflows.UnknownAction{TypeName: "CustomThing"}, "MicroflowAction"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getMicroflowActionType(tt.action); got != tt.want { + t.Errorf("getMicroflowActionType() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestGetDataTypeName(t *testing.T) { + tests := []struct { + name string + dt microflows.DataType + want string + }{ + {"nil", nil, ""}, + {"Boolean", µflows.BooleanType{}, "Boolean"}, + {"Integer", µflows.IntegerType{}, "Integer"}, + {"Long", µflows.LongType{}, "Long"}, + {"Decimal", µflows.DecimalType{}, "Decimal"}, + {"String", µflows.StringType{}, "String"}, + {"DateTime", µflows.DateTimeType{}, "DateTime"}, + {"Date", µflows.DateType{}, "Date"}, + {"Void", µflows.VoidType{}, "Void"}, + {"Object with entity", µflows.ObjectType{EntityQualifiedName: "Module.Entity"}, "Object:Module.Entity"}, + {"List with entity", µflows.ListType{EntityQualifiedName: "Module.Entity"}, "List:Module.Entity"}, + {"Enumeration", µflows.EnumerationType{EnumerationQualifiedName: "Module.Color"}, "Enumeration:Module.Color"}, + {"Binary falls to Unknown", µflows.BinaryType{}, "Unknown"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getDataTypeName(tt.dt); got != tt.want { + t.Errorf("getDataTypeName() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestCountMicroflowActivities(t *testing.T) { + tests := []struct { + name string + mf *microflows.Microflow + want int + }{ + { + name: "nil object collection", + mf: µflows.Microflow{}, + want: 0, + }, + { + name: "empty objects", + mf: µflows.Microflow{ + ObjectCollection: µflows.MicroflowObjectCollection{}, + }, + want: 0, + }, + { + name: "excludes start/end/merge", + mf: µflows.Microflow{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.StartEvent{}, + µflows.ActionActivity{}, + µflows.ExclusiveSplit{}, + µflows.EndEvent{}, + µflows.ExclusiveMerge{}, + }, + }, + }, + want: 2, // ActionActivity + ExclusiveSplit + }, + { + name: "counts loops and annotations", + mf: µflows.Microflow{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.LoopedActivity{}, + µflows.Annotation{}, + µflows.ErrorEvent{}, + }, + }, + }, + want: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := countMicroflowActivities(tt.mf); got != tt.want { + t.Errorf("countMicroflowActivities() = %d, want %d", got, tt.want) + } + }) + } +} + +func TestCalculateMcCabeComplexity(t *testing.T) { + tests := []struct { + name string + mf *microflows.Microflow + want int + }{ + { + name: "nil object collection — base complexity", + mf: µflows.Microflow{}, + want: 1, + }, + { + name: "no decision points", + mf: µflows.Microflow{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.StartEvent{}, + µflows.ActionActivity{}, + µflows.EndEvent{}, + }, + }, + }, + want: 1, + }, + { + name: "exclusive split adds 1", + mf: µflows.Microflow{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.ExclusiveSplit{}, + }, + }, + }, + want: 2, + }, + { + name: "inheritance split adds 1", + mf: µflows.Microflow{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.InheritanceSplit{}, + }, + }, + }, + want: 2, + }, + { + name: "loop adds 1 plus nested decisions", + mf: µflows.Microflow{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.LoopedActivity{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.ExclusiveSplit{}, + }, + }, + }, + }, + }, + }, + want: 3, // 1 base + 1 loop + 1 nested split + }, + { + name: "error event adds 1", + mf: µflows.Microflow{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.ErrorEvent{}, + }, + }, + }, + want: 2, + }, + { + name: "complex flow", + mf: µflows.Microflow{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.ExclusiveSplit{}, + µflows.ExclusiveSplit{}, + µflows.InheritanceSplit{}, + µflows.LoopedActivity{}, + µflows.ErrorEvent{}, + }, + }, + }, + want: 6, // 1 + 2 splits + 1 inheritance + 1 loop + 1 error + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := calculateMcCabeComplexity(tt.mf); got != tt.want { + t.Errorf("calculateMcCabeComplexity() = %d, want %d", got, tt.want) + } + }) + } +} + +func TestCountNanoflowActivities(t *testing.T) { + tests := []struct { + name string + nf *microflows.Nanoflow + want int + }{ + { + name: "nil object collection", + nf: µflows.Nanoflow{}, + want: 0, + }, + { + name: "excludes structural elements", + nf: µflows.Nanoflow{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.StartEvent{}, + µflows.ActionActivity{}, + µflows.EndEvent{}, + µflows.ExclusiveMerge{}, + }, + }, + }, + want: 1, // only ActionActivity + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := countNanoflowActivities(tt.nf); got != tt.want { + t.Errorf("countNanoflowActivities() = %d, want %d", got, tt.want) + } + }) + } +} + +func TestCalculateNanoflowComplexity(t *testing.T) { + nf := µflows.Nanoflow{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.ExclusiveSplit{}, + µflows.LoopedActivity{ + ObjectCollection: µflows.MicroflowObjectCollection{ + Objects: []microflows.MicroflowObject{ + µflows.InheritanceSplit{}, + }, + }, + }, + }, + }, + } + + got := calculateNanoflowComplexity(nf) + want := 4 // 1 base + 1 split + 1 loop + 1 nested inheritance + if got != want { + t.Errorf("calculateNanoflowComplexity() = %d, want %d", got, want) + } +} diff --git a/mdl/catalog/builder_modules_test.go b/mdl/catalog/builder_modules_test.go new file mode 100644 index 00000000..f3947006 --- /dev/null +++ b/mdl/catalog/builder_modules_test.go @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 + +package catalog + +import "testing" + +func TestExtractAttrName(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + {"three parts", "Module.Entity.Attribute", "Attribute"}, + {"four parts", "Module.Entity.Attribute.Sub", "Sub"}, + {"two parts", "Module.Entity", ""}, + {"single part", "Single", ""}, + {"empty string", "", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := extractAttrName(tt.input); got != tt.want { + t.Errorf("extractAttrName(%q) = %q, want %q", tt.input, got, tt.want) + } + }) + } +} diff --git a/mdl/catalog/builder_navigation_test.go b/mdl/catalog/builder_navigation_test.go new file mode 100644 index 00000000..88f49de4 --- /dev/null +++ b/mdl/catalog/builder_navigation_test.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +package catalog + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/types" +) + +func TestCountMenuItems(t *testing.T) { + tests := []struct { + name string + items []*types.NavMenuItem + want int + }{ + { + name: "nil items", + items: nil, + want: 0, + }, + { + name: "empty items", + items: []*types.NavMenuItem{}, + want: 0, + }, + { + name: "flat items", + items: []*types.NavMenuItem{ + {Caption: "Home"}, + {Caption: "About"}, + {Caption: "Contact"}, + }, + want: 3, + }, + { + name: "nested items", + items: []*types.NavMenuItem{ + { + Caption: "Admin", + Items: []*types.NavMenuItem{ + {Caption: "Users"}, + {Caption: "Roles"}, + }, + }, + {Caption: "Home"}, + }, + want: 4, // Admin + Users + Roles + Home + }, + { + name: "deeply nested", + items: []*types.NavMenuItem{ + { + Caption: "L1", + Items: []*types.NavMenuItem{ + { + Caption: "L2", + Items: []*types.NavMenuItem{ + {Caption: "L3"}, + }, + }, + }, + }, + }, + want: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := countMenuItems(tt.items); got != tt.want { + t.Errorf("countMenuItems() = %d, want %d", got, tt.want) + } + }) + } +} diff --git a/mdl/catalog/builder_pages_test.go b/mdl/catalog/builder_pages_test.go new file mode 100644 index 00000000..25bf104a --- /dev/null +++ b/mdl/catalog/builder_pages_test.go @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: Apache-2.0 + +package catalog + +import ( + "testing" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestExtractLayoutRef(t *testing.T) { + tests := []struct { + name string + rawData map[string]any + want string + }{ + { + name: "no FormCall", + rawData: map[string]any{}, + want: "", + }, + { + name: "string Form field", + rawData: map[string]any{ + "FormCall": map[string]any{ + "Form": "MyModule.MyLayout", + }, + }, + want: "MyModule.MyLayout", + }, + { + name: "empty Form and no Layout", + rawData: map[string]any{ + "FormCall": map[string]any{ + "Form": "", + }, + }, + want: "", + }, + { + name: "binary Layout field", + rawData: map[string]any{ + "FormCall": map[string]any{ + "Layout": primitive.Binary{ + Data: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, + }, + }, + }, + want: "04030201-0605-0807-090a-0b0c0d0e0f10", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := extractLayoutRef(tt.rawData); got != tt.want { + t.Errorf("extractLayoutRef() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestExtractPageWidgets(t *testing.T) { + t.Run("no FormCall", func(t *testing.T) { + got := extractPageWidgets(map[string]any{}, "container-1") + if got != nil { + t.Errorf("expected nil, got %v", got) + } + }) + + t.Run("no Arguments", func(t *testing.T) { + got := extractPageWidgets(map[string]any{ + "FormCall": map[string]any{}, + }, "container-1") + if got != nil { + t.Errorf("expected nil, got %v", got) + } + }) + + t.Run("extracts widgets from arguments", func(t *testing.T) { + rawData := map[string]any{ + "FormCall": map[string]any{ + "Arguments": []any{ + int32(0), // BSON type indicator + map[string]any{ + "Widgets": []any{ + int32(0), + map[string]any{ + "$ID": "widget-1", + "Name": "textBox1", + "$Type": "Forms$TextBox", + }, + }, + }, + }, + }, + } + got := extractPageWidgets(rawData, "container-1") + if len(got) != 1 { + t.Fatalf("expected 1 widget, got %d", len(got)) + } + if got[0].Name != "textBox1" { + t.Errorf("widget name = %q, want %q", got[0].Name, "textBox1") + } + if got[0].WidgetType != "Forms$TextBox" { + t.Errorf("widget type = %q, want %q", got[0].WidgetType, "Forms$TextBox") + } + }) +} + +func TestExtractWidgetsRecursive(t *testing.T) { + t.Run("simple widget", func(t *testing.T) { + w := map[string]any{ + "$ID": "w1", + "Name": "myWidget", + "$Type": "Forms$TextBox", + } + got := extractWidgetsRecursive(w) + if len(got) != 1 { + t.Fatalf("expected 1 widget, got %d", len(got)) + } + if got[0].ID != "w1" || got[0].Name != "myWidget" { + t.Errorf("unexpected widget: %+v", got[0]) + } + }) + + t.Run("skips DivContainer but includes children", func(t *testing.T) { + w := map[string]any{ + "$ID": "div1", + "Name": "divContainer", + "$Type": "Forms$DivContainer", + "Widgets": []any{ + int32(0), + map[string]any{ + "$ID": "child1", + "Name": "childWidget", + "$Type": "Forms$Button", + }, + }, + } + got := extractWidgetsRecursive(w) + if len(got) != 1 { + t.Fatalf("expected 1 widget (child only), got %d", len(got)) + } + if got[0].ID != "child1" { + t.Errorf("expected child1, got %q", got[0].ID) + } + }) + + t.Run("skips Pages$DivContainer but includes children", func(t *testing.T) { + w := map[string]any{ + "$ID": "div2", + "Name": "pagesDivContainer", + "$Type": "Pages$DivContainer", + "Widgets": []any{ + int32(0), + map[string]any{ + "$ID": "child2", + "Name": "childWidget2", + "$Type": "Pages$Button", + }, + }, + } + got := extractWidgetsRecursive(w) + if len(got) != 1 { + t.Fatalf("expected 1 widget (child only), got %d", len(got)) + } + if got[0].ID != "child2" { + t.Errorf("expected child2, got %q", got[0].ID) + } + }) + + t.Run("CustomWidget resolves WidgetId", func(t *testing.T) { + w := map[string]any{ + "$ID": "cw1", + "Name": "customWidget", + "$Type": "CustomWidgets$CustomWidget", + "Type": map[string]any{ + "WidgetId": "com.mendix.widget.MyCustom", + }, + } + got := extractWidgetsRecursive(w) + if len(got) != 1 { + t.Fatalf("expected 1 widget, got %d", len(got)) + } + if got[0].WidgetType != "com.mendix.widget.MyCustom" { + t.Errorf("WidgetType = %q, want %q", got[0].WidgetType, "com.mendix.widget.MyCustom") + } + }) + + t.Run("extracts attribute reference", func(t *testing.T) { + w := map[string]any{ + "$ID": "w1", + "Name": "inputWidget", + "$Type": "Forms$TextBox", + "AttributeRef": map[string]any{ + "Attribute": "Module.Entity.Name", + }, + } + got := extractWidgetsRecursive(w) + if len(got) != 1 { + t.Fatalf("expected 1 widget, got %d", len(got)) + } + if got[0].AttributeRef != "Module.Entity.Name" { + t.Errorf("AttributeRef = %q, want %q", got[0].AttributeRef, "Module.Entity.Name") + } + }) + + t.Run("recurses into LayoutGrid rows/columns", func(t *testing.T) { + w := map[string]any{ + "$ID": "grid1", + "Name": "layoutGrid", + "$Type": "Forms$LayoutGrid", + "Rows": []any{ + int32(0), + map[string]any{ + "Columns": []any{ + int32(0), + map[string]any{ + "Widgets": []any{ + int32(0), + map[string]any{ + "$ID": "nested1", + "Name": "nestedWidget", + "$Type": "Forms$Text", + }, + }, + }, + }, + }, + }, + } + got := extractWidgetsRecursive(w) + // grid1 itself + nested1 + if len(got) != 2 { + t.Fatalf("expected 2 widgets, got %d", len(got)) + } + }) +} + +func TestExtractSnippetWidgets(t *testing.T) { + t.Run("Widgets plural format", func(t *testing.T) { + rawData := map[string]any{ + "Widgets": []any{ + int32(0), + map[string]any{ + "$ID": "sw1", + "Name": "snippetWidget", + "$Type": "Forms$TextBox", + }, + }, + } + got := extractSnippetWidgets(rawData) + if len(got) != 1 { + t.Fatalf("expected 1 widget, got %d", len(got)) + } + }) + + t.Run("Widget singular container format", func(t *testing.T) { + rawData := map[string]any{ + "Widget": map[string]any{ + "Widgets": []any{ + int32(0), + map[string]any{ + "$ID": "sw1", + "Name": "innerWidget", + "$Type": "Forms$Button", + }, + }, + }, + } + got := extractSnippetWidgets(rawData) + if len(got) != 1 { + t.Fatalf("expected 1 widget, got %d", len(got)) + } + }) + + t.Run("nil when no widgets", func(t *testing.T) { + got := extractSnippetWidgets(map[string]any{}) + if got != nil { + t.Errorf("expected nil, got %v", got) + } + }) +} + +func TestGetBsonArrayElements(t *testing.T) { + tests := []struct { + name string + v any + want int // expected length, -1 for nil + }{ + {"nil input", nil, -1}, + {"int32 type indicator", []any{int32(0), "a", "b"}, 2}, + {"int type indicator", []any{int(0), "a", "b"}, 2}, + {"no type indicator", []any{"a", "b"}, 2}, + {"primitive.A with indicator", primitive.A{int32(0), "a"}, 1}, + {"empty array", []any{}, -1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getBsonArrayElements(tt.v) + if tt.want == -1 { + if got != nil { + t.Errorf("expected nil, got %v", got) + } + } else if len(got) != tt.want { + t.Errorf("len = %d, want %d", len(got), tt.want) + } + }) + } +} + +func TestToBsonArray(t *testing.T) { + t.Run("[]any passthrough", func(t *testing.T) { + input := []any{"a", "b"} + got := toBsonArray(input) + if len(got) != 2 { + t.Errorf("expected 2, got %d", len(got)) + } + }) + + t.Run("primitive.A converted", func(t *testing.T) { + input := primitive.A{"x", "y"} + got := toBsonArray(input) + if len(got) != 2 { + t.Errorf("expected 2, got %d", len(got)) + } + }) + + t.Run("unsupported type returns nil", func(t *testing.T) { + got := toBsonArray("not an array") + if got != nil { + t.Errorf("expected nil, got %v", got) + } + }) +} + +func TestExtractString(t *testing.T) { + if got := extractString("hello"); got != "hello" { + t.Errorf("expected hello, got %q", got) + } + if got := extractString(42); got != "" { + t.Errorf("expected empty, got %q", got) + } + if got := extractString(nil); got != "" { + t.Errorf("expected empty, got %q", got) + } +} + +func TestExtractBsonID(t *testing.T) { + tests := []struct { + name string + v any + want string + }{ + {"nil", nil, ""}, + {"string", "my-id", "my-id"}, + {"binary map with base64 GUID", map[string]any{"Data": "AQIDBAUGBwgJCgsMDQ4PEA=="}, "04030201-0605-0807-090a-0b0c0d0e0f10"}, + {"primitive.Binary", primitive.Binary{Data: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}}, "04030201-0605-0807-090a-0b0c0d0e0f10"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := extractBsonID(tt.v); got != tt.want { + t.Errorf("extractBsonID() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestDecodeBase64GUID(t *testing.T) { + t.Run("short data returned as-is", func(t *testing.T) { + encoded := "AQIDBAUG" // only 6 bytes — too short for GUID + got := decodeBase64GUID(encoded) + if got != encoded { + t.Errorf("expected passthrough for short data, got %q", got) + } + }) + + t.Run("valid 16-byte GUID", func(t *testing.T) { + // base64 of bytes 0x01..0x10 + encoded := "AQIDBAUGBwgJCgsMDQ4PEA==" + got := decodeBase64GUID(encoded) + want := "04030201-0605-0807-090a-0b0c0d0e0f10" + if got != want { + t.Errorf("decodeBase64GUID() = %q, want %q", got, want) + } + }) + + t.Run("invalid base64", func(t *testing.T) { + encoded := "not-valid-base64!!!" + got := decodeBase64GUID(encoded) + if got != encoded { + t.Errorf("expected passthrough for invalid base64, got %q", got) + } + }) +} + +func TestExtractBinaryID(t *testing.T) { + tests := []struct { + name string + v any + want string + }{ + {"string", "my-id", "my-id"}, + {"bytes 16", []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, "04030201-0605-0807-090a-0b0c0d0e0f10"}, + {"primitive.Binary", primitive.Binary{Data: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}}, "04030201-0605-0807-090a-0b0c0d0e0f10"}, + {"nil", nil, ""}, + {"unsupported", 42, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := extractBinaryID(tt.v); got != tt.want { + t.Errorf("extractBinaryID() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestFormatGUID(t *testing.T) { + tests := []struct { + name string + data []byte + want string + }{ + { + name: "standard 16-byte GUID", + data: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, + want: "04030201-0605-0807-090a-0b0c0d0e0f10", + }, + { + name: "all zeros", + data: make([]byte, 16), + want: "00000000-0000-0000-0000-000000000000", + }, + { + name: "short data — returns raw string passthrough", + data: []byte{0x41, 0x42}, + want: "AB", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := formatGUID(tt.data); got != tt.want { + t.Errorf("formatGUID() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestBytesToHex(t *testing.T) { + tests := []struct { + name string + data []byte + want string + }{ + {"zero byte", []byte{0x00}, "00"}, + {"max byte", []byte{0xff}, "ff"}, + {"two bytes", []byte{0x0a, 0xbc}, "0abc"}, + {"empty", []byte{}, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := bytesToHex(tt.data); got != tt.want { + t.Errorf("bytesToHex(%v) = %q, want %q", tt.data, got, tt.want) + } + }) + } +} diff --git a/mdl/catalog/builder_permissions_test.go b/mdl/catalog/builder_permissions_test.go new file mode 100644 index 00000000..f89aaa23 --- /dev/null +++ b/mdl/catalog/builder_permissions_test.go @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 + +package catalog + +import ( + "testing" + + "github.com/mendixlabs/mxcli/sdk/domainmodel" +) + +func TestEntityAccessFromMemberRights(t *testing.T) { + tests := []struct { + name string + rule *domainmodel.AccessRule + wantRead bool + wantWrite bool + }{ + { + name: "no member accesses, default None", + rule: &domainmodel.AccessRule{ + DefaultMemberAccessRights: domainmodel.MemberAccessRightsNone, + }, + wantRead: false, + wantWrite: false, + }, + { + name: "no member accesses, default ReadOnly", + rule: &domainmodel.AccessRule{ + DefaultMemberAccessRights: domainmodel.MemberAccessRightsReadOnly, + }, + wantRead: true, + wantWrite: false, + }, + { + name: "no member accesses, default ReadWrite", + rule: &domainmodel.AccessRule{ + DefaultMemberAccessRights: domainmodel.MemberAccessRightsReadWrite, + }, + wantRead: true, + wantWrite: true, + }, + { + name: "explicit member ReadOnly", + rule: &domainmodel.AccessRule{ + MemberAccesses: []*domainmodel.MemberAccess{ + {AttributeName: "Name", AccessRights: domainmodel.MemberAccessRightsReadOnly}, + }, + }, + wantRead: true, + wantWrite: false, + }, + { + name: "explicit member ReadWrite", + rule: &domainmodel.AccessRule{ + MemberAccesses: []*domainmodel.MemberAccess{ + {AttributeName: "Name", AccessRights: domainmodel.MemberAccessRightsReadWrite}, + }, + }, + wantRead: true, + wantWrite: true, + }, + { + name: "mixed members — one ReadOnly one None", + rule: &domainmodel.AccessRule{ + MemberAccesses: []*domainmodel.MemberAccess{ + {AttributeName: "Name", AccessRights: domainmodel.MemberAccessRightsReadOnly}, + {AttributeName: "Age", AccessRights: domainmodel.MemberAccessRightsNone}, + }, + }, + wantRead: true, + wantWrite: false, + }, + { + name: "mixed members — one ReadOnly one ReadWrite", + rule: &domainmodel.AccessRule{ + MemberAccesses: []*domainmodel.MemberAccess{ + {AttributeName: "Name", AccessRights: domainmodel.MemberAccessRightsReadOnly}, + {AttributeName: "Age", AccessRights: domainmodel.MemberAccessRightsReadWrite}, + }, + }, + wantRead: true, + wantWrite: true, + }, + { + name: "all members None", + rule: &domainmodel.AccessRule{ + MemberAccesses: []*domainmodel.MemberAccess{ + {AttributeName: "Name", AccessRights: domainmodel.MemberAccessRightsNone}, + }, + }, + wantRead: false, + wantWrite: false, + }, + { + name: "empty member accesses falls through to default", + rule: &domainmodel.AccessRule{ + MemberAccesses: []*domainmodel.MemberAccess{}, + DefaultMemberAccessRights: domainmodel.MemberAccessRightsReadOnly, + }, + wantRead: true, + wantWrite: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotRead, gotWrite := entityAccessFromMemberRights(tt.rule) + if gotRead != tt.wantRead { + t.Errorf("hasRead = %v, want %v", gotRead, tt.wantRead) + } + if gotWrite != tt.wantWrite { + t.Errorf("hasWrite = %v, want %v", gotWrite, tt.wantWrite) + } + }) + } +} diff --git a/mdl/catalog/builder_workflows_test.go b/mdl/catalog/builder_workflows_test.go new file mode 100644 index 00000000..5dcb3cd6 --- /dev/null +++ b/mdl/catalog/builder_workflows_test.go @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 + +package catalog + +import ( + "testing" + + "github.com/mendixlabs/mxcli/sdk/workflows" +) + +func TestCountWorkflowActivityTypes(t *testing.T) { + tests := []struct { + name string + wf *workflows.Workflow + wantTotal, wantUT, wantMF, wantDec int + }{ + { + name: "nil flow", + wf: &workflows.Workflow{}, + wantTotal: 0, + }, + { + name: "empty flow", + wf: &workflows.Workflow{ + Flow: &workflows.Flow{}, + }, + wantTotal: 0, + }, + { + name: "user task", + wf: &workflows.Workflow{ + Flow: &workflows.Flow{ + Activities: []workflows.WorkflowActivity{ + &workflows.UserTask{}, + }, + }, + }, + wantTotal: 1, + wantUT: 1, + }, + { + name: "call microflow task", + wf: &workflows.Workflow{ + Flow: &workflows.Flow{ + Activities: []workflows.WorkflowActivity{ + &workflows.CallMicroflowTask{}, + }, + }, + }, + wantTotal: 1, + wantMF: 1, + }, + { + name: "system task counts as microflow call", + wf: &workflows.Workflow{ + Flow: &workflows.Flow{ + Activities: []workflows.WorkflowActivity{ + &workflows.SystemTask{}, + }, + }, + }, + wantTotal: 1, + wantMF: 1, + }, + { + name: "exclusive split counts as decision", + wf: &workflows.Workflow{ + Flow: &workflows.Flow{ + Activities: []workflows.WorkflowActivity{ + &workflows.ExclusiveSplitActivity{}, + }, + }, + }, + wantTotal: 1, + wantDec: 1, + }, + { + name: "nested activities in user task outcomes", + wf: &workflows.Workflow{ + Flow: &workflows.Flow{ + Activities: []workflows.WorkflowActivity{ + &workflows.UserTask{ + Outcomes: []*workflows.UserTaskOutcome{ + { + Flow: &workflows.Flow{ + Activities: []workflows.WorkflowActivity{ + &workflows.CallMicroflowTask{}, + }, + }, + }, + }, + }, + }, + }, + }, + wantTotal: 2, + wantUT: 1, + wantMF: 1, + }, + { + name: "parallel split recurses into outcomes", + wf: &workflows.Workflow{ + Flow: &workflows.Flow{ + Activities: []workflows.WorkflowActivity{ + &workflows.ParallelSplitActivity{ + Outcomes: []*workflows.ParallelSplitOutcome{ + { + Flow: &workflows.Flow{ + Activities: []workflows.WorkflowActivity{ + &workflows.UserTask{}, + }, + }, + }, + { + Flow: &workflows.Flow{ + Activities: []workflows.WorkflowActivity{ + &workflows.ExclusiveSplitActivity{}, + }, + }, + }, + }, + }, + }, + }, + }, + wantTotal: 3, // parallel split + user task + decision + wantUT: 1, + wantDec: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + total, ut, mf, dec := countWorkflowActivityTypes(tt.wf) + if total != tt.wantTotal { + t.Errorf("total = %d, want %d", total, tt.wantTotal) + } + if ut != tt.wantUT { + t.Errorf("userTasks = %d, want %d", ut, tt.wantUT) + } + if mf != tt.wantMF { + t.Errorf("microflowCalls = %d, want %d", mf, tt.wantMF) + } + if dec != tt.wantDec { + t.Errorf("decisions = %d, want %d", dec, tt.wantDec) + } + }) + } +}