Skip to content

Commit 66f637a

Browse files
committed
fix: mock defaults, nil guards, nav spec aliasing, field-count drift tests
1 parent 748d51b commit 66f637a

File tree

8 files changed

+82
-56
lines changed

8 files changed

+82
-56
lines changed

mdl/backend/mock/mock_mutation.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
package mock
44

55
import (
6+
"fmt"
7+
68
"github.com/mendixlabs/mxcli/mdl/backend"
79
"github.com/mendixlabs/mxcli/model"
810
"github.com/mendixlabs/mxcli/sdk/pages"
@@ -17,7 +19,7 @@ func (m *MockBackend) OpenPageForMutation(unitID model.ID) (backend.PageMutator,
1719
if m.OpenPageForMutationFunc != nil {
1820
return m.OpenPageForMutationFunc(unitID)
1921
}
20-
return nil, nil
22+
return nil, fmt.Errorf("MockBackend.OpenPageForMutation not configured")
2123
}
2224

2325
// ---------------------------------------------------------------------------
@@ -28,7 +30,7 @@ func (m *MockBackend) OpenWorkflowForMutation(unitID model.ID) (backend.Workflow
2830
if m.OpenWorkflowForMutationFunc != nil {
2931
return m.OpenWorkflowForMutationFunc(unitID)
3032
}
31-
return nil, nil
33+
return nil, fmt.Errorf("MockBackend.OpenWorkflowForMutation not configured")
3234
}
3335

3436
// ---------------------------------------------------------------------------

mdl/backend/mpr/convert.go

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -263,36 +263,10 @@ func convertNavMenuItem(in *mpr.NavMenuItem) *types.NavMenuItem {
263263
// Conversion helpers: mdl/types -> sdk/mpr (for write methods)
264264
// ---------------------------------------------------------------------------
265265

266+
// unconvertNavProfileSpec is now a pass-through since mpr.NavigationProfileSpec
267+
// is aliased to types.NavigationProfileSpec.
266268
func unconvertNavProfileSpec(s types.NavigationProfileSpec) mpr.NavigationProfileSpec {
267-
out := mpr.NavigationProfileSpec{
268-
LoginPage: s.LoginPage,
269-
NotFoundPage: s.NotFoundPage,
270-
HasMenu: s.HasMenu,
271-
}
272-
if s.HomePages != nil {
273-
out.HomePages = make([]mpr.NavHomePageSpec, len(s.HomePages))
274-
for i, hp := range s.HomePages {
275-
out.HomePages[i] = mpr.NavHomePageSpec{IsPage: hp.IsPage, Target: hp.Target, ForRole: hp.ForRole}
276-
}
277-
}
278-
if s.MenuItems != nil {
279-
out.MenuItems = make([]mpr.NavMenuItemSpec, len(s.MenuItems))
280-
for i, mi := range s.MenuItems {
281-
out.MenuItems[i] = unconvertNavMenuItemSpec(mi)
282-
}
283-
}
284-
return out
285-
}
286-
287-
func unconvertNavMenuItemSpec(in types.NavMenuItemSpec) mpr.NavMenuItemSpec {
288-
out := mpr.NavMenuItemSpec{Caption: in.Caption, Page: in.Page, Microflow: in.Microflow}
289-
if in.Items != nil {
290-
out.Items = make([]mpr.NavMenuItemSpec, len(in.Items))
291-
for i, sub := range in.Items {
292-
out.Items[i] = unconvertNavMenuItemSpec(sub)
293-
}
294-
}
295-
return out
269+
return s
296270
}
297271

298272
func unconvertEntityMemberAccessSlice(in []types.EntityMemberAccess) []mpr.EntityMemberAccess {

mdl/backend/mpr/convert_roundtrip_test.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package mprbackend
77

88
import (
99
"errors"
10+
"reflect"
1011
"testing"
1112

1213
"github.com/mendixlabs/mxcli/mdl/types"
@@ -492,7 +493,9 @@ func TestUnconvertNavMenuItemSpec_Isolated(t *testing.T) {
492493
{Caption: "Child", Microflow: "MF2"},
493494
},
494495
}
495-
out := unconvertNavMenuItemSpec(in)
496+
// Since mpr.NavMenuItemSpec is aliased to types.NavMenuItemSpec,
497+
// unconvert is now a pass-through. Verify the alias holds.
498+
var out mpr.NavMenuItemSpec = in
496499
if out.Caption != "Parent" || out.Page != "Page1" || out.Microflow != "MF1" {
497500
t.Errorf("field mismatch: %+v", out)
498501
}
@@ -503,7 +506,7 @@ func TestUnconvertNavMenuItemSpec_Isolated(t *testing.T) {
503506

504507
func TestUnconvertNavMenuItemSpec_NilItems(t *testing.T) {
505508
in := types.NavMenuItemSpec{Caption: "Leaf"}
506-
out := unconvertNavMenuItemSpec(in)
509+
var out mpr.NavMenuItemSpec = in
507510
if out.Items != nil {
508511
t.Errorf("expected nil Items for leaf: %+v", out.Items)
509512
}
@@ -598,3 +601,51 @@ func TestUnconvertImageCollection(t *testing.T) {
598601
}
599602
}
600603

604+
// ============================================================================
605+
// Field-count drift assertions
606+
// ============================================================================
607+
//
608+
// These tests catch silent field drift: if a struct gains a new field but
609+
// the convert/unconvert function is not updated, the test fails.
610+
611+
func assertFieldCount(t *testing.T, name string, v any, expected int) {
612+
t.Helper()
613+
actual := reflect.TypeOf(v).NumField()
614+
if actual != expected {
615+
t.Errorf("%s field count changed: expected %d, got %d — update convert.go and this test", name, expected, actual)
616+
}
617+
}
618+
619+
func TestFieldCountDrift(t *testing.T) {
620+
// mpr → types pairs (manually copied in convert.go).
621+
// If a struct gains a field, update the convert function AND this count.
622+
assertFieldCount(t, "mpr.FolderInfo", mpr.FolderInfo{}, 3)
623+
assertFieldCount(t, "types.FolderInfo", types.FolderInfo{}, 3)
624+
assertFieldCount(t, "mpr.UnitInfo", mpr.UnitInfo{}, 4)
625+
assertFieldCount(t, "types.UnitInfo", types.UnitInfo{}, 4)
626+
assertFieldCount(t, "mpr.RenameHit", mpr.RenameHit{}, 4)
627+
assertFieldCount(t, "types.RenameHit", types.RenameHit{}, 4)
628+
assertFieldCount(t, "mpr.RawUnit", mpr.RawUnit{}, 4)
629+
assertFieldCount(t, "types.RawUnit", types.RawUnit{}, 4)
630+
assertFieldCount(t, "mpr.RawUnitInfo", mpr.RawUnitInfo{}, 5)
631+
assertFieldCount(t, "types.RawUnitInfo", types.RawUnitInfo{}, 5)
632+
assertFieldCount(t, "mpr.RawCustomWidgetType", mpr.RawCustomWidgetType{}, 6)
633+
assertFieldCount(t, "types.RawCustomWidgetType", types.RawCustomWidgetType{}, 6)
634+
assertFieldCount(t, "mpr.JavaAction", mpr.JavaAction{}, 4)
635+
assertFieldCount(t, "types.JavaAction", types.JavaAction{}, 4)
636+
assertFieldCount(t, "mpr.JavaScriptAction", mpr.JavaScriptAction{}, 12)
637+
assertFieldCount(t, "types.JavaScriptAction", types.JavaScriptAction{}, 12)
638+
assertFieldCount(t, "mpr.NavigationDocument", mpr.NavigationDocument{}, 4)
639+
assertFieldCount(t, "types.NavigationDocument", types.NavigationDocument{}, 4)
640+
assertFieldCount(t, "mpr.JsonStructure", mpr.JsonStructure{}, 8)
641+
assertFieldCount(t, "types.JsonStructure", types.JsonStructure{}, 8)
642+
assertFieldCount(t, "mpr.JsonElement", mpr.JsonElement{}, 14)
643+
assertFieldCount(t, "types.JsonElement", types.JsonElement{}, 14)
644+
assertFieldCount(t, "mpr.ImageCollection", mpr.ImageCollection{}, 6)
645+
assertFieldCount(t, "types.ImageCollection", types.ImageCollection{}, 6)
646+
assertFieldCount(t, "mpr.EntityMemberAccess", mpr.EntityMemberAccess{}, 3)
647+
assertFieldCount(t, "types.EntityMemberAccess", types.EntityMemberAccess{}, 3)
648+
assertFieldCount(t, "mpr.EntityAccessRevocation", mpr.EntityAccessRevocation{}, 6)
649+
assertFieldCount(t, "types.EntityAccessRevocation", types.EntityAccessRevocation{}, 6)
650+
}
651+

mdl/backend/mpr/page_mutator.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"go.mongodb.org/mongo-driver/bson"
1010
"go.mongodb.org/mongo-driver/bson/primitive"
1111

12+
mdlerrors "github.com/mendixlabs/mxcli/mdl/errors"
13+
1214
"github.com/mendixlabs/mxcli/mdl/backend"
1315
"github.com/mendixlabs/mxcli/mdl/bsonutil"
1416
"github.com/mendixlabs/mxcli/mdl/types"
@@ -580,10 +582,14 @@ func dSetArray(doc bson.D, key string, elements []any) {
580582

581583
// extractBinaryIDFromDoc extracts a binary ID string from a bson.D field.
582584
func extractBinaryIDFromDoc(val any) string {
583-
if bin, ok := val.(primitive.Binary); ok {
585+
switch bin := val.(type) {
586+
case primitive.Binary:
584587
return types.BlobToUUID(bin.Data)
588+
case []byte:
589+
return types.BlobToUUID(bin)
590+
default:
591+
return ""
585592
}
586-
return ""
587593
}
588594

589595
// ---------------------------------------------------------------------------
@@ -1305,8 +1311,7 @@ func setRawWidgetPropertyMut(widget bson.D, propName string, value any) error {
13051311
func setWidgetCaptionMut(widget bson.D, value any) error {
13061312
caption := dGetDoc(widget, "Caption")
13071313
if caption == nil {
1308-
setTranslatableText(widget, "Caption", value)
1309-
return nil
1314+
return mdlerrors.NewValidation("widget has no Caption property")
13101315
}
13111316
setTranslatableText(caption, "", value)
13121317
return nil

mdl/executor/cmd_diff_local.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,9 @@ func attributeBsonToMDL(_ *ExecContext, raw map[string]any) string {
497497
func microflowBsonToMDL(ctx *ExecContext, raw map[string]any, qualifiedName string) string {
498498
qn := splitQualifiedName(qualifiedName)
499499
mf := ctx.Backend.ParseMicroflowFromRaw(raw, model.ID(qn.Name), "")
500+
if mf == nil {
501+
return fmt.Sprintf("MICROFLOW %s\n -- parse failed --\nEND MICROFLOW\n", qualifiedName)
502+
}
500503

501504
entityNames, microflowNames := buildNameLookups(ctx)
502505
return renderMicroflowMDL(ctx, mf, qn, entityNames, microflowNames, nil)

mdl/executor/cmd_pages_builder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func (pb *pageBuilder) initPluggableEngine() {
7474
// registerWidgetName registers a widget name and returns an error if it's already used.
7575
// Widget names must be unique within a page/snippet.
7676

77-
// getProjectPath returns the project directory path from the underlying reader.
77+
// getProjectPath returns the project directory path from the backend.
7878
func (pb *pageBuilder) getProjectPath() string {
7979
if pb.backend != nil {
8080
return pb.backend.Path()

mdl/executor/cmd_widgets.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ func updateWidgetsInContainer(ctx *ExecContext, containerID string, widgetRefs [
209209
if err != nil {
210210
return 0, mdlerrors.NewBackend(fmt.Sprintf("open %s for mutation", containerName), err)
211211
}
212+
if mutator == nil {
213+
return 0, mdlerrors.NewBackend(fmt.Sprintf("open %s for mutation", containerName),
214+
fmt.Errorf("backend returned nil mutator for %s", containerID))
215+
}
212216

213217
updated := 0
214218
for _, ref := range widgetRefs {

sdk/mpr/writer_navigation.go

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,21 @@ import (
66
"fmt"
77
"strings"
88

9+
"github.com/mendixlabs/mxcli/mdl/types"
910
"github.com/mendixlabs/mxcli/model"
1011

1112
"go.mongodb.org/mongo-driver/bson"
1213
)
1314

1415
// NavigationProfileSpec describes the desired state for a navigation profile.
15-
type NavigationProfileSpec struct {
16-
HomePages []NavHomePageSpec
17-
LoginPage string // qualified page name, empty to clear
18-
NotFoundPage string // qualified page name, empty to clear
19-
MenuItems []NavMenuItemSpec
20-
HasMenu bool // true = replace menu (even if empty)
21-
}
16+
// Aliased from mdl/types to avoid duplicate definitions.
17+
type NavigationProfileSpec = types.NavigationProfileSpec
2218

2319
// NavHomePageSpec describes a home page entry.
24-
type NavHomePageSpec struct {
25-
IsPage bool
26-
Target string // qualified name
27-
ForRole string // empty for default
28-
}
20+
type NavHomePageSpec = types.NavHomePageSpec
2921

3022
// NavMenuItemSpec describes a menu item.
31-
type NavMenuItemSpec struct {
32-
Caption string
33-
Page string
34-
Microflow string
35-
Items []NavMenuItemSpec
36-
}
23+
type NavMenuItemSpec = types.NavMenuItemSpec
3724

3825
// UpdateNavigationProfile patches a navigation profile's home pages, login page, and menu.
3926
func (w *Writer) UpdateNavigationProfile(navDocID model.ID, profileName string, spec NavigationProfileSpec) error {

0 commit comments

Comments
 (0)