diff --git a/mdl/executor/cmd_agenteditor_mock_test.go b/mdl/executor/cmd_agenteditor_mock_test.go index aa3c18ea..9ae26e53 100644 --- a/mdl/executor/cmd_agenteditor_mock_test.go +++ b/mdl/executor/cmd_agenteditor_mock_test.go @@ -526,3 +526,179 @@ func TestDescribeAgentEditorConsumedMCPService_Mock(t *testing.T) { assertContainsStr(t, out, "create consumed mcp service") assertContainsStr(t, out, "ProtocolVersion") } + +// --------------------------------------------------------------------------- +// DESCRIBE — Not Found +// --------------------------------------------------------------------------- + +func TestDescribeAgentEditorModel_Mock_NotFound(t *testing.T) { + mod := mkModule("M") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListAgentEditorModelsFunc: func() ([]*agenteditor.Model, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeAgentEditorModel(ctx, ast.QualifiedName{Module: "M", Name: "NonExistent"})) +} + +func TestDescribeAgentEditorAgent_Mock_NotFound(t *testing.T) { + mod := mkModule("M") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListAgentEditorAgentsFunc: func() ([]*agenteditor.Agent, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeAgentEditorAgent(ctx, ast.QualifiedName{Module: "M", Name: "NonExistent"})) +} + +func TestDescribeAgentEditorKnowledgeBase_Mock_NotFound(t *testing.T) { + mod := mkModule("M") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListAgentEditorKnowledgeBasesFunc: func() ([]*agenteditor.KnowledgeBase, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeAgentEditorKnowledgeBase(ctx, ast.QualifiedName{Module: "M", Name: "NonExistent"})) +} + +func TestDescribeAgentEditorConsumedMCPService_Mock_NotFound(t *testing.T) { + mod := mkModule("M") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListAgentEditorConsumedMCPServicesFunc: func() ([]*agenteditor.ConsumedMCPService, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeAgentEditorConsumedMCPService(ctx, ast.QualifiedName{Module: "M", Name: "NonExistent"})) +} + +// --------------------------------------------------------------------------- +// DROP — Not Found +// --------------------------------------------------------------------------- + +func TestDropAgentEditorModel_Mock_NotFound(t *testing.T) { + mod := mkModule("M") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListAgentEditorModelsFunc: func() ([]*agenteditor.Model, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, execDropAgentEditorModel(ctx, &ast.DropModelStmt{ + Name: ast.QualifiedName{Module: "M", Name: "NonExistent"}, + })) +} + +func TestDropConsumedMCPService_Mock_NotFound(t *testing.T) { + mod := mkModule("M") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListAgentEditorConsumedMCPServicesFunc: func() ([]*agenteditor.ConsumedMCPService, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, execDropConsumedMCPService(ctx, &ast.DropConsumedMCPServiceStmt{ + Name: ast.QualifiedName{Module: "M", Name: "NonExistent"}, + })) +} + +func TestDropKnowledgeBase_Mock_NotFound(t *testing.T) { + mod := mkModule("M") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListAgentEditorKnowledgeBasesFunc: func() ([]*agenteditor.KnowledgeBase, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, execDropKnowledgeBase(ctx, &ast.DropKnowledgeBaseStmt{ + Name: ast.QualifiedName{Module: "M", Name: "NonExistent"}, + })) +} + +func TestDropAgent_Mock_NotFound(t *testing.T) { + mod := mkModule("M") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListAgentEditorAgentsFunc: func() ([]*agenteditor.Agent, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, execDropAgent(ctx, &ast.DropAgentStmt{ + Name: ast.QualifiedName{Module: "M", Name: "NonExistent"}, + })) +} + +// --------------------------------------------------------------------------- +// LIST — Filter by Module +// --------------------------------------------------------------------------- + +func TestShowAgentEditorModels_Mock_FilterByModule(t *testing.T) { + mod1 := mkModule("A") + mod2 := mkModule("B") + m1 := &agenteditor.Model{ + BaseElement: model.BaseElement{ID: nextID("aem")}, + ContainerID: mod1.ID, + Name: "M1", + } + m2 := &agenteditor.Model{ + BaseElement: model.BaseElement{ID: nextID("aem")}, + ContainerID: mod2.ID, + Name: "M2", + } + + h := mkHierarchy(mod1, mod2) + withContainer(h, m1.ContainerID, mod1.ID) + withContainer(h, m2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListAgentEditorModelsFunc: func() ([]*agenteditor.Model, error) { return []*agenteditor.Model{m1, m2}, nil }, + } + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listAgentEditorModels(ctx, "B")) + + out := buf.String() + assertNotContainsStr(t, out, "A.M1") + assertContainsStr(t, out, "B.M2") +} + +func TestShowAgentEditorAgents_Mock_FilterByModule(t *testing.T) { + mod1 := mkModule("A") + mod2 := mkModule("B") + a1 := &agenteditor.Agent{ + BaseElement: model.BaseElement{ID: nextID("aea")}, + ContainerID: mod1.ID, + Name: "Agent1", + } + a2 := &agenteditor.Agent{ + BaseElement: model.BaseElement{ID: nextID("aea")}, + ContainerID: mod2.ID, + Name: "Agent2", + } + + h := mkHierarchy(mod1, mod2) + withContainer(h, a1.ContainerID, mod1.ID) + withContainer(h, a2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListAgentEditorAgentsFunc: func() ([]*agenteditor.Agent, error) { return []*agenteditor.Agent{a1, a2}, nil }, + } + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listAgentEditorAgents(ctx, "B")) + + out := buf.String() + assertNotContainsStr(t, out, "A.Agent1") + assertContainsStr(t, out, "B.Agent2") +} 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_associations_mock_test.go b/mdl/executor/cmd_associations_mock_test.go index f65cc9cc..e36a319b 100644 --- a/mdl/executor/cmd_associations_mock_test.go +++ b/mdl/executor/cmd_associations_mock_test.go @@ -3,6 +3,7 @@ package executor import ( + "fmt" "testing" "github.com/mendixlabs/mxcli/mdl/backend/mock" @@ -75,3 +76,40 @@ func TestShowAssociations_Mock_FilterByModule(t *testing.T) { assertContainsStr(t, out, "HR.Employee_Dept") assertContainsStr(t, out, "(1 associations)") } + +// NOTE: listAssociations and describeAssociation have no Connected() guard. +// They call backend directly — error propagation is the only failure mode. + +func TestShowAssociations_BackendError(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return nil, fmt.Errorf("connection lost") }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, listAssociations(ctx, "")) +} + +func TestShowAssociations_JSON(t *testing.T) { + mod := mkModule("App") + ent1 := mkEntity(mod.ID, "A") + ent2 := mkEntity(mod.ID, "B") + assoc := mkAssociation(mod.ID, "A_B", ent1.ID, ent2.ID) + + dm := &domainmodel.DomainModel{ + BaseElement: model.BaseElement{ID: nextID("dm")}, + ContainerID: mod.ID, + Entities: []*domainmodel.Entity{ent1, ent2}, + Associations: []*domainmodel.Association{assoc}, + } + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { return []*domainmodel.DomainModel{dm}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withFormat(FormatJSON)) + assertNoError(t, listAssociations(ctx, "")) + assertValidJSON(t, buf.String()) + assertContainsStr(t, buf.String(), "A_B") +} diff --git a/mdl/executor/cmd_businessevents_mock_test.go b/mdl/executor/cmd_businessevents_mock_test.go index b76d236b..c84ad199 100644 --- a/mdl/executor/cmd_businessevents_mock_test.go +++ b/mdl/executor/cmd_businessevents_mock_test.go @@ -77,3 +77,36 @@ func TestDescribeBusinessEventService_Mock(t *testing.T) { assertContainsStr(t, out, "create or replace business event service") assertContainsStr(t, out, "MyModule.OrderEvents") } + +func TestDescribeBusinessEventService_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListBusinessEventServicesFunc: func() ([]*model.BusinessEventService, error) { + return nil, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeBusinessEventService(ctx, ast.QualifiedName{Module: "X", Name: "NoSuch"})) +} + +func TestShowBusinessEventServices_FilterByModule(t *testing.T) { + mod := mkModule("Orders") + svc := &model.BusinessEventService{ + BaseElement: model.BaseElement{ID: nextID("bes")}, + ContainerID: mod.ID, + Name: "OrderEvents", + } + h := mkHierarchy(mod) + withContainer(h, svc.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListBusinessEventServicesFunc: func() ([]*model.BusinessEventService, error) { + return []*model.BusinessEventService{svc}, nil + }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listBusinessEventServices(ctx, "Orders")) + assertContainsStr(t, buf.String(), "Orders.OrderEvents") +} diff --git a/mdl/executor/cmd_constants_mock_test.go b/mdl/executor/cmd_constants_mock_test.go index e1eaab4c..913442af 100644 --- a/mdl/executor/cmd_constants_mock_test.go +++ b/mdl/executor/cmd_constants_mock_test.go @@ -104,3 +104,6 @@ func TestDescribeConstant_Mock_NotFound(t *testing.T) { err := describeConstant(ctx, ast.QualifiedName{Module: "MyModule", Name: "Missing"}) assertError(t, err) } + +// Backend error: cmd_error_mock_test.go (TestShowConstants_Mock_BackendError) +// JSON: cmd_json_mock_test.go (TestShowConstants_Mock_JSON) diff --git a/mdl/executor/cmd_datatransformer_mock_test.go b/mdl/executor/cmd_datatransformer_mock_test.go index 408eb5db..4f397e6a 100644 --- a/mdl/executor/cmd_datatransformer_mock_test.go +++ b/mdl/executor/cmd_datatransformer_mock_test.go @@ -35,6 +35,52 @@ func TestListDataTransformers_Mock(t *testing.T) { assertContainsStr(t, out, "ETL.TransformOrders") } +func TestListDataTransformers_FilterByModule(t *testing.T) { + mod1 := mkModule("ETL") + mod2 := mkModule("Other") + dt1 := &model.DataTransformer{ + BaseElement: model.BaseElement{ID: nextID("dt")}, + ContainerID: mod1.ID, + Name: "TransformOrders", + SourceType: "Entity", + } + dt2 := &model.DataTransformer{ + BaseElement: model.BaseElement{ID: nextID("dt")}, + ContainerID: mod2.ID, + Name: "TransformCustomers", + SourceType: "Entity", + } + + h := mkHierarchy(mod1, mod2) + withContainer(h, dt1.ContainerID, mod1.ID) + withContainer(h, dt2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListDataTransformersFunc: func() ([]*model.DataTransformer, error) { return []*model.DataTransformer{dt1, dt2}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listDataTransformers(ctx, "ETL")) + + out := buf.String() + assertContainsStr(t, out, "ETL.TransformOrders") + assertNotContainsStr(t, out, "Other.TransformCustomers") +} + +func TestDescribeDataTransformer_NotFound(t *testing.T) { + mod := mkModule("ETL") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListDataTransformersFunc: func() ([]*model.DataTransformer, error) { return nil, nil }, + } + + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeDataTransformer(ctx, ast.QualifiedName{Module: "ETL", Name: "NoSuch"})) +} + func TestDescribeDataTransformer_Mock(t *testing.T) { mod := mkModule("ETL") dt := &model.DataTransformer{ diff --git a/mdl/executor/cmd_dbconnection_mock_test.go b/mdl/executor/cmd_dbconnection_mock_test.go index 726e6eb5..03b81e67 100644 --- a/mdl/executor/cmd_dbconnection_mock_test.go +++ b/mdl/executor/cmd_dbconnection_mock_test.go @@ -35,6 +35,52 @@ func TestShowDatabaseConnections_Mock(t *testing.T) { assertContainsStr(t, out, "DataMod.MyDB") } +func TestShowDatabaseConnections_FilterByModule(t *testing.T) { + mod1 := mkModule("DataMod") + mod2 := mkModule("Other") + conn1 := &model.DatabaseConnection{ + BaseElement: model.BaseElement{ID: nextID("dbc")}, + ContainerID: mod1.ID, + Name: "MyDB", + DatabaseType: "PostgreSQL", + } + conn2 := &model.DatabaseConnection{ + BaseElement: model.BaseElement{ID: nextID("dbc")}, + ContainerID: mod2.ID, + Name: "OtherDB", + DatabaseType: "MySQL", + } + + h := mkHierarchy(mod1, mod2) + withContainer(h, conn1.ContainerID, mod1.ID) + withContainer(h, conn2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListDatabaseConnectionsFunc: func() ([]*model.DatabaseConnection, error) { return []*model.DatabaseConnection{conn1, conn2}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listDatabaseConnections(ctx, "DataMod")) + + out := buf.String() + assertContainsStr(t, out, "DataMod.MyDB") + assertNotContainsStr(t, out, "Other.OtherDB") +} + +func TestDescribeDatabaseConnection_NotFound(t *testing.T) { + mod := mkModule("DataMod") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListDatabaseConnectionsFunc: func() ([]*model.DatabaseConnection, error) { return nil, nil }, + } + + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeDatabaseConnection(ctx, ast.QualifiedName{Module: "DataMod", Name: "NoSuch"})) +} + func TestDescribeDatabaseConnection_Mock(t *testing.T) { mod := mkModule("DataMod") conn := &model.DatabaseConnection{ diff --git a/mdl/executor/cmd_entities_mock_test.go b/mdl/executor/cmd_entities_mock_test.go index 249fa8ca..f3ca9b69 100644 --- a/mdl/executor/cmd_entities_mock_test.go +++ b/mdl/executor/cmd_entities_mock_test.go @@ -3,6 +3,7 @@ package executor import ( + "fmt" "testing" "github.com/mendixlabs/mxcli/mdl/backend/mock" @@ -56,3 +57,44 @@ func TestShowEntities_Mock_FilterByModule(t *testing.T) { assertContainsStr(t, out, "HR.Employee") assertContainsStr(t, out, "(1 entities)") } + +// NOTE: listEntities has no Connected() guard — it calls backend directly. + +func TestShowEntities_BackendError_Modules(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return nil, fmt.Errorf("not connected") }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, listEntities(ctx, "")) +} + +func TestShowEntities_BackendError(t *testing.T) { + mod := mkModule("Sales") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { + return nil, fmt.Errorf("backend down") + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, listEntities(ctx, "")) +} + +func TestShowEntities_JSON(t *testing.T) { + mod := mkModule("App") + ent := mkEntity(mod.ID, "Item") + dm := mkDomainModel(mod.ID, ent) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { return []*domainmodel.DomainModel{dm}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withFormat(FormatJSON)) + assertNoError(t, listEntities(ctx, "")) + assertValidJSON(t, buf.String()) + assertContainsStr(t, buf.String(), "Item") +} diff --git a/mdl/executor/cmd_enumerations_mock_test.go b/mdl/executor/cmd_enumerations_mock_test.go index 70baaff0..d62b5e71 100644 --- a/mdl/executor/cmd_enumerations_mock_test.go +++ b/mdl/executor/cmd_enumerations_mock_test.go @@ -97,3 +97,6 @@ func TestDescribeEnumeration_Mock_NotFound(t *testing.T) { err := describeEnumeration(ctx, ast.QualifiedName{Module: "MyModule", Name: "Missing"}) assertError(t, err) } + +// Backend error: cmd_error_mock_test.go (TestShowEnumerations_Mock_BackendError) +// JSON: cmd_json_mock_test.go (TestShowEnumerations_Mock_JSON) diff --git a/mdl/executor/cmd_export_mappings_mock_test.go b/mdl/executor/cmd_export_mappings_mock_test.go index 2d521a8f..493c7b59 100644 --- a/mdl/executor/cmd_export_mappings_mock_test.go +++ b/mdl/executor/cmd_export_mappings_mock_test.go @@ -3,8 +3,10 @@ package executor import ( + "fmt" "testing" + "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend/mock" "github.com/mendixlabs/mxcli/model" ) @@ -32,3 +34,69 @@ func TestShowExportMappings_Mock(t *testing.T) { assertContainsStr(t, out, "Export Mapping") assertContainsStr(t, out, "Integration.ExportOrders") } + +func TestShowExportMappings_FilterByModule(t *testing.T) { + mod1 := mkModule("Integration") + mod2 := mkModule("Other") + em1 := &model.ExportMapping{ + BaseElement: model.BaseElement{ID: nextID("em")}, + ContainerID: mod1.ID, + Name: "ExportOrders", + } + em2 := &model.ExportMapping{ + BaseElement: model.BaseElement{ID: nextID("em")}, + ContainerID: mod2.ID, + Name: "ExportOther", + } + + h := mkHierarchy(mod1, mod2) + withContainer(h, em1.ContainerID, mod1.ID) + withContainer(h, em2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListExportMappingsFunc: func() ([]*model.ExportMapping, error) { return []*model.ExportMapping{em1, em2}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listExportMappings(ctx, "Integration")) + + out := buf.String() + assertContainsStr(t, out, "Integration.ExportOrders") + assertNotContainsStr(t, out, "Other.ExportOther") +} + +func TestDescribeExportMapping_Mock(t *testing.T) { + mod := mkModule("Integration") + em := &model.ExportMapping{ + BaseElement: model.BaseElement{ID: nextID("em")}, + ContainerID: mod.ID, + Name: "ExportOrders", + } + + h := mkHierarchy(mod) + withContainer(h, em.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + GetExportMappingByQualifiedNameFunc: func(moduleName, name string) (*model.ExportMapping, error) { + return em, nil + }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, describeExportMapping(ctx, ast.QualifiedName{Module: "Integration", Name: "ExportOrders"})) + assertContainsStr(t, buf.String(), "create export mapping") +} + +func TestDescribeExportMapping_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + GetExportMappingByQualifiedNameFunc: func(moduleName, name string) (*model.ExportMapping, error) { + return nil, fmt.Errorf("export mapping not found: %s.%s", moduleName, name) + }, + } + + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeExportMapping(ctx, ast.QualifiedName{Module: "Integration", Name: "NoSuch"})) +} 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_fragments_mock_test.go b/mdl/executor/cmd_fragments_mock_test.go index 631a7923..ed568af6 100644 --- a/mdl/executor/cmd_fragments_mock_test.go +++ b/mdl/executor/cmd_fragments_mock_test.go @@ -29,3 +29,37 @@ func TestShowFragments_Empty_Mock(t *testing.T) { out := buf.String() assertContainsStr(t, out, "No fragments defined.") } + +func TestDescribeFragment_Mock(t *testing.T) { + ctx, buf := newMockCtx(t) + ctx.Fragments = map[string]*ast.DefineFragmentStmt{ + "myFrag": { + Name: "myFrag", + Widgets: []*ast.WidgetV3{ + {Type: "Button", Name: "btnSave"}, + }, + }, + } + + assertNoError(t, describeFragment(ctx, ast.QualifiedName{Name: "myFrag"})) + + out := buf.String() + assertContainsStr(t, out, "define fragment myFrag") + assertContainsStr(t, out, "Button btnSave") +} + +func TestDescribeFragment_NotFound(t *testing.T) { + ctx, _ := newMockCtx(t) + ctx.Fragments = map[string]*ast.DefineFragmentStmt{} + + err := describeFragment(ctx, ast.QualifiedName{Name: "noSuchFrag"}) + assertError(t, err) +} + +func TestDescribeFragment_NilFragments(t *testing.T) { + ctx, _ := newMockCtx(t) + // ctx.Fragments is nil by default + + err := describeFragment(ctx, ast.QualifiedName{Name: "noSuchFrag"}) + assertError(t, err) +} diff --git a/mdl/executor/cmd_imagecollections_mock_test.go b/mdl/executor/cmd_imagecollections_mock_test.go index 79c61e3a..810d52ab 100644 --- a/mdl/executor/cmd_imagecollections_mock_test.go +++ b/mdl/executor/cmd_imagecollections_mock_test.go @@ -36,6 +36,52 @@ func TestShowImageCollections_Mock(t *testing.T) { assertContainsStr(t, out, "Icons.AppIcons") } +func TestShowImageCollections_FilterByModule(t *testing.T) { + mod1 := mkModule("Icons") + mod2 := mkModule("Other") + ic1 := &types.ImageCollection{ + BaseElement: model.BaseElement{ID: nextID("ic")}, + ContainerID: mod1.ID, + Name: "AppIcons", + ExportLevel: "Hidden", + } + ic2 := &types.ImageCollection{ + BaseElement: model.BaseElement{ID: nextID("ic")}, + ContainerID: mod2.ID, + Name: "OtherIcons", + ExportLevel: "Hidden", + } + + h := mkHierarchy(mod1, mod2) + withContainer(h, ic1.ContainerID, mod1.ID) + withContainer(h, ic2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListImageCollectionsFunc: func() ([]*types.ImageCollection, error) { return []*types.ImageCollection{ic1, ic2}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listImageCollections(ctx, "Icons")) + + out := buf.String() + assertContainsStr(t, out, "Icons.AppIcons") + assertNotContainsStr(t, out, "Other.OtherIcons") +} + +func TestDescribeImageCollection_NotFound(t *testing.T) { + mod := mkModule("Icons") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListImageCollectionsFunc: func() ([]*types.ImageCollection, error) { return nil, nil }, + } + + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeImageCollection(ctx, ast.QualifiedName{Module: "Icons", Name: "NoSuch"})) +} + func TestDescribeImageCollection_Mock(t *testing.T) { mod := mkModule("Icons") ic := &types.ImageCollection{ diff --git a/mdl/executor/cmd_import_mappings_mock_test.go b/mdl/executor/cmd_import_mappings_mock_test.go index 9c9b9623..55257dd2 100644 --- a/mdl/executor/cmd_import_mappings_mock_test.go +++ b/mdl/executor/cmd_import_mappings_mock_test.go @@ -3,8 +3,10 @@ package executor import ( + "fmt" "testing" + "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend/mock" "github.com/mendixlabs/mxcli/model" ) @@ -32,3 +34,69 @@ func TestShowImportMappings_Mock(t *testing.T) { assertContainsStr(t, out, "Import Mapping") assertContainsStr(t, out, "Integration.ImportOrders") } + +func TestShowImportMappings_FilterByModule(t *testing.T) { + mod1 := mkModule("Integration") + mod2 := mkModule("Other") + im1 := &model.ImportMapping{ + BaseElement: model.BaseElement{ID: nextID("im")}, + ContainerID: mod1.ID, + Name: "ImportOrders", + } + im2 := &model.ImportMapping{ + BaseElement: model.BaseElement{ID: nextID("im")}, + ContainerID: mod2.ID, + Name: "ImportOther", + } + + h := mkHierarchy(mod1, mod2) + withContainer(h, im1.ContainerID, mod1.ID) + withContainer(h, im2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListImportMappingsFunc: func() ([]*model.ImportMapping, error) { return []*model.ImportMapping{im1, im2}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listImportMappings(ctx, "Integration")) + + out := buf.String() + assertContainsStr(t, out, "Integration.ImportOrders") + assertNotContainsStr(t, out, "Other.ImportOther") +} + +func TestDescribeImportMapping_Mock(t *testing.T) { + mod := mkModule("Integration") + im := &model.ImportMapping{ + BaseElement: model.BaseElement{ID: nextID("im")}, + ContainerID: mod.ID, + Name: "ImportOrders", + } + + h := mkHierarchy(mod) + withContainer(h, im.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + GetImportMappingByQualifiedNameFunc: func(moduleName, name string) (*model.ImportMapping, error) { + return im, nil + }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, describeImportMapping(ctx, ast.QualifiedName{Module: "Integration", Name: "ImportOrders"})) + assertContainsStr(t, buf.String(), "create import mapping") +} + +func TestDescribeImportMapping_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + GetImportMappingByQualifiedNameFunc: func(moduleName, name string) (*model.ImportMapping, error) { + return nil, fmt.Errorf("import mapping not found: %s.%s", moduleName, name) + }, + } + + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeImportMapping(ctx, ast.QualifiedName{Module: "Integration", Name: "NoSuch"})) +} 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_javaactions_mock_test.go b/mdl/executor/cmd_javaactions_mock_test.go index 8792b96c..5cf56dcf 100644 --- a/mdl/executor/cmd_javaactions_mock_test.go +++ b/mdl/executor/cmd_javaactions_mock_test.go @@ -3,6 +3,7 @@ package executor import ( + "fmt" "testing" "github.com/mendixlabs/mxcli/mdl/ast" @@ -56,3 +57,85 @@ func TestDescribeJavaAction_Mock(t *testing.T) { out := buf.String() assertContainsStr(t, out, "create java action") } + +// NOTE: listJavaActions has no explicit not-connected guard. It calls +// getHierarchy (returns nil when disconnected) then proceeds to call +// ListJavaActions on the backend. The handler degrades gracefully — +// with an empty result set it succeeds with a nil hierarchy. + +func TestShowJavaActions_BackendError(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListJavaActionsFunc: func() ([]*types.JavaAction, error) { + return nil, fmt.Errorf("backend unavailable") + }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, listJavaActions(ctx, "")) +} + +func TestDescribeJavaAction_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ReadJavaActionByNameFunc: func(qn string) (*javaactions.JavaAction, error) { + return nil, fmt.Errorf("not found") + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeJavaAction(ctx, ast.QualifiedName{Module: "MyModule", Name: "Missing"})) +} + +func TestShowJavaActions_FilterByModule(t *testing.T) { + mod1 := mkModule("Alpha") + mod2 := mkModule("Beta") + ja1 := &types.JavaAction{ + BaseElement: model.BaseElement{ID: nextID("ja")}, + ContainerID: mod1.ID, + Name: "ActionA", + } + ja2 := &types.JavaAction{ + BaseElement: model.BaseElement{ID: nextID("ja")}, + ContainerID: mod2.ID, + Name: "ActionB", + } + + h := mkHierarchy(mod1, mod2) + withContainer(h, ja1.ContainerID, mod1.ID) + withContainer(h, ja2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListJavaActionsFunc: func() ([]*types.JavaAction, error) { return []*types.JavaAction{ja1, ja2}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listJavaActions(ctx, "Alpha")) + + out := buf.String() + assertContainsStr(t, out, "Alpha.ActionA") + assertNotContainsStr(t, out, "Beta.ActionB") +} + +func TestShowJavaActions_JSON(t *testing.T) { + mod := mkModule("MyModule") + ja := &types.JavaAction{ + BaseElement: model.BaseElement{ID: nextID("ja")}, + ContainerID: mod.ID, + Name: "DoSomething", + } + + h := mkHierarchy(mod) + withContainer(h, ja.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListJavaActionsFunc: func() ([]*types.JavaAction, error) { return []*types.JavaAction{ja}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h), withFormat(FormatJSON)) + assertNoError(t, listJavaActions(ctx, "")) + assertValidJSON(t, buf.String()) +} diff --git a/mdl/executor/cmd_javascript_actions_mock_test.go b/mdl/executor/cmd_javascript_actions_mock_test.go index 08fcb39b..a1e5ca91 100644 --- a/mdl/executor/cmd_javascript_actions_mock_test.go +++ b/mdl/executor/cmd_javascript_actions_mock_test.go @@ -3,6 +3,7 @@ package executor import ( + "fmt" "testing" "github.com/mendixlabs/mxcli/mdl/ast" @@ -57,3 +58,36 @@ func TestDescribeJavaScriptAction_Mock(t *testing.T) { out := buf.String() assertContainsStr(t, out, "create javascript action") } + +func TestDescribeJavaScriptAction_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ReadJavaScriptActionByNameFunc: func(qn string) (*types.JavaScriptAction, error) { + return nil, fmt.Errorf("not found: %s", qn) + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeJavaScriptAction(ctx, ast.QualifiedName{Module: "X", Name: "NoSuch"})) +} + +func TestShowJavaScriptActions_FilterByModule(t *testing.T) { + mod := mkModule("WebMod") + jsa := &types.JavaScriptAction{ + BaseElement: model.BaseElement{ID: nextID("jsa")}, + ContainerID: mod.ID, + Name: "ShowAlert", + Platform: "Web", + } + + h := mkHierarchy(mod) + withContainer(h, jsa.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListJavaScriptActionsFunc: func() ([]*types.JavaScriptAction, error) { return []*types.JavaScriptAction{jsa}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listJavaScriptActions(ctx, "WebMod")) + assertContainsStr(t, buf.String(), "WebMod.ShowAlert") +} diff --git a/mdl/executor/cmd_jsonstructures_mock_test.go b/mdl/executor/cmd_jsonstructures_mock_test.go index 5ab7f50a..abd6d4f3 100644 --- a/mdl/executor/cmd_jsonstructures_mock_test.go +++ b/mdl/executor/cmd_jsonstructures_mock_test.go @@ -5,6 +5,7 @@ package executor import ( "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" @@ -33,3 +34,68 @@ func TestShowJsonStructures_Mock(t *testing.T) { assertContainsStr(t, out, "json Structure") assertContainsStr(t, out, "OrderMgmt.OrderSchema") } + +func TestShowJsonStructures_FilterByModule(t *testing.T) { + mod1 := mkModule("OrderMgmt") + mod2 := mkModule("Other") + js1 := &types.JsonStructure{ + BaseElement: model.BaseElement{ID: nextID("js")}, + ContainerID: mod1.ID, + Name: "OrderSchema", + } + js2 := &types.JsonStructure{ + BaseElement: model.BaseElement{ID: nextID("js")}, + ContainerID: mod2.ID, + Name: "OtherSchema", + } + + h := mkHierarchy(mod1, mod2) + withContainer(h, js1.ContainerID, mod1.ID) + withContainer(h, js2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListJsonStructuresFunc: func() ([]*types.JsonStructure, error) { return []*types.JsonStructure{js1, js2}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listJsonStructures(ctx, "OrderMgmt")) + + out := buf.String() + assertContainsStr(t, out, "OrderMgmt.OrderSchema") + assertNotContainsStr(t, out, "Other.OtherSchema") +} + +func TestDescribeJsonStructure_Mock(t *testing.T) { + mod := mkModule("OrderMgmt") + js := &types.JsonStructure{ + BaseElement: model.BaseElement{ID: nextID("js")}, + ContainerID: mod.ID, + Name: "OrderSchema", + } + + h := mkHierarchy(mod) + withContainer(h, js.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListJsonStructuresFunc: func() ([]*types.JsonStructure, error) { return []*types.JsonStructure{js}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, describeJsonStructure(ctx, ast.QualifiedName{Module: "OrderMgmt", Name: "OrderSchema"})) + assertContainsStr(t, buf.String(), "create or replace json structure") +} + +func TestDescribeJsonStructure_NotFound(t *testing.T) { + mod := mkModule("OrderMgmt") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListJsonStructuresFunc: func() ([]*types.JsonStructure, error) { return nil, nil }, + } + + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeJsonStructure(ctx, ast.QualifiedName{Module: "OrderMgmt", Name: "NoSuch"})) +} diff --git a/mdl/executor/cmd_lint_mock_test.go b/mdl/executor/cmd_lint_mock_test.go new file mode 100644 index 00000000..c4a289c1 --- /dev/null +++ b/mdl/executor/cmd_lint_mock_test.go @@ -0,0 +1,60 @@ +// 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 +// +// Although listLintRules itself only writes to ctx.Output, execLint currently +// checks ctx.Connected() before dispatching to the ShowRules branch, so this +// test still needs a connected backend. 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_mermaid_mock_test.go b/mdl/executor/cmd_mermaid_mock_test.go index cb0af0d8..1a5eef6f 100644 --- a/mdl/executor/cmd_mermaid_mock_test.go +++ b/mdl/executor/cmd_mermaid_mock_test.go @@ -3,11 +3,14 @@ package executor import ( + "fmt" "testing" "github.com/mendixlabs/mxcli/mdl/backend/mock" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" + "github.com/mendixlabs/mxcli/sdk/microflows" + "github.com/mendixlabs/mxcli/sdk/pages" ) func TestDescribeMermaid_DomainModel_Mock(t *testing.T) { @@ -50,3 +53,68 @@ func TestDescribeMermaid_DomainModel_Mock(t *testing.T) { assertContainsStr(t, out, "Order") assertContainsStr(t, out, "Order_Customer") } + +func TestDescribeMermaid_Microflow_Mock(t *testing.T) { + mod := mkModule("MyModule") + mf := µflows.Microflow{ + BaseElement: model.BaseElement{ID: nextID("mf")}, + ContainerID: mod.ID, + Name: "ACT_Process", + } + + h := mkHierarchy(mod) + withContainer(h, mf.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListMicroflowsFunc: func() ([]*microflows.Microflow, error) { return []*microflows.Microflow{mf}, nil }, + ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { return nil, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, describeMermaid(ctx, "microflow", "MyModule.ACT_Process")) + + out := buf.String() + assertContainsStr(t, out, "flowchart") +} + +func TestDescribeMermaid_Microflow_NotFound(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListMicroflowsFunc: func() ([]*microflows.Microflow, error) { return nil, nil }, + ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { return nil, nil }, + } + + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeMermaid(ctx, "microflow", "MyModule.NoSuch")) +} + +func TestDescribeMermaid_Page_NotFound(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return nil, nil }, + } + + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeMermaid(ctx, "page", "MyModule.NoSuch")) +} + +func TestDescribeMermaid_UnsupportedType(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + } + + ctx, _ := newMockCtx(t, withBackend(mb)) + err := describeMermaid(ctx, "nanoflow", "MyModule.Something") + assertError(t, err) + assertContainsStr(t, fmt.Sprint(err), "not supported") +} diff --git a/mdl/executor/cmd_microflows_mock_test.go b/mdl/executor/cmd_microflows_mock_test.go index 63bc813d..a3166c48 100644 --- a/mdl/executor/cmd_microflows_mock_test.go +++ b/mdl/executor/cmd_microflows_mock_test.go @@ -111,3 +111,6 @@ func TestDescribeMicroflow_Mock_NotFound(t *testing.T) { err := describeMicroflow(ctx, ast.QualifiedName{Module: "MyModule", Name: "Missing"}) assertError(t, err) } + +// Backend error: cmd_error_mock_test.go (TestShowMicroflows_Mock_BackendError, TestShowNanoflows_Mock_BackendError) +// JSON: cmd_json_mock_test.go (TestShowMicroflows_Mock_JSON, TestShowNanoflows_Mock_JSON) diff --git a/mdl/executor/cmd_misc_mock_test.go b/mdl/executor/cmd_misc_mock_test.go index f2c5aa85..a8ca879f 100644 --- a/mdl/executor/cmd_misc_mock_test.go +++ b/mdl/executor/cmd_misc_mock_test.go @@ -33,3 +33,40 @@ func TestShowVersion_Mock(t *testing.T) { assertContainsStr(t, out, "Schema Hash") assertContainsStr(t, out, "abc123def456") } + +func TestShowVersion_NotConnected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, listVersion(ctx)) +} + +func TestShowVersion_NoSchemaHash(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ProjectVersionFunc: func() *types.ProjectVersion { + return &types.ProjectVersion{ + ProductVersion: "9.24.0", + BuildVersion: "9.24.0.5678", + FormatVersion: 1, + SchemaHash: "", + } + }, + } + ctx, buf := newMockCtx(t, withBackend(mb)) + assertNoError(t, listVersion(ctx)) + + out := buf.String() + assertContainsStr(t, out, "9.24.0") + assertNotContainsStr(t, out, "Schema Hash") +} + +func TestHelp_Mock(t *testing.T) { + ctx, buf := newMockCtx(t) + assertNoError(t, execHelp(ctx)) + + out := buf.String() + assertContainsStr(t, out, "MDL Commands") + assertContainsStr(t, out, "connect local") +} diff --git a/mdl/executor/cmd_modules_mock_test.go b/mdl/executor/cmd_modules_mock_test.go index 4995d3b7..5b63e7b5 100644 --- a/mdl/executor/cmd_modules_mock_test.go +++ b/mdl/executor/cmd_modules_mock_test.go @@ -3,6 +3,7 @@ package executor import ( + "fmt" "testing" "github.com/mendixlabs/mxcli/mdl/backend/mock" @@ -15,16 +16,12 @@ func TestShowModules_Mock(t *testing.T) { mod1 := mkModule("MyModule") mod2 := mkModule("System") - // listModules uses ListUnits to count documents per module. - // Provide a unit belonging to mod1 so the count is non-zero. unitID := nextID("unit") units := []*types.UnitInfo{{ID: unitID, ContainerID: mod1.ID}} - // Need a hierarchy for getHierarchy — provide modules + units + folders h := mkHierarchy(mod1, mod2) withContainer(h, unitID, mod1.ID) - // Provide one domain model for mod1 with one entity ent := mkEntity(mod1.ID, "Customer") dm := mkDomainModel(mod1.ID, ent) @@ -33,7 +30,6 @@ func TestShowModules_Mock(t *testing.T) { ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod1, mod2}, nil }, ListUnitsFunc: func() ([]*types.UnitInfo, error) { return units, nil }, ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { return []*domainmodel.DomainModel{dm}, nil }, - // All other list functions return nil (zero counts) via MockBackend defaults. } ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) @@ -44,3 +40,31 @@ func TestShowModules_Mock(t *testing.T) { assertContainsStr(t, out, "System") assertContainsStr(t, out, "(2 modules)") } + +// Not-connected covered in cmd_notconnected_mock_test.go + +func TestShowModules_BackendError(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return nil, fmt.Errorf("connection lost") }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(mkHierarchy())) + assertError(t, listModules(ctx)) +} + +func TestShowModules_JSON(t *testing.T) { + mod := mkModule("App") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListUnitsFunc: func() ([]*types.UnitInfo, error) { return nil, nil }, + ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { return nil, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h), withFormat(FormatJSON)) + assertNoError(t, listModules(ctx)) + assertValidJSON(t, buf.String()) + assertContainsStr(t, buf.String(), "App") +} 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_navigation_mock_test.go b/mdl/executor/cmd_navigation_mock_test.go index 1ff1f1d2..a25d98d8 100644 --- a/mdl/executor/cmd_navigation_mock_test.go +++ b/mdl/executor/cmd_navigation_mock_test.go @@ -90,3 +90,39 @@ func TestDescribeNavigation_Mock(t *testing.T) { assertNoError(t, describeNavigation(ctx, ast.QualifiedName{Name: "Responsive"})) assertContainsStr(t, buf.String(), "create or replace navigation") } + +func TestDescribeNavigation_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + GetNavigationFunc: func() (*types.NavigationDocument, error) { + return &types.NavigationDocument{ + Profiles: []*types.NavigationProfile{{ + Name: "Responsive", + Kind: "Responsive", + }}, + }, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeNavigation(ctx, ast.QualifiedName{Name: "NonExistent"})) +} + +func TestDescribeNavigation_AllProfiles(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + GetNavigationFunc: func() (*types.NavigationDocument, error) { + return &types.NavigationDocument{ + Profiles: []*types.NavigationProfile{ + {Name: "Responsive", Kind: "Responsive"}, + {Name: "NativePhone", Kind: "NativePhone"}, + }, + }, nil + }, + } + ctx, buf := newMockCtx(t, withBackend(mb)) + assertNoError(t, describeNavigation(ctx, ast.QualifiedName{Name: ""})) + + out := buf.String() + assertContainsStr(t, out, "Responsive") + assertContainsStr(t, out, "NativePhone") +} diff --git a/mdl/executor/cmd_odata_mock_test.go b/mdl/executor/cmd_odata_mock_test.go index dea62752..1608e051 100644 --- a/mdl/executor/cmd_odata_mock_test.go +++ b/mdl/executor/cmd_odata_mock_test.go @@ -96,6 +96,71 @@ func TestDescribeODataClient_Mock(t *testing.T) { assertContainsStr(t, out, "2.0") } +func TestDescribeODataClient_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListConsumedODataServicesFunc: func() ([]*model.ConsumedODataService, error) { + return nil, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeODataClient(ctx, ast.QualifiedName{Module: "MyModule", Name: "NoSuch"})) +} + +func TestShowODataClients_FilterByModule(t *testing.T) { + mod1 := mkModule("Alpha") + mod2 := mkModule("Beta") + svc1 := &model.ConsumedODataService{ + BaseElement: model.BaseElement{ID: nextID("cos")}, + ContainerID: mod1.ID, + Name: "AlphaSvc", + } + svc2 := &model.ConsumedODataService{ + BaseElement: model.BaseElement{ID: nextID("cos")}, + ContainerID: mod2.ID, + Name: "BetaSvc", + } + h := mkHierarchy(mod1, mod2) + withContainer(h, svc1.ContainerID, mod1.ID) + withContainer(h, svc2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListConsumedODataServicesFunc: func() ([]*model.ConsumedODataService, error) { + return []*model.ConsumedODataService{svc1, svc2}, nil + }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listODataClients(ctx, "Alpha")) + + out := buf.String() + assertContainsStr(t, out, "Alpha.AlphaSvc") + assertNotContainsStr(t, out, "Beta.BetaSvc") +} + +func TestShowODataServices_FilterByModule(t *testing.T) { + mod := mkModule("Sales") + svc := &model.PublishedODataService{ + BaseElement: model.BaseElement{ID: nextID("pos")}, + ContainerID: mod.ID, + Name: "SalesSvc", + } + h := mkHierarchy(mod) + withContainer(h, svc.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListPublishedODataServicesFunc: func() ([]*model.PublishedODataService, error) { + return []*model.PublishedODataService{svc}, nil + }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listODataServices(ctx, "Sales")) + assertContainsStr(t, buf.String(), "Sales.SalesSvc") +} + func TestDescribeODataService_Mock(t *testing.T) { mod := mkModule("MyModule") svc := &model.PublishedODataService{ @@ -124,3 +189,14 @@ func TestDescribeODataService_Mock(t *testing.T) { assertContainsStr(t, out, "create odata service") assertContainsStr(t, out, "MyModule.CatalogService") } + +func TestDescribeODataService_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListPublishedODataServicesFunc: func() ([]*model.PublishedODataService, error) { + return nil, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeODataService(ctx, ast.QualifiedName{Module: "X", Name: "NoSuch"})) +} diff --git a/mdl/executor/cmd_pages_mock_test.go b/mdl/executor/cmd_pages_mock_test.go index b9698e1b..356859b9 100644 --- a/mdl/executor/cmd_pages_mock_test.go +++ b/mdl/executor/cmd_pages_mock_test.go @@ -5,6 +5,7 @@ package executor import ( "testing" + "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend/mock" "github.com/mendixlabs/mxcli/sdk/pages" ) @@ -52,6 +53,91 @@ func TestShowPages_Mock_FilterByModule(t *testing.T) { assertContainsStr(t, out, "HR.EmployeeList") } +func TestShowSnippets_Mock_FilterByModule(t *testing.T) { + mod1 := mkModule("Sales") + mod2 := mkModule("HR") + snp1 := mkSnippet(mod1.ID, "OrderHeader") + snp2 := mkSnippet(mod2.ID, "EmployeeCard") + + h := mkHierarchy(mod1, mod2) + withContainer(h, snp1.ContainerID, mod1.ID) + withContainer(h, snp2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListSnippetsFunc: func() ([]*pages.Snippet, error) { return []*pages.Snippet{snp1, snp2}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listSnippets(ctx, "HR")) + + out := buf.String() + assertNotContainsStr(t, out, "Sales.OrderHeader") + assertContainsStr(t, out, "HR.EmployeeCard") +} + +func TestShowLayouts_Mock_FilterByModule(t *testing.T) { + mod1 := mkModule("Sales") + mod2 := mkModule("HR") + lay1 := mkLayout(mod1.ID, "SalesLayout") + lay2 := mkLayout(mod2.ID, "HRLayout") + + h := mkHierarchy(mod1, mod2) + withContainer(h, lay1.ContainerID, mod1.ID) + withContainer(h, lay2.ContainerID, mod2.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListLayoutsFunc: func() ([]*pages.Layout, error) { return []*pages.Layout{lay1, lay2}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listLayouts(ctx, "HR")) + + out := buf.String() + assertNotContainsStr(t, out, "Sales.SalesLayout") + assertContainsStr(t, out, "HR.HRLayout") +} + +func TestDescribePage_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListPagesFunc: func() ([]*pages.Page, error) { return []*pages.Page{}, nil }, + } + + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describePage(ctx, ast.QualifiedName{Module: "MyModule", Name: "NonExistent"})) +} + +func TestDescribeSnippet_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListSnippetsFunc: func() ([]*pages.Snippet, error) { return []*pages.Snippet{}, nil }, + } + + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeSnippet(ctx, ast.QualifiedName{Module: "MyModule", Name: "NonExistent"})) +} + +func TestDescribeLayout_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListLayoutsFunc: func() ([]*pages.Layout, error) { return []*pages.Layout{}, nil }, + } + + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeLayout(ctx, ast.QualifiedName{Module: "MyModule", Name: "NonExistent"})) +} + func TestShowSnippets_Mock(t *testing.T) { mod := mkModule("MyModule") snp := mkSnippet(mod.ID, "Header") diff --git a/mdl/executor/cmd_published_rest_mock_test.go b/mdl/executor/cmd_published_rest_mock_test.go index 1ba51582..22c33f1b 100644 --- a/mdl/executor/cmd_published_rest_mock_test.go +++ b/mdl/executor/cmd_published_rest_mock_test.go @@ -68,3 +68,37 @@ func TestDescribePublishedRestService_Mock(t *testing.T) { assertContainsStr(t, out, "create published rest service") assertContainsStr(t, out, "MyModule.OrderAPI") } + +func TestDescribePublishedRestService_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListPublishedRestServicesFunc: func() ([]*model.PublishedRestService, error) { + return nil, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describePublishedRestService(ctx, ast.QualifiedName{Module: "X", Name: "NoSuch"})) +} + +func TestShowPublishedRestServices_FilterByModule(t *testing.T) { + mod := mkModule("Sales") + svc := &model.PublishedRestService{ + BaseElement: model.BaseElement{ID: nextID("prs")}, + ContainerID: mod.ID, + Name: "SalesAPI", + Path: "/rest/sales/v1", + } + h := mkHierarchy(mod) + withContainer(h, svc.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListPublishedRestServicesFunc: func() ([]*model.PublishedRestService, error) { + return []*model.PublishedRestService{svc}, nil + }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listPublishedRestServices(ctx, "Sales")) + assertContainsStr(t, buf.String(), "Sales.SalesAPI") +} 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_rest_clients_mock_test.go b/mdl/executor/cmd_rest_clients_mock_test.go index f9517f5b..4d35749c 100644 --- a/mdl/executor/cmd_rest_clients_mock_test.go +++ b/mdl/executor/cmd_rest_clients_mock_test.go @@ -61,3 +61,37 @@ func TestDescribeRestClient_Mock(t *testing.T) { assertContainsStr(t, out, "create rest client") assertContainsStr(t, out, "MyModule.WeatherAPI") } + +func TestDescribeRestClient_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListConsumedRestServicesFunc: func() ([]*model.ConsumedRestService, error) { + return nil, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeRestClient(ctx, ast.QualifiedName{Module: "X", Name: "NoSuch"})) +} + +func TestShowRestClients_FilterByModule(t *testing.T) { + mod := mkModule("Integrations") + svc := &model.ConsumedRestService{ + BaseElement: model.BaseElement{ID: nextID("crs")}, + ContainerID: mod.ID, + Name: "PaymentAPI", + BaseUrl: "https://api.payment.com", + } + h := mkHierarchy(mod) + withContainer(h, svc.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListConsumedRestServicesFunc: func() ([]*model.ConsumedRestService, error) { + return []*model.ConsumedRestService{svc}, nil + }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listRestClients(ctx, "Integrations")) + assertContainsStr(t, buf.String(), "Integrations.PaymentAPI") +} diff --git a/mdl/executor/cmd_security_mock_test.go b/mdl/executor/cmd_security_mock_test.go index 4bfdc1e5..fa6e71fc 100644 --- a/mdl/executor/cmd_security_mock_test.go +++ b/mdl/executor/cmd_security_mock_test.go @@ -7,6 +7,8 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/sdk/microflows" + "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/security" ) @@ -172,3 +174,110 @@ func TestDescribeDemoUser_Mock(t *testing.T) { assertNoError(t, describeDemoUser(ctx, "demo_admin")) assertContainsStr(t, buf.String(), "create demo user") } + +func TestShowModuleRoles_Mock_FilterByModule(t *testing.T) { + mod1 := mkModule("Sales") + mod2 := mkModule("HR") + h := mkHierarchy(mod1, mod2) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModuleSecurityFunc: func() ([]*security.ModuleSecurity, error) { + return []*security.ModuleSecurity{ + {ContainerID: mod1.ID, ModuleRoles: []*security.ModuleRole{{Name: "Manager"}}}, + {ContainerID: mod2.ID, ModuleRoles: []*security.ModuleRole{{Name: "Employee"}}}, + }, nil + }, + } + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listModuleRoles(ctx, "HR")) + + out := buf.String() + assertNotContainsStr(t, out, "Sales") + assertContainsStr(t, out, "HR") + assertContainsStr(t, out, "Employee") +} + +func TestDescribeModuleRole_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModuleSecurityFunc: func() ([]*security.ModuleSecurity, error) { + return []*security.ModuleSecurity{{ + ContainerID: mod.ID, + ModuleRoles: []*security.ModuleRole{{Name: "Admin"}}, + }}, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, describeModuleRole(ctx, ast.QualifiedName{Module: "MyModule", Name: "NonExistent"})) +} + +func TestDescribeUserRole_Mock_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + GetProjectSecurityFunc: func() (*security.ProjectSecurity, error) { + return &security.ProjectSecurity{ + UserRoles: []*security.UserRole{{Name: "Admin"}}, + }, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeUserRole(ctx, ast.QualifiedName{Name: "NonExistent"})) +} + +func TestDescribeDemoUser_Mock_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + GetProjectSecurityFunc: func() (*security.ProjectSecurity, error) { + return &security.ProjectSecurity{ + EnableDemoUsers: true, + DemoUsers: []*security.DemoUser{{UserName: "demo_admin"}}, + }, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeDemoUser(ctx, "nonexistent")) +} + +func TestShowAccessOnEntity_Mock_NilName(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, listAccessOnEntity(ctx, nil)) +} + +func TestShowAccessOnMicroflow_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListMicroflowsFunc: func() ([]*microflows.Microflow, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, listAccessOnMicroflow(ctx, &ast.QualifiedName{Module: "MyModule", Name: "NonExistent"})) +} + +func TestShowAccessOnPage_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListPagesFunc: func() ([]*pages.Page, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, listAccessOnPage(ctx, &ast.QualifiedName{Module: "MyModule", Name: "NonExistent"})) +} + +func TestShowAccessOnWorkflow_Mock_Unsupported(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, listAccessOnWorkflow(ctx, &ast.QualifiedName{Module: "MyModule", Name: "SomeWorkflow"})) +} diff --git a/mdl/executor/cmd_settings_mock_test.go b/mdl/executor/cmd_settings_mock_test.go index 02e9e4ba..d33aecaa 100644 --- a/mdl/executor/cmd_settings_mock_test.go +++ b/mdl/executor/cmd_settings_mock_test.go @@ -3,6 +3,7 @@ package executor import ( + "fmt" "testing" "github.com/mendixlabs/mxcli/mdl/backend/mock" @@ -46,3 +47,47 @@ func TestDescribeSettings_Mock(t *testing.T) { assertNoError(t, describeSettings(ctx)) assertContainsStr(t, buf.String(), "alter settings") } + +func TestShowSettings_NotConnected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, listSettings(ctx)) +} + +func TestDescribeSettings_NotConnected(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return false }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeSettings(ctx)) +} + +func TestShowSettings_BackendError(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + GetProjectSettingsFunc: func() (*model.ProjectSettings, error) { + return nil, fmt.Errorf("connection lost") + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, listSettings(ctx)) +} + +func TestShowSettings_JSON(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + GetProjectSettingsFunc: func() (*model.ProjectSettings, error) { + return &model.ProjectSettings{ + Model: &model.ModelSettings{ + HashAlgorithm: "BCrypt", + JavaVersion: "17", + }, + }, nil + }, + } + ctx, buf := newMockCtx(t, withBackend(mb), withFormat(FormatJSON)) + assertNoError(t, listSettings(ctx)) + assertValidJSON(t, buf.String()) +} 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. diff --git a/mdl/executor/cmd_workflows_mock_test.go b/mdl/executor/cmd_workflows_mock_test.go index a69ec9e0..724f711f 100644 --- a/mdl/executor/cmd_workflows_mock_test.go +++ b/mdl/executor/cmd_workflows_mock_test.go @@ -50,3 +50,29 @@ func TestDescribeWorkflow_Mock(t *testing.T) { assertContainsStr(t, out, "workflow") assertContainsStr(t, out, "Sales.ApproveOrder") } + +func TestDescribeWorkflow_NotFound(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListWorkflowsFunc: func() ([]*workflows.Workflow, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, describeWorkflow(ctx, ast.QualifiedName{Module: "X", Name: "NoSuch"})) +} + +func TestShowWorkflows_FilterByModule(t *testing.T) { + mod := mkModule("Sales") + wf := mkWorkflow(mod.ID, "ApproveOrder") + + h := mkHierarchy(mod) + withContainer(h, wf.ContainerID, mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListWorkflowsFunc: func() ([]*workflows.Workflow, error) { return []*workflows.Workflow{wf}, nil }, + } + + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertNoError(t, listWorkflows(ctx, "Sales")) + assertContainsStr(t, buf.String(), "Sales.ApproveOrder") +} diff --git a/mdl/executor/cmd_write_handlers_mock_test.go b/mdl/executor/cmd_write_handlers_mock_test.go index 5d442758..7e965347 100644 --- a/mdl/executor/cmd_write_handlers_mock_test.go +++ b/mdl/executor/cmd_write_handlers_mock_test.go @@ -326,3 +326,112 @@ func TestExecDropFolder_Mock(t *testing.T) { t.Fatal("DeleteFolderFunc was not called") } } + +func TestExecCreateModule_Mock_AlreadyExists(t *testing.T) { + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { + return []*model.Module{{Name: "Existing"}}, nil + }, + } + ctx, buf := newMockCtx(t, withBackend(mb)) + assertNoError(t, execCreateModule(ctx, &ast.CreateModuleStmt{Name: "Existing"})) + assertContainsStr(t, buf.String(), "already exists") +} + +func TestExecDropEnumeration_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListEnumerationsFunc: func() ([]*model.Enumeration, error) { + return nil, nil + }, + ListModulesFunc: func() ([]*model.Module, error) { + return []*model.Module{mod}, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, execDropEnumeration(ctx, &ast.DropEnumerationStmt{ + Name: ast.QualifiedName{Module: "MyModule", Name: "NonExistent"}, + })) +} + +func TestExecDropEntity_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + dm := mkDomainModel(mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { + return []*model.Module{mod}, nil + }, + GetDomainModelFunc: func(moduleID model.ID) (*domainmodel.DomainModel, error) { + return dm, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, execDropEntity(ctx, &ast.DropEntityStmt{ + Name: ast.QualifiedName{Module: "MyModule", Name: "NonExistent"}, + })) +} + +func TestExecDropMicroflow_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListMicroflowsFunc: func() ([]*microflows.Microflow, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, execDropMicroflow(ctx, &ast.DropMicroflowStmt{ + Name: ast.QualifiedName{Module: "MyModule", Name: "NonExistent"}, + })) +} + +func TestExecDropPage_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListPagesFunc: func() ([]*pages.Page, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, execDropPage(ctx, &ast.DropPageStmt{ + Name: ast.QualifiedName{Module: "MyModule", Name: "NonExistent"}, + })) +} + +func TestExecDropSnippet_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + h := mkHierarchy(mod) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListSnippetsFunc: func() ([]*pages.Snippet, error) { return nil, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + assertError(t, execDropSnippet(ctx, &ast.DropSnippetStmt{ + Name: ast.QualifiedName{Module: "MyModule", Name: "NonExistent"}, + })) +} + +func TestExecDropAssociation_Mock_NotFound(t *testing.T) { + mod := mkModule("MyModule") + dm := mkDomainModel(mod.ID) + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { + return []*model.Module{mod}, nil + }, + GetDomainModelFunc: func(moduleID model.ID) (*domainmodel.DomainModel, error) { + return dm, nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + assertError(t, execDropAssociation(ctx, &ast.DropAssociationStmt{ + Name: ast.QualifiedName{Module: "MyModule", Name: "NonExistent"}, + })) +}