From fc7ef35fa7a122bd55ace504ff7980953bc4ce51 Mon Sep 17 00:00:00 2001 From: Andrew Vasilyev Date: Tue, 21 Apr 2026 13:45:28 +0200 Subject: [PATCH] test: add mock tests for 8 executor handler files Add 49 tests across 8 new test files covering executor handlers that previously had zero test coverage: - cmd_folders: 9 tests (DropFolder + MoveFolder paths) - cmd_move: 6 tests (page moves, cross-module refs, error paths) - cmd_rename: 11 tests (entity/module/microflow/page/enum/assoc/const/nanoflow) - cmd_alter_page: 9 tests (set property, snippets, widgets, variables, layout) - cmd_features: 7 tests (ForVersion, AddedSince, connected, invalid version) - cmd_styling: 5 tests (not-connected guards for all 3 styling commands) - cmd_lint: 1 test (not-connected guard) - cmd_import: 1 test (not-connected guard) Styling, lint, and import have partial coverage (not-connected + error paths only) because their happy paths depend on filesystem or external DB connections that cannot be mocked through the current Backend interface. --- mdl/executor/cmd_alter_page_mock_test.go | 310 ++++++++++++++++++++++ mdl/executor/cmd_features_mock_test.go | 107 ++++++++ mdl/executor/cmd_folders_mock_test.go | 190 ++++++++++++++ mdl/executor/cmd_import_mock_test.go | 35 +++ mdl/executor/cmd_lint_mock_test.go | 58 +++++ mdl/executor/cmd_move_mock_test.go | 173 +++++++++++++ mdl/executor/cmd_rename_mock_test.go | 312 +++++++++++++++++++++++ mdl/executor/cmd_styling_mock_test.go | 85 ++++++ 8 files changed, 1270 insertions(+) create mode 100644 mdl/executor/cmd_alter_page_mock_test.go create mode 100644 mdl/executor/cmd_features_mock_test.go create mode 100644 mdl/executor/cmd_folders_mock_test.go create mode 100644 mdl/executor/cmd_import_mock_test.go create mode 100644 mdl/executor/cmd_lint_mock_test.go create mode 100644 mdl/executor/cmd_move_mock_test.go create mode 100644 mdl/executor/cmd_rename_mock_test.go create mode 100644 mdl/executor/cmd_styling_mock_test.go diff --git a/mdl/executor/cmd_alter_page_mock_test.go b/mdl/executor/cmd_alter_page_mock_test.go new file mode 100644 index 00000000..c6fa60ee --- /dev/null +++ b/mdl/executor/cmd_alter_page_mock_test.go @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "fmt" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// --------------------------------------------------------------------------- +// Not connected +// --------------------------------------------------------------------------- + +func TestAlterPage_NotConnected(t *testing.T) { + mb := &mock.MockBackend{IsConnectedFunc: func() bool { return false }} + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execAlterPage(ctx, &ast.AlterPageStmt{ + PageName: ast.QualifiedName{Module: "M", Name: "P"}, + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not connected") +} + +// --------------------------------------------------------------------------- +// Page not found +// --------------------------------------------------------------------------- + +func TestAlterPage_PageNotFound(t *testing.T) { + mod := mkModule("MyModule") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return nil, nil }, + } + h := mkHierarchy(mod) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execAlterPage(ctx, &ast.AlterPageStmt{ + PageName: ast.QualifiedName{Module: "MyModule", Name: "Missing"}, + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not found") +} + +// --------------------------------------------------------------------------- +// Page happy path — SET property + Save +// --------------------------------------------------------------------------- + +func TestAlterPage_SetProperty_Success(t *testing.T) { + mod := mkModule("MyModule") + pg := mkPage(mod.ID, "TestPage") + saved := false + setPropCalled := false + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return []*pages.Page{pg}, nil }, + OpenPageForMutationFunc: func(unitID model.ID) (backend.PageMutator, error) { + return &mock.MockPageMutator{ + SetWidgetPropertyFunc: func(widgetRef string, prop string, value any) error { + setPropCalled = true + if widgetRef != "myWidget" { + t.Errorf("expected widgetRef myWidget, got %s", widgetRef) + } + if prop != "Caption" { + t.Errorf("expected prop Caption, got %s", prop) + } + return nil + }, + SaveFunc: func() error { saved = true; return nil }, + }, nil + }, + } + h := mkHierarchy(mod) + withContainer(h, pg.ContainerID, mod.ID) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execAlterPage(ctx, &ast.AlterPageStmt{ + PageName: ast.QualifiedName{Module: "MyModule", Name: "TestPage"}, + Operations: []ast.AlterPageOperation{ + &ast.SetPropertyOp{ + Target: ast.WidgetRef{Widget: "myWidget"}, + Properties: map[string]any{"Caption": "Hello"}, + }, + }, + })) + if !setPropCalled { + t.Error("expected SetWidgetProperty to be called") + } + if !saved { + t.Error("expected Save to be called") + } + assertContainsStr(t, buf.String(), "Altered page") + assertContainsStr(t, buf.String(), "MyModule.TestPage") +} + +// --------------------------------------------------------------------------- +// Snippet happy path +// --------------------------------------------------------------------------- + +func TestAlterPage_Snippet_Success(t *testing.T) { + mod := mkModule("MyModule") + snp := mkSnippet(mod.ID, "TestSnippet") + saved := false + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListSnippetsFunc: func() ([]*pages.Snippet, error) { + return []*pages.Snippet{snp}, nil + }, + OpenPageForMutationFunc: func(unitID model.ID) (backend.PageMutator, error) { + return &mock.MockPageMutator{ + SaveFunc: func() error { saved = true; return nil }, + }, nil + }, + } + h := mkHierarchy(mod) + withContainer(h, snp.ContainerID, mod.ID) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execAlterPage(ctx, &ast.AlterPageStmt{ + ContainerType: "snippet", + PageName: ast.QualifiedName{Module: "MyModule", Name: "TestSnippet"}, + })) + if !saved { + t.Error("expected Save to be called") + } + assertContainsStr(t, buf.String(), "Altered snippet") +} + +// --------------------------------------------------------------------------- +// Open mutator error +// --------------------------------------------------------------------------- + +func TestAlterPage_OpenMutatorError(t *testing.T) { + mod := mkModule("MyModule") + pg := mkPage(mod.ID, "TestPage") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return []*pages.Page{pg}, nil }, + OpenPageForMutationFunc: func(unitID model.ID) (backend.PageMutator, error) { + return nil, fmt.Errorf("lock error") + }, + } + h := mkHierarchy(mod) + withContainer(h, pg.ContainerID, mod.ID) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execAlterPage(ctx, &ast.AlterPageStmt{ + PageName: ast.QualifiedName{Module: "MyModule", Name: "TestPage"}, + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "open page") +} + +// --------------------------------------------------------------------------- +// Save error +// --------------------------------------------------------------------------- + +func TestAlterPage_SaveError(t *testing.T) { + mod := mkModule("MyModule") + pg := mkPage(mod.ID, "TestPage") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return []*pages.Page{pg}, nil }, + OpenPageForMutationFunc: func(unitID model.ID) (backend.PageMutator, error) { + return &mock.MockPageMutator{ + SaveFunc: func() error { return fmt.Errorf("disk full") }, + }, nil + }, + } + h := mkHierarchy(mod) + withContainer(h, pg.ContainerID, mod.ID) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execAlterPage(ctx, &ast.AlterPageStmt{ + PageName: ast.QualifiedName{Module: "MyModule", Name: "TestPage"}, + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "save") +} + +// --------------------------------------------------------------------------- +// DROP widget via mutator +// --------------------------------------------------------------------------- + +func TestAlterPage_DropWidget_Success(t *testing.T) { + mod := mkModule("MyModule") + pg := mkPage(mod.ID, "TestPage") + dropCalled := false + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return []*pages.Page{pg}, nil }, + OpenPageForMutationFunc: func(unitID model.ID) (backend.PageMutator, error) { + return &mock.MockPageMutator{ + DropWidgetFunc: func(refs []backend.WidgetRef) error { + dropCalled = true + if len(refs) != 1 || refs[0].Widget != "oldWidget" { + t.Errorf("unexpected refs: %v", refs) + } + return nil + }, + SaveFunc: func() error { return nil }, + }, nil + }, + } + h := mkHierarchy(mod) + withContainer(h, pg.ContainerID, mod.ID) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execAlterPage(ctx, &ast.AlterPageStmt{ + PageName: ast.QualifiedName{Module: "MyModule", Name: "TestPage"}, + Operations: []ast.AlterPageOperation{ + &ast.DropWidgetOp{ + Targets: []ast.WidgetRef{{Widget: "oldWidget"}}, + }, + }, + })) + if !dropCalled { + t.Error("expected DropWidget to be called") + } + assertContainsStr(t, buf.String(), "Altered page") +} + +// --------------------------------------------------------------------------- +// ADD VARIABLE +// --------------------------------------------------------------------------- + +func TestAlterPage_AddVariable_Success(t *testing.T) { + mod := mkModule("MyModule") + pg := mkPage(mod.ID, "TestPage") + addVarCalled := false + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return []*pages.Page{pg}, nil }, + OpenPageForMutationFunc: func(unitID model.ID) (backend.PageMutator, error) { + return &mock.MockPageMutator{ + AddVariableFunc: func(name, dataType, defaultValue string) error { + addVarCalled = true + if name != "MyVar" || dataType != "String" || defaultValue != "hello" { + t.Errorf("unexpected variable: %s %s %s", name, dataType, defaultValue) + } + return nil + }, + SaveFunc: func() error { return nil }, + }, nil + }, + } + h := mkHierarchy(mod) + withContainer(h, pg.ContainerID, mod.ID) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execAlterPage(ctx, &ast.AlterPageStmt{ + PageName: ast.QualifiedName{Module: "MyModule", Name: "TestPage"}, + Operations: []ast.AlterPageOperation{ + &ast.AddVariableOp{ + Variable: ast.PageVariable{Name: "MyVar", DataType: "String", DefaultValue: "hello"}, + }, + }, + })) + if !addVarCalled { + t.Error("expected AddVariable to be called") + } + assertContainsStr(t, buf.String(), "Altered page") +} + +// --------------------------------------------------------------------------- +// SET Layout on snippet — unsupported +// --------------------------------------------------------------------------- + +func TestAlterPage_SetLayout_Snippet_Unsupported(t *testing.T) { + mod := mkModule("MyModule") + snp := mkSnippet(mod.ID, "TestSnippet") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListSnippetsFunc: func() ([]*pages.Snippet, error) { + return []*pages.Snippet{snp}, nil + }, + OpenPageForMutationFunc: func(unitID model.ID) (backend.PageMutator, error) { + return &mock.MockPageMutator{}, nil + }, + } + h := mkHierarchy(mod) + withContainer(h, snp.ContainerID, mod.ID) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execAlterPage(ctx, &ast.AlterPageStmt{ + ContainerType: "snippet", + PageName: ast.QualifiedName{Module: "MyModule", Name: "TestSnippet"}, + Operations: []ast.AlterPageOperation{ + &ast.SetLayoutOp{ + NewLayout: ast.QualifiedName{Module: "M", Name: "L"}, + }, + }, + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not supported") +} diff --git a/mdl/executor/cmd_features_mock_test.go b/mdl/executor/cmd_features_mock_test.go new file mode 100644 index 00000000..afc11bc0 --- /dev/null +++ b/mdl/executor/cmd_features_mock_test.go @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" +) + +// --------------------------------------------------------------------------- +// execShowFeatures +// --------------------------------------------------------------------------- + +func TestShowFeatures_NotConnected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execShowFeatures(ctx, &ast.ShowFeaturesStmt{}) + assertError(t, err) + assertContainsStr(t, err.Error(), "not connected") +} + +func TestShowFeatures_ForVersion(t *testing.T) { + // ForVersion doesn't require connection — uses embedded registry directly. + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, buf := newMockCtx(t, withBackend(mb)) + err := execShowFeatures(ctx, &ast.ShowFeaturesStmt{ForVersion: "10.0"}) + assertNoError(t, err) + out := buf.String() + if len(out) == 0 { + t.Fatal("expected output, got empty") + } + assertContainsStr(t, out, "Features for Mendix") +} + +func TestShowFeatures_ForVersion_InvalidVersion(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execShowFeatures(ctx, &ast.ShowFeaturesStmt{ForVersion: "not-a-version"}) + assertError(t, err) + assertContainsStr(t, err.Error(), "invalid version") +} + +func TestShowFeatures_AddedSince(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, buf := newMockCtx(t, withBackend(mb)) + err := execShowFeatures(ctx, &ast.ShowFeaturesStmt{AddedSince: "10.0"}) + assertNoError(t, err) + out := buf.String() + if len(out) == 0 { + t.Fatal("expected output, got empty") + } + assertContainsStr(t, out, "Features added since Mendix") +} + +func TestShowFeatures_AddedSince_InvalidVersion(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execShowFeatures(ctx, &ast.ShowFeaturesStmt{AddedSince: "xyz"}) + assertError(t, err) + assertContainsStr(t, err.Error(), "invalid version") +} + +func TestShowFeatures_Connected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ProjectVersionFunc: func() *types.ProjectVersion { + return &types.ProjectVersion{ + ProductVersion: "10.6.0", + MajorVersion: 10, + MinorVersion: 6, + PatchVersion: 0, + } + }, + } + ctx, buf := newMockCtx(t, withBackend(mb)) + err := execShowFeatures(ctx, &ast.ShowFeaturesStmt{}) + assertNoError(t, err) + out := buf.String() + if len(out) == 0 { + t.Fatal("expected output, got empty") + } + assertContainsStr(t, out, "Features for Mendix") +} + +func TestShowFeatures_InArea_ForVersion(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, buf := newMockCtx(t, withBackend(mb)) + err := execShowFeatures(ctx, &ast.ShowFeaturesStmt{ForVersion: "10.6", InArea: "domain_model"}) + assertNoError(t, err) + // Area filter narrows output; assert header contains area name. + assertContainsStr(t, buf.String(), "domain_model") +} diff --git a/mdl/executor/cmd_folders_mock_test.go b/mdl/executor/cmd_folders_mock_test.go new file mode 100644 index 00000000..c355ee29 --- /dev/null +++ b/mdl/executor/cmd_folders_mock_test.go @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "fmt" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" +) + +// --------------------------------------------------------------------------- +// execDropFolder +// --------------------------------------------------------------------------- + +func TestDropFolder_NotConnected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execDropFolder(ctx, &ast.DropFolderStmt{FolderPath: "Resources", Module: "MyModule"}) + assertError(t, err) + assertContainsStr(t, err.Error(), "not connected") +} + +func TestDropFolder_ModuleNotFound(t *testing.T) { + mod := mkModule("OtherModule") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + } + h := mkHierarchy(mod) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execDropFolder(ctx, &ast.DropFolderStmt{FolderPath: "Resources", Module: "MyModule"}) + assertError(t, err) + assertContainsStr(t, err.Error(), "not found") +} + +func TestDropFolder_FolderNotFound(t *testing.T) { + mod := mkModule("MyModule") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + } + h := mkHierarchy(mod) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execDropFolder(ctx, &ast.DropFolderStmt{FolderPath: "NonExistent", Module: "MyModule"}) + assertError(t, err) + assertContainsStr(t, err.Error(), "not found") +} + +func TestDropFolder_Success(t *testing.T) { + mod := mkModule("MyModule") + folderID := nextID("folder") + folders := []*types.FolderInfo{ + {ID: folderID, ContainerID: mod.ID, Name: "Resources"}, + } + deleteCalled := false + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return folders, nil }, + DeleteFolderFunc: func(id model.ID) error { deleteCalled = true; return nil }, + } + h := mkHierarchy(mod) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execDropFolder(ctx, &ast.DropFolderStmt{FolderPath: "Resources", Module: "MyModule"})) + if !deleteCalled { + t.Error("expected DeleteFolder to be called") + } + assertContainsStr(t, buf.String(), "Dropped folder") + assertContainsStr(t, buf.String(), "Resources") +} + +func TestDropFolder_BackendError(t *testing.T) { + mod := mkModule("MyModule") + folderID := nextID("folder") + folders := []*types.FolderInfo{ + {ID: folderID, ContainerID: mod.ID, Name: "Resources"}, + } + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return folders, nil }, + DeleteFolderFunc: func(id model.ID) error { return fmt.Errorf("folder not empty") }, + } + h := mkHierarchy(mod) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execDropFolder(ctx, &ast.DropFolderStmt{FolderPath: "Resources", Module: "MyModule"}) + assertError(t, err) + assertContainsStr(t, err.Error(), "delete folder") +} + +func TestDropFolder_NestedPath(t *testing.T) { + mod := mkModule("MyModule") + parentFolderID := nextID("folder") + childFolderID := nextID("folder") + folders := []*types.FolderInfo{ + {ID: parentFolderID, ContainerID: mod.ID, Name: "Resources"}, + {ID: childFolderID, ContainerID: parentFolderID, Name: "Images"}, + } + var deletedID model.ID + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return folders, nil }, + DeleteFolderFunc: func(id model.ID) error { deletedID = id; return nil }, + } + h := mkHierarchy(mod) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execDropFolder(ctx, &ast.DropFolderStmt{FolderPath: "Resources/Images", Module: "MyModule"})) + if deletedID != childFolderID { + t.Errorf("expected to delete child folder %s, got %s", childFolderID, deletedID) + } + assertContainsStr(t, buf.String(), "Dropped folder") +} + +// --------------------------------------------------------------------------- +// execMoveFolder +// --------------------------------------------------------------------------- + +func TestMoveFolder_NotConnected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execMoveFolder(ctx, &ast.MoveFolderStmt{ + Name: ast.QualifiedName{Module: "MyModule", Name: "Resources"}, + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not connected") +} + +func TestMoveFolder_ToModule(t *testing.T) { + srcMod := mkModule("MyModule") + dstMod := mkModule("OtherModule") + folderID := nextID("folder") + folders := []*types.FolderInfo{ + {ID: folderID, ContainerID: srcMod.ID, Name: "Resources"}, + } + var movedTo model.ID + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{srcMod, dstMod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return folders, nil }, + MoveFolderFunc: func(id, target model.ID) error { movedTo = target; return nil }, + } + h := mkHierarchy(srcMod, dstMod) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execMoveFolder(ctx, &ast.MoveFolderStmt{ + Name: ast.QualifiedName{Module: "MyModule", Name: "Resources"}, + TargetModule: "OtherModule", + })) + if movedTo != dstMod.ID { + t.Errorf("expected move to %s, got %s", dstMod.ID, movedTo) + } + assertContainsStr(t, buf.String(), "Moved folder") + assertContainsStr(t, buf.String(), "OtherModule") +} + +func TestMoveFolder_ToFolder(t *testing.T) { + mod := mkModule("MyModule") + srcFolderID := nextID("folder") + dstFolderID := nextID("folder") + folders := []*types.FolderInfo{ + {ID: srcFolderID, ContainerID: mod.ID, Name: "OldFolder"}, + {ID: dstFolderID, ContainerID: mod.ID, Name: "NewParent"}, + } + var movedTo model.ID + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return folders, nil }, + MoveFolderFunc: func(id, target model.ID) error { movedTo = target; return nil }, + } + h := mkHierarchy(mod) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execMoveFolder(ctx, &ast.MoveFolderStmt{ + Name: ast.QualifiedName{Module: "MyModule", Name: "OldFolder"}, + TargetFolder: "NewParent", + })) + if movedTo != dstFolderID { + t.Errorf("expected move to %s, got %s", dstFolderID, movedTo) + } + assertContainsStr(t, buf.String(), "Moved folder") +} diff --git a/mdl/executor/cmd_import_mock_test.go b/mdl/executor/cmd_import_mock_test.go new file mode 100644 index 00000000..452056aa --- /dev/null +++ b/mdl/executor/cmd_import_mock_test.go @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend/mock" +) + +// --------------------------------------------------------------------------- +// execImport +// +// NOTE: execImport depends on sqllib.Connection, ensureSQLManager, +// getOrAutoConnect, sqllib.ExecuteImport, and resolveImportLinks (which uses +// sqllib.LookupAssociationInfo with a live DB connection). The current mock +// infrastructure does not provide SQL connection/manager mocks, so only the +// not-connected guard can be exercised here. Expanding coverage requires +// building sqllib mock infrastructure (tracked separately). +// --------------------------------------------------------------------------- + +func TestImport_NotConnected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execImport(ctx, &ast.ImportStmt{ + SourceAlias: "mydb", + Query: "SELECT * FROM users", + TargetEntity: "MyModule.User", + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not connected") +} diff --git a/mdl/executor/cmd_lint_mock_test.go b/mdl/executor/cmd_lint_mock_test.go new file mode 100644 index 00000000..cbd51d9c --- /dev/null +++ b/mdl/executor/cmd_lint_mock_test.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend/mock" +) + +// --------------------------------------------------------------------------- +// execLint — not connected +// --------------------------------------------------------------------------- + +func TestLint_NotConnected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execLint(ctx, &ast.LintStmt{}) + assertError(t, err) + assertContainsStr(t, err.Error(), "not connected") +} + +// --------------------------------------------------------------------------- +// listLintRules — ShowRules happy path +// +// The ShowRules=true path calls listLintRules which only needs ctx.Output. +// It prints built-in rules (ID + Name + Description + Category + Severity). +// --------------------------------------------------------------------------- + +func TestLint_ShowRules(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + } + ctx, buf := newMockCtx(t, withBackend(mb)) + err := execLint(ctx, &ast.LintStmt{ShowRules: true}) + assertNoError(t, err) + out := buf.String() + if len(out) == 0 { + t.Fatal("expected output listing rules, got empty") + } + // listLintRules registers at least MPR001 (NamingConvention) + assertContainsStr(t, out, "MPR001") + assertContainsStr(t, out, "NamingConvention") + assertContainsStr(t, out, "Built-in rules:") +} + +// --------------------------------------------------------------------------- +// execLint — full lint path +// +// NOTE: The full lint path (ShowRules=false) requires ctx.executor, +// ctx.Catalog, buildCatalog, and the linter package pipeline. The current +// mock infrastructure does not expose executor or catalog mocks, so only +// the ShowRules branch can be exercised. Expanding coverage requires +// building executor/catalog mock support (tracked separately). +// --------------------------------------------------------------------------- diff --git a/mdl/executor/cmd_move_mock_test.go b/mdl/executor/cmd_move_mock_test.go new file mode 100644 index 00000000..7c1fc9ff --- /dev/null +++ b/mdl/executor/cmd_move_mock_test.go @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "fmt" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// --------------------------------------------------------------------------- +// execMove — not connected +// --------------------------------------------------------------------------- + +func TestMove_NotConnected(t *testing.T) { + mb := &mock.MockBackend{IsConnectedFunc: func() bool { return false }} + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execMove(ctx, &ast.MoveStmt{ + DocumentType: ast.DocumentTypePage, + Name: ast.QualifiedName{Module: "MyModule", Name: "MyPage"}, + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not connected") +} + +// --------------------------------------------------------------------------- +// execMove — page happy path +// --------------------------------------------------------------------------- + +func TestMove_Page_ToFolder(t *testing.T) { + mod := mkModule("MyModule") + pg := mkPage(mod.ID, "MyPage") + folderID := nextID("folder") + folders := []*types.FolderInfo{ + {ID: folderID, ContainerID: mod.ID, Name: "Admin"}, + } + var movedPage *pages.Page + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return folders, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return []*pages.Page{pg}, nil }, + MovePageFunc: func(p *pages.Page) error { movedPage = p; return nil }, + } + h := mkHierarchy(mod) + withContainer(h, pg.ContainerID, mod.ID) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execMove(ctx, &ast.MoveStmt{ + DocumentType: ast.DocumentTypePage, + Name: ast.QualifiedName{Module: "MyModule", Name: "MyPage"}, + Folder: "Admin", + })) + if movedPage == nil { + t.Fatal("Expected MovePage to be called") + } + if movedPage.ContainerID != folderID { + t.Errorf("Expected container %s, got %s", folderID, movedPage.ContainerID) + } + assertContainsStr(t, buf.String(), "Moved page") +} + +// --------------------------------------------------------------------------- +// execMove — page not found +// --------------------------------------------------------------------------- + +func TestMove_Page_NotFound(t *testing.T) { + mod := mkModule("MyModule") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return nil, nil }, + } + h := mkHierarchy(mod) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execMove(ctx, &ast.MoveStmt{ + DocumentType: ast.DocumentTypePage, + Name: ast.QualifiedName{Module: "MyModule", Name: "NonExistent"}, + Folder: "SomeFolder", + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not found") +} + +// --------------------------------------------------------------------------- +// execMove — cross-module move updates references +// --------------------------------------------------------------------------- + +func TestMove_Page_CrossModule(t *testing.T) { + srcMod := mkModule("SrcModule") + dstMod := mkModule("DstModule") + pg := mkPage(srcMod.ID, "MyPage") + var movedPage *pages.Page + refUpdated := false + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{srcMod, dstMod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return []*pages.Page{pg}, nil }, + MovePageFunc: func(p *pages.Page) error { movedPage = p; return nil }, + UpdateQualifiedNameInAllUnitsFunc: func(old, new string) (int, error) { + refUpdated = true + return 3, nil + }, + } + h := mkHierarchy(srcMod, dstMod) + withContainer(h, pg.ContainerID, srcMod.ID) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execMove(ctx, &ast.MoveStmt{ + DocumentType: ast.DocumentTypePage, + Name: ast.QualifiedName{Module: "SrcModule", Name: "MyPage"}, + TargetModule: "DstModule", + })) + if movedPage == nil { + t.Fatal("Expected MovePage to be called") + } + if !refUpdated { + t.Error("Expected reference update for cross-module move") + } + assertContainsStr(t, buf.String(), "Moved page") + assertContainsStr(t, buf.String(), "Updated references") +} + +// --------------------------------------------------------------------------- +// execMove — unsupported type +// --------------------------------------------------------------------------- + +func TestMove_UnsupportedType(t *testing.T) { + mod := mkModule("MyModule") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + } + h := mkHierarchy(mod) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execMove(ctx, &ast.MoveStmt{ + DocumentType: "UNKNOWN", + Name: ast.QualifiedName{Module: "MyModule", Name: "Thing"}, + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "unsupported") +} + +// --------------------------------------------------------------------------- +// execMove — backend error on move +// --------------------------------------------------------------------------- + +func TestMove_Page_BackendError(t *testing.T) { + mod := mkModule("MyModule") + pg := mkPage(mod.ID, "MyPage") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return []*pages.Page{pg}, nil }, + MovePageFunc: func(p *pages.Page) error { return fmt.Errorf("disk full") }, + } + h := mkHierarchy(mod) + withContainer(h, pg.ContainerID, mod.ID) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execMove(ctx, &ast.MoveStmt{ + DocumentType: ast.DocumentTypePage, + Name: ast.QualifiedName{Module: "MyModule", Name: "MyPage"}, + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "move page") +} diff --git a/mdl/executor/cmd_rename_mock_test.go b/mdl/executor/cmd_rename_mock_test.go new file mode 100644 index 00000000..76660aff --- /dev/null +++ b/mdl/executor/cmd_rename_mock_test.go @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "fmt" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/domainmodel" + "github.com/mendixlabs/mxcli/sdk/microflows" + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// --------------------------------------------------------------------------- +// Not connected +// --------------------------------------------------------------------------- + +func TestRename_NotConnected(t *testing.T) { + mb := &mock.MockBackend{IsConnectedFunc: func() bool { return false }} + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execRename(ctx, &ast.RenameStmt{ + ObjectType: "entity", + Name: ast.QualifiedName{Module: "MyModule", Name: "OldName"}, + NewName: "NewName", + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not connected") +} + +// --------------------------------------------------------------------------- +// Unsupported type +// --------------------------------------------------------------------------- + +func TestRename_UnsupportedType(t *testing.T) { + mb := &mock.MockBackend{IsConnectedFunc: func() bool { return true }} + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execRename(ctx, &ast.RenameStmt{ + ObjectType: "workflow", + Name: ast.QualifiedName{Module: "M", Name: "N"}, + NewName: "X", + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not supported") +} + +// --------------------------------------------------------------------------- +// Rename entity — happy path +// --------------------------------------------------------------------------- + +func TestRename_Entity_Success(t *testing.T) { + mod := mkModule("MyModule") + ent := mkEntity(mod.ID, "OldEntity") + dm := mkDomainModel(mod.ID, ent) + dmUpdated := false + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + GetDomainModelFunc: func(id model.ID) (*domainmodel.DomainModel, error) { return dm, nil }, + RenameReferencesFunc: func(old, new string, dryRun bool) ([]types.RenameHit, error) { + return []types.RenameHit{{UnitID: "u1", Name: "SomeDoc", Count: 2}}, nil + }, + UpdateDomainModelFunc: func(d *domainmodel.DomainModel) error { dmUpdated = true; return nil }, + } + h := mkHierarchy(mod) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execRename(ctx, &ast.RenameStmt{ + ObjectType: "entity", + Name: ast.QualifiedName{Module: "MyModule", Name: "OldEntity"}, + NewName: "NewEntity", + })) + if !dmUpdated { + t.Error("Expected UpdateDomainModel to be called") + } + assertContainsStr(t, buf.String(), "Renamed entity") + assertContainsStr(t, buf.String(), "MyModule.OldEntity") + assertContainsStr(t, buf.String(), "MyModule.NewEntity") + assertContainsStr(t, buf.String(), "Updated 2 reference(s)") +} + +// --------------------------------------------------------------------------- +// Rename entity — not found +// --------------------------------------------------------------------------- + +func TestRename_Entity_NotFound(t *testing.T) { + mod := mkModule("MyModule") + dm := mkDomainModel(mod.ID) // no entities + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + GetDomainModelFunc: func(id model.ID) (*domainmodel.DomainModel, error) { return dm, nil }, + } + h := mkHierarchy(mod) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execRename(ctx, &ast.RenameStmt{ + ObjectType: "entity", + Name: ast.QualifiedName{Module: "MyModule", Name: "Missing"}, + NewName: "New", + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not found") +} + +// --------------------------------------------------------------------------- +// Rename entity — dry run +// --------------------------------------------------------------------------- + +func TestRename_Entity_DryRun(t *testing.T) { + mod := mkModule("MyModule") + ent := mkEntity(mod.ID, "OldEntity") + dm := mkDomainModel(mod.ID, ent) + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + GetDomainModelFunc: func(id model.ID) (*domainmodel.DomainModel, error) { return dm, nil }, + RenameReferencesFunc: func(old, new string, dryRun bool) ([]types.RenameHit, error) { + if !dryRun { + t.Error("Expected dryRun=true") + } + return []types.RenameHit{{UnitID: "u1", Name: "Page1", UnitType: "Pages$Page", Count: 1}}, nil + }, + } + h := mkHierarchy(mod) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execRename(ctx, &ast.RenameStmt{ + ObjectType: "entity", + Name: ast.QualifiedName{Module: "MyModule", Name: "OldEntity"}, + NewName: "NewEntity", + DryRun: true, + })) + assertContainsStr(t, buf.String(), "Would rename") + assertContainsStr(t, buf.String(), "Page1") +} + +// --------------------------------------------------------------------------- +// Rename microflow (document type) — happy path +// --------------------------------------------------------------------------- + +func TestRename_Microflow_Success(t *testing.T) { + mod := mkModule("MyModule") + mf := mkMicroflow(mod.ID, "OldMF") + renameCalled := false + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListMicroflowsFunc: func() ([]*microflows.Microflow, error) { return []*microflows.Microflow{mf}, nil }, + RenameReferencesFunc: func(old, new string, dryRun bool) ([]types.RenameHit, error) { + return nil, nil + }, + RenameDocumentByNameFunc: func(mod, old, new string) error { + renameCalled = true + return nil + }, + } + h := mkHierarchy(mod) + withContainer(h, mf.ContainerID, mod.ID) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execRename(ctx, &ast.RenameStmt{ + ObjectType: "microflow", + Name: ast.QualifiedName{Module: "MyModule", Name: "OldMF"}, + NewName: "NewMF", + })) + if !renameCalled { + t.Error("Expected RenameDocumentByName to be called") + } + assertContainsStr(t, buf.String(), "Renamed microflow") +} + +// --------------------------------------------------------------------------- +// Rename page — not found +// --------------------------------------------------------------------------- + +func TestRename_Page_NotFound(t *testing.T) { + mod := mkModule("MyModule") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return nil, nil }, + } + h := mkHierarchy(mod) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execRename(ctx, &ast.RenameStmt{ + ObjectType: "page", + Name: ast.QualifiedName{Module: "MyModule", Name: "Missing"}, + NewName: "New", + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not found") +} + +// --------------------------------------------------------------------------- +// Rename module — happy path +// --------------------------------------------------------------------------- + +func TestRename_Module_Success(t *testing.T) { + mod := mkModule("OldModule") + moduleUpdated := false + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + RenameReferencesFunc: func(old, new string, dryRun bool) ([]types.RenameHit, error) { + return nil, nil + }, + UpdateModuleFunc: func(m *model.Module) error { + moduleUpdated = true + if m.Name != "NewModule" { + t.Errorf("Expected new name NewModule, got %s", m.Name) + } + return nil + }, + } + h := mkHierarchy(mod) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execRename(ctx, &ast.RenameStmt{ + ObjectType: "module", + Name: ast.QualifiedName{Module: "OldModule"}, + NewName: "NewModule", + })) + if !moduleUpdated { + t.Error("Expected UpdateModule to be called") + } + assertContainsStr(t, buf.String(), "Renamed module") +} + +// --------------------------------------------------------------------------- +// Rename association — happy path +// --------------------------------------------------------------------------- + +func TestRename_Association_Success(t *testing.T) { + mod := mkModule("MyModule") + ent1 := mkEntity(mod.ID, "Parent") + ent2 := mkEntity(mod.ID, "Child") + assoc := mkAssociation(mod.ID, "OldAssoc", ent1.ID, ent2.ID) + dm := mkDomainModel(mod.ID, ent1, ent2) + dm.Associations = []*domainmodel.Association{assoc} + dmUpdated := false + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + GetDomainModelFunc: func(id model.ID) (*domainmodel.DomainModel, error) { return dm, nil }, + RenameReferencesFunc: func(old, new string, dryRun bool) ([]types.RenameHit, error) { + return nil, nil + }, + UpdateDomainModelFunc: func(d *domainmodel.DomainModel) error { dmUpdated = true; return nil }, + } + h := mkHierarchy(mod) + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, execRename(ctx, &ast.RenameStmt{ + ObjectType: "association", + Name: ast.QualifiedName{Module: "MyModule", Name: "OldAssoc"}, + NewName: "NewAssoc", + })) + if !dmUpdated { + t.Error("Expected UpdateDomainModel to be called") + } + assertContainsStr(t, buf.String(), "Renamed association") +} + +// --------------------------------------------------------------------------- +// Rename association — not found +// --------------------------------------------------------------------------- + +func TestRename_Association_NotFound(t *testing.T) { + mod := mkModule("MyModule") + dm := mkDomainModel(mod.ID) // no associations + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + GetDomainModelFunc: func(id model.ID) (*domainmodel.DomainModel, error) { return dm, nil }, + } + h := mkHierarchy(mod) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execRename(ctx, &ast.RenameStmt{ + ObjectType: "association", + Name: ast.QualifiedName{Module: "MyModule", Name: "Missing"}, + NewName: "New", + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not found") +} + +// --------------------------------------------------------------------------- +// Rename backend error +// --------------------------------------------------------------------------- + +func TestRename_Entity_BackendError(t *testing.T) { + mod := mkModule("MyModule") + ent := mkEntity(mod.ID, "Ent") + dm := mkDomainModel(mod.ID, ent) + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + GetDomainModelFunc: func(id model.ID) (*domainmodel.DomainModel, error) { return dm, nil }, + RenameReferencesFunc: func(old, new string, dryRun bool) ([]types.RenameHit, error) { + return nil, fmt.Errorf("scan error") + }, + } + h := mkHierarchy(mod) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + err := execRename(ctx, &ast.RenameStmt{ + ObjectType: "entity", + Name: ast.QualifiedName{Module: "MyModule", Name: "Ent"}, + NewName: "New", + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "scan references") +} diff --git a/mdl/executor/cmd_styling_mock_test.go b/mdl/executor/cmd_styling_mock_test.go new file mode 100644 index 00000000..e614c6ec --- /dev/null +++ b/mdl/executor/cmd_styling_mock_test.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend/mock" +) + +// --------------------------------------------------------------------------- +// execShowDesignProperties +// --------------------------------------------------------------------------- + +func TestShowDesignProperties_NotConnected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execShowDesignProperties(ctx, &ast.ShowDesignPropertiesStmt{}) + assertError(t, err) + assertContainsStr(t, err.Error(), "not connected") +} + +func TestShowDesignProperties_NoMprPath(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + ctx.MprPath = "" + err := execShowDesignProperties(ctx, &ast.ShowDesignPropertiesStmt{}) + assertError(t, err) + assertContainsStr(t, err.Error(), "project path") +} + +// NOTE: execShowDesignProperties happy path requires loadThemeRegistry which +// reads design-properties.json from the filesystem. Would need a temp dir with +// a valid theme structure to test. Tracked separately. + +// --------------------------------------------------------------------------- +// execDescribeStyling +// --------------------------------------------------------------------------- + +func TestDescribeStyling_NotConnected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execDescribeStyling(ctx, &ast.DescribeStylingStmt{ + ContainerType: "page", + ContainerName: ast.QualifiedName{Module: "Mod", Name: "Home"}, + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not connected") +} + +// NOTE: execDescribeStyling happy path calls getPageWidgetsFromRaw / +// getSnippetWidgetsFromRaw which use ctx.Backend.GetRawUnit for BSON parsing. +// MockBackend has GetRawUnitFunc but producing valid BSON test data for the +// page widget walker is non-trivial. Tracked separately. + +// --------------------------------------------------------------------------- +// execAlterStyling +// --------------------------------------------------------------------------- + +func TestAlterStyling_NotConnected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + err := execAlterStyling(ctx, &ast.AlterStylingStmt{ + ContainerType: "page", + ContainerName: ast.QualifiedName{Module: "Mod", Name: "Home"}, + WidgetName: "container1", + }) + assertError(t, err) + assertContainsStr(t, err.Error(), "not connected") +} + +// NOTE: execAlterStyling happy path uses walkPageWidgets / walkSnippetWidgets +// (reflection-based applyStylingAssignments on real Page/Snippet structs) + +// ListPages/UpdatePage. The reflection walker needs real page struct data. +// ConnectedForWrite delegates to Connected — cannot differentiate in mock. +// Tracked separately.