diff --git a/cmd/cli/app/project/fixture/mock_project_create.json b/cmd/cli/app/project/fixture/mock_project_create.json new file mode 100644 index 0000000000..878730833b --- /dev/null +++ b/cmd/cli/app/project/fixture/mock_project_create.json @@ -0,0 +1,10 @@ +{ + "project": { + "projectId": "12345678-1234-1234-1234-123456789012", + "name": "mock-project", + "description": "A newly created mock project", + "createdAt": "2026-04-24T14:38:18.810108Z", + "updatedAt": "2026-04-24T14:38:18.810108Z", + "displayName": "mock-project" + } +} \ No newline at end of file diff --git a/cmd/cli/app/project/fixture/mock_project_delete.json b/cmd/cli/app/project/fixture/mock_project_delete.json new file mode 100644 index 0000000000..98fb0707af --- /dev/null +++ b/cmd/cli/app/project/fixture/mock_project_delete.json @@ -0,0 +1,3 @@ +{ + "projectId": "12345678-1234-1234-1234-123456789012" +} \ No newline at end of file diff --git a/cmd/cli/app/project/fixture/mock_project_list.json b/cmd/cli/app/project/fixture/mock_project_list.json new file mode 100644 index 0000000000..1ce4b42a9f --- /dev/null +++ b/cmd/cli/app/project/fixture/mock_project_list.json @@ -0,0 +1,28 @@ +{ + "projects": [ + { + "projectId": "00000000-0000-0000-0000-000000000001", + "name": "mock-project-alpha", + "description": "First mock project for testing.", + "createdAt": "2024-01-01T12:00:00.000000Z", + "updatedAt": "2024-01-01T12:00:00.000000Z", + "displayName": "mock-project-alpha" + }, + { + "projectId": "00000000-0000-0000-0000-000000000002", + "name": "mock-project-beta", + "description": "Second mock project for testing.", + "createdAt": "2024-01-02T12:00:00.000000Z", + "updatedAt": "2024-01-02T12:00:00.000000Z", + "displayName": "mock-project-beta" + }, + { + "projectId": "00000000-0000-0000-0000-000000000003", + "name": "mock-project-gamma", + "description": "Third mock project for testing.", + "createdAt": "2024-01-03T12:00:00.000000Z", + "updatedAt": "2024-01-03T12:00:00.000000Z", + "displayName": "mock-project-gamma" + } + ] +} \ No newline at end of file diff --git a/cmd/cli/app/project/project_create.go b/cmd/cli/app/project/project_create.go index b8ba9e5ace..817c4007e8 100644 --- a/cmd/cli/app/project/project_create.go +++ b/cmd/cli/app/project/project_create.go @@ -4,13 +4,11 @@ package project import ( - "context" "fmt" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "github.com/mindersec/minder/cmd/cli/app" "github.com/mindersec/minder/internal/util" @@ -24,13 +22,23 @@ import ( var projectCreateCmd = &cobra.Command{ Use: "create", Short: "Create a sub-project within a minder control plane", - Long: `The list command lists the projects available to you within a minder control plane.`, - RunE: cli.GRPCClientWrapRunE(createCommand), + Long: `The create command creates a sub-project within a minder control plane.`, + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return cli.MessageAndError("Error binding flags", err) + } + return nil + }, + RunE: createCommand, } -// listCommand is the command for listing projects -func createCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { - client := minderv1.NewProjectsServiceClient(conn) +// createCommand is the command for listing projects +func createCommand(cmd *cobra.Command, _ []string) error { + client, cleanup, err := GetProjectsClient(cmd) + if err != nil { + return cli.MessageAndError("Error getting client", err) + } + defer cleanup() format := viper.GetString("output") project := viper.GetString("project") @@ -40,7 +48,7 @@ func createCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *gr // See https://github.com/spf13/cobra/issues/340#issuecomment-374617413 cmd.SilenceUsage = true - resp, err := client.CreateProject(ctx, &minderv1.CreateProjectRequest{ + resp, err := client.CreateProject(cmd.Context(), &minderv1.CreateProjectRequest{ Context: &minderv1.Context{ Project: &project, }, diff --git a/cmd/cli/app/project/project_create_test.go b/cmd/cli/app/project/project_create_test.go new file mode 100644 index 0000000000..3fddbebb8a --- /dev/null +++ b/cmd/cli/app/project/project_create_test.go @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package project + +import ( + "context" + "testing" + + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" + mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" +) + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestProjectCreateCommand(t *testing.T) { + const ( + projectName = "mock-project" + ) + tests := []cli.CmdTestCase{ + { + Name: "create project table output", + Args: []string{"project", "create", "-n", projectName, "-o", "table"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockProjectsServiceClient(ctrl) + + mockResp := &minderv1.CreateProjectResponse{} + cli.LoadFixture(t, "mock_project_create.json", mockResp) + + client.EXPECT(). + CreateProject(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.ProjectsServiceClient](context.Background(), client) + }, + GoldenFileName: "create_table.txt", + }, + { + Name: "create project json output", + Args: []string{"project", "create", "-n", projectName, "-o", "json"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockProjectsServiceClient(ctrl) + + mockResp := &minderv1.CreateProjectResponse{} + cli.LoadFixture(t, "mock_project_create.json", mockResp) + + client.EXPECT(). + CreateProject(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.ProjectsServiceClient](context.Background(), client) + }, + GoldenFileName: "create_json.txt", + }, + { + Name: "create project yaml output", + Args: []string{"project", "create", "-n", projectName, "-o", "yaml"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockProjectsServiceClient(ctrl) + + mockResp := &minderv1.CreateProjectResponse{} + cli.LoadFixture(t, "mock_project_create.json", mockResp) + + client.EXPECT(). + CreateProject(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.ProjectsServiceClient](context.Background(), client) + }, + GoldenFileName: "create_yaml.txt", + }, + { + Name: "fails when missing name flag", + Args: []string{"project", "create"}, + ExpectedError: "required flag(s) \"name\" not set", + }, + { + Name: "server error handling", + Args: []string{"project", "create", "-n", projectName}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockProjectsServiceClient(ctrl) + + client.EXPECT(). + CreateProject(gomock.Any(), gomock.Any()). + Return(nil, status.Error(codes.Internal, "internal server error")). + Times(1) + + return cli.WithRPCClient[minderv1.ProjectsServiceClient](context.Background(), client) + }, + ExpectedError: "internal server error", + }, + } + + cli.RunCmdTests(t, tests, ProjectCmd) +} diff --git a/cmd/cli/app/project/project_delete.go b/cmd/cli/app/project/project_delete.go index 2a05e0c94b..10e8991336 100644 --- a/cmd/cli/app/project/project_delete.go +++ b/cmd/cli/app/project/project_delete.go @@ -4,11 +4,8 @@ package project import ( - "context" - "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "github.com/mindersec/minder/internal/util/cli" minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" @@ -19,12 +16,22 @@ var projectDeleteCmd = &cobra.Command{ Use: "delete", Short: "Delete a sub-project within a minder control plane", Long: `Delete a sub-project within a minder control plane`, - RunE: cli.GRPCClientWrapRunE(deleteCommand), + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return cli.MessageAndError("Error binding flags", err) + } + return nil + }, + RunE: deleteCommand, } -// listCommand is the command for listing projects -func deleteCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { - client := minderv1.NewProjectsServiceClient(conn) +// deleteCommand is the command for listing projects +func deleteCommand(cmd *cobra.Command, _ []string) error { + client, cleanup, err := GetProjectsClient(cmd) + if err != nil { + return cli.MessageAndError("Error getting client", err) + } + defer cleanup() project := viper.GetString("project") @@ -32,7 +39,7 @@ func deleteCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *gr // See https://github.com/spf13/cobra/issues/340#issuecomment-374617413 cmd.SilenceUsage = true - resp, err := client.DeleteProject(ctx, &minderv1.DeleteProjectRequest{ + resp, err := client.DeleteProject(cmd.Context(), &minderv1.DeleteProjectRequest{ Context: &minderv1.Context{ Project: &project, }, diff --git a/cmd/cli/app/project/project_delete_test.go b/cmd/cli/app/project/project_delete_test.go new file mode 100644 index 0000000000..494ecfb1bd --- /dev/null +++ b/cmd/cli/app/project/project_delete_test.go @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package project + +import ( + "context" + "testing" + + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" + mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" +) + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestProjectDeleteCommand(t *testing.T) { + const ( + projectID = "12345678-1234-1234-1234-123456789012" + ) + + tests := []cli.CmdTestCase{ + { + Name: "delete project success", + Args: []string{"project", "delete", "-j", projectID}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockProjectsServiceClient(ctrl) + + mockResp := &minderv1.DeleteProjectResponse{} + cli.LoadFixture(t, "mock_project_delete.json", mockResp) + + client.EXPECT(). + DeleteProject(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.ProjectsServiceClient](context.Background(), client) + }, + GoldenFileName: "delete_success.txt", + }, + { + Name: "fails when missing project flag", + Args: []string{"project", "delete"}, + ExpectedError: "required flag(s) \"project\" not set", + }, + { + Name: "server error handling", + Args: []string{"project", "delete", "-j", projectID}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockProjectsServiceClient(ctrl) + + client.EXPECT(). + DeleteProject(gomock.Any(), gomock.Any()). + Return(nil, status.Error(codes.Internal, "internal server error")). + Times(1) + + return cli.WithRPCClient[minderv1.ProjectsServiceClient](context.Background(), client) + }, + ExpectedError: "internal server error", + }, + } + + cli.RunCmdTests(t, tests, ProjectCmd) +} diff --git a/cmd/cli/app/project/project_list.go b/cmd/cli/app/project/project_list.go index abcdf1acbd..13e2e99790 100644 --- a/cmd/cli/app/project/project_list.go +++ b/cmd/cli/app/project/project_list.go @@ -4,13 +4,11 @@ package project import ( - "context" "fmt" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "github.com/mindersec/minder/cmd/cli/app" "github.com/mindersec/minder/internal/util" @@ -25,12 +23,22 @@ var projectListCmd = &cobra.Command{ Use: "list", Short: "List the projects available to you within a minder control plane", Long: `The list command lists the projects available to you within a minder control plane.`, - RunE: cli.GRPCClientWrapRunE(listCommand), + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return cli.MessageAndError("Error binding flags", err) + } + return nil + }, + RunE: listCommand, } // listCommand is the command for listing projects -func listCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { - client := minderv1.NewProjectsServiceClient(conn) +func listCommand(cmd *cobra.Command, _ []string) error { + client, cleanup, err := GetProjectsClient(cmd) + if err != nil { + return cli.MessageAndError("Error getting client", err) + } + defer cleanup() format := viper.GetString("output") @@ -38,7 +46,7 @@ func listCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc // See https://github.com/spf13/cobra/issues/340#issuecomment-374617413 cmd.SilenceUsage = true - resp, err := client.ListProjects(ctx, &minderv1.ListProjectsRequest{}) + resp, err := client.ListProjects(cmd.Context(), &minderv1.ListProjectsRequest{}) if err != nil { return cli.MessageAndError("Error listing projects", err) } diff --git a/cmd/cli/app/project/project_list_test.go b/cmd/cli/app/project/project_list_test.go new file mode 100644 index 0000000000..03b3603f45 --- /dev/null +++ b/cmd/cli/app/project/project_list_test.go @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package project + +import ( + "context" + "testing" + + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" + mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" +) + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestProjectListCommand(t *testing.T) { + tests := []cli.CmdTestCase{ + { + Name: "list projects table output", + Args: []string{"project", "list", "-o", "table"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockProjectsServiceClient(ctrl) + + mockResp := &minderv1.ListProjectsResponse{} + cli.LoadFixture(t, "mock_project_list.json", mockResp) + + client.EXPECT(). + ListProjects(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.ProjectsServiceClient](context.Background(), client) + }, + GoldenFileName: "list_table.txt", + }, + { + Name: "list projects json output", + Args: []string{"project", "list", "-o", "json"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockProjectsServiceClient(ctrl) + + mockResp := &minderv1.ListProjectsResponse{} + cli.LoadFixture(t, "mock_project_list.json", mockResp) + + client.EXPECT(). + ListProjects(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.ProjectsServiceClient](context.Background(), client) + }, + GoldenFileName: "list_json.txt", + }, + { + Name: "list projects yaml output", + Args: []string{"project", "list", "-o", "yaml"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockProjectsServiceClient(ctrl) + + mockResp := &minderv1.ListProjectsResponse{} + cli.LoadFixture(t, "mock_project_list.json", mockResp) + + client.EXPECT(). + ListProjects(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.ProjectsServiceClient](context.Background(), client) + }, + GoldenFileName: "list_yaml.txt", + }, + { + Name: "server error handling", + Args: []string{"project", "list"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockProjectsServiceClient(ctrl) + + client.EXPECT(). + ListProjects(gomock.Any(), gomock.Any()). + Return(nil, status.Error(codes.Internal, "internal server error")). + Times(1) + + return cli.WithRPCClient[minderv1.ProjectsServiceClient](context.Background(), client) + }, + ExpectedError: "internal server error", + }, + } + + cli.RunCmdTests(t, tests, ProjectCmd) +} diff --git a/cmd/cli/app/project/project_test.go b/cmd/cli/app/project/project_test.go new file mode 100644 index 0000000000..4c820b96b2 --- /dev/null +++ b/cmd/cli/app/project/project_test.go @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package project + +import ( + "testing" + + "github.com/mindersec/minder/internal/util/cli" +) + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestProjectRootCommand(t *testing.T) { + tests := []cli.CmdTestCase{ + { + Name: "project root command prints usage", + Args: []string{"project"}, + GoldenFileName: "project_root_usage.txt", + }, + { + Name: "project help flag prints usage", + Args: []string{"project", "-h"}, + GoldenFileName: "project_help_usage.txt", + }, + } + + cli.RunCmdTests(t, tests, ProjectCmd) +} diff --git a/cmd/cli/app/project/role/fixture/mock_role_deny.json b/cmd/cli/app/project/role/fixture/mock_role_deny.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/cmd/cli/app/project/role/fixture/mock_role_deny.json @@ -0,0 +1 @@ +{} diff --git a/cmd/cli/app/project/role/fixture/mock_role_grant.json b/cmd/cli/app/project/role/fixture/mock_role_grant.json new file mode 100644 index 0000000000..234158f0c2 --- /dev/null +++ b/cmd/cli/app/project/role/fixture/mock_role_grant.json @@ -0,0 +1,8 @@ +{ + "roleAssignment": { + "displayName": "Mock User", + "subject": "mock-subject-123", + "role": "admin", + "project": "00000000-0000-0000-0000-000000000001" + } +} diff --git a/cmd/cli/app/project/role/fixture/mock_role_grant_invite.json b/cmd/cli/app/project/role/fixture/mock_role_grant_invite.json new file mode 100644 index 0000000000..1aad0323b5 --- /dev/null +++ b/cmd/cli/app/project/role/fixture/mock_role_grant_invite.json @@ -0,0 +1,10 @@ +{ + "invitation": { + "email": "test@example.com", + "role": "viewer", + "sponsorDisplay": "Admin User", + "expiresAt": "2026-05-01T12:00:00Z", + "code": "MOCK-INVITE-CODE", + "inviteUrl": "https://minder.example.com/invite/MOCK-INVITE-CODE" + } +} diff --git a/cmd/cli/app/project/role/fixture/mock_role_grant_list.json b/cmd/cli/app/project/role/fixture/mock_role_grant_list.json new file mode 100644 index 0000000000..98960a7e82 --- /dev/null +++ b/cmd/cli/app/project/role/fixture/mock_role_grant_list.json @@ -0,0 +1,18 @@ +{ + "roleAssignments": [ + { + "displayName": "Mock Admin", + "subject": "admin-subject-123", + "role": "admin", + "project": "00000000-0000-0000-0000-000000000001" + } + ], + "invitations": [ + { + "email": "pending@example.com", + "role": "viewer", + "sponsorDisplay": "Mock Admin", + "expiresAt": "2026-05-01T12:00:00Z" + } + ] +} diff --git a/cmd/cli/app/project/role/fixture/mock_role_list.json b/cmd/cli/app/project/role/fixture/mock_role_list.json new file mode 100644 index 0000000000..38175808db --- /dev/null +++ b/cmd/cli/app/project/role/fixture/mock_role_list.json @@ -0,0 +1,12 @@ +{ + "roles": [ + { + "name": "admin", + "description": "Administrator role with full access" + }, + { + "name": "viewer", + "description": "Read-only access role" + } + ] +} diff --git a/cmd/cli/app/project/role/fixture/mock_role_update.json b/cmd/cli/app/project/role/fixture/mock_role_update.json new file mode 100644 index 0000000000..97310e043f --- /dev/null +++ b/cmd/cli/app/project/role/fixture/mock_role_update.json @@ -0,0 +1,10 @@ +{ + "roleAssignments": [ + { + "displayName": "Mock User", + "subject": "mock-subject-123", + "role": "admin", + "project": "00000000-0000-0000-0000-000000000001" + } + ] +} diff --git a/cmd/cli/app/project/role/fixture/mock_role_update_invite.json b/cmd/cli/app/project/role/fixture/mock_role_update_invite.json new file mode 100644 index 0000000000..7ad8812cc5 --- /dev/null +++ b/cmd/cli/app/project/role/fixture/mock_role_update_invite.json @@ -0,0 +1,11 @@ +{ + "invitations": [ + { + "email": "test@example.com", + "role": "admin", + "project": "00000000-0000-0000-0000-000000000001", + "code": "MOCK-UPDATE-INVITE", + "inviteUrl": "https://minder.example.com/invite/MOCK-UPDATE-INVITE" + } + ] +} diff --git a/cmd/cli/app/project/role/role.go b/cmd/cli/app/project/role/role.go index 7149c231e7..7a567a5e29 100644 --- a/cmd/cli/app/project/role/role.go +++ b/cmd/cli/app/project/role/role.go @@ -6,8 +6,11 @@ package role import ( "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/mindersec/minder/cmd/cli/app/project" + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" ) // RoleCmd is the root command for the project subcommands @@ -24,3 +27,26 @@ func init() { project.ProjectCmd.AddCommand(RoleCmd) RoleCmd.PersistentFlags().StringP("project", "j", "", "ID of the project") } + +// GetPermissionsClient is a helper to get the PermissionsServiceClient, supporting mocks +func GetPermissionsClient(cmd *cobra.Command) (minderv1.PermissionsServiceClient, func(), error) { + ctx, cancel := cli.GetAppContext(cmd.Context(), viper.GetViper()) + cmd.SetContext(ctx) + + if mockClient, ok := cli.GetRPCClient[minderv1.PermissionsServiceClient](ctx); ok { + return mockClient, func() { cancel() }, nil + } + + conn, err := cli.GrpcForCommand(cmd, viper.GetViper()) + if err != nil { + cancel() + return nil, nil, err + } + + client := minderv1.NewPermissionsServiceClient(conn) + + return client, func() { + cancel() + _ = conn.Close() + }, nil +} diff --git a/cmd/cli/app/project/role/role_deny.go b/cmd/cli/app/project/role/role_deny.go index 956cf46ef9..db44ca3208 100644 --- a/cmd/cli/app/project/role/role_deny.go +++ b/cmd/cli/app/project/role/role_deny.go @@ -4,12 +4,10 @@ package role import ( - "context" "os" "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "github.com/mindersec/minder/internal/util/cli" minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" @@ -20,12 +18,22 @@ var denyCmd = &cobra.Command{ Short: "Deny a role to a subject on a project within the minder control plane", Long: `The minder project role deny command removes a user from a role grant on a particular project.`, - RunE: cli.GRPCClientWrapRunE(DenyCommand), + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return cli.MessageAndError("Error binding flags", err) + } + return nil + }, + RunE: DenyCommand, } // DenyCommand is the command for removing a role assignment from a project -func DenyCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { - client := minderv1.NewPermissionsServiceClient(conn) +func DenyCommand(cmd *cobra.Command, _ []string) error { + client, cleanup, err := GetPermissionsClient(cmd) + if err != nil { + return cli.MessageAndError("Error getting client", err) + } + defer cleanup() sub := viper.GetString("sub") r := viper.GetString("role") @@ -52,7 +60,7 @@ func DenyCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc successMsg = "Invite deleted successfully." } - _, err := client.RemoveRole(ctx, &minderv1.RemoveRoleRequest{ + _, err = client.RemoveRole(cmd.Context(), &minderv1.RemoveRoleRequest{ Context: &minderv1.Context{ Project: &project, }, diff --git a/cmd/cli/app/project/role/role_deny_test.go b/cmd/cli/app/project/role/role_deny_test.go new file mode 100644 index 0000000000..a4c0841ee9 --- /dev/null +++ b/cmd/cli/app/project/role/role_deny_test.go @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package role + +import ( + "context" + "testing" + + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" + mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" +) + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestRoleDenyCommand(t *testing.T) { + const ( + projectSub = "00000000-0000-0000-0000-000000000001" + mockProjectID = "12345678-1234-1234-1234-123456789012" + ) + + tests := []cli.CmdTestCase{ + { + Name: "deny role success", + Args: []string{"project", "role", "deny", "-s", projectSub, "-r", "admin", "-j", mockProjectID}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.RemoveRoleResponse{} + cli.LoadFixture(t, "mock_role_deny.json", mockResp) + + client.EXPECT(). + RemoveRole(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "deny_success.txt", + }, + { + Name: "deny role via email success", + Args: []string{"project", "role", "deny", "-e", "test@example.com", "-r", "admin", "-j", mockProjectID}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.RemoveRoleResponse{} + cli.LoadFixture(t, "mock_role_deny.json", mockResp) + + client.EXPECT(). + RemoveRole(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "deny_email_success.txt", + }, + { + Name: "fails when missing required flags (sub or email)", + Args: []string{"project", "role", "deny", "-r", "admin"}, + ExpectedError: "at least one of the flags in the group [sub email] is required", + }, + { + Name: "server error handling", + Args: []string{"project", "role", "deny", "-s", projectSub, "-r", "admin"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + client.EXPECT(). + RemoveRole(gomock.Any(), gomock.Any()). + Return(nil, status.Error(codes.Internal, "internal server error")). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + ExpectedError: "internal server error", + }, + } + + cli.RunCmdTests(t, tests, RoleCmd) +} diff --git a/cmd/cli/app/project/role/role_grant.go b/cmd/cli/app/project/role/role_grant.go index 14287c59e3..cc611372ac 100644 --- a/cmd/cli/app/project/role/role_grant.go +++ b/cmd/cli/app/project/role/role_grant.go @@ -4,7 +4,6 @@ package role import ( - "context" "fmt" "os" "strings" @@ -12,7 +11,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "github.com/mindersec/minder/cmd/cli/app" "github.com/mindersec/minder/internal/util" @@ -25,12 +23,22 @@ var grantCmd = &cobra.Command{ Short: "Grant a role to a subject on a project within the minder control plane", Long: `The minder project role grant command allows one to grant a role to a user (subject) on a particular project.`, - RunE: cli.GRPCClientWrapRunE(GrantCommand), + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return cli.MessageAndError("Error binding flags", err) + } + return nil + }, + RunE: GrantCommand, } // GrantCommand is the command for granting roles -func GrantCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { - client := minderv1.NewPermissionsServiceClient(conn) +func GrantCommand(cmd *cobra.Command, _ []string) error { + client, cleanup, err := GetPermissionsClient(cmd) + if err != nil { + return cli.MessageAndError("Error getting client", err) + } + defer cleanup() sub := viper.GetString("sub") r := viper.GetString("role") @@ -63,7 +71,7 @@ func GrantCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grp successMsg = "Invite created successfully." } - resp, err := client.AssignRole(ctx, &minderv1.AssignRoleRequest{ + resp, err := client.AssignRole(cmd.Context(), &minderv1.AssignRoleRequest{ Context: &minderv1.Context{ Project: &project, }, diff --git a/cmd/cli/app/project/role/role_grant_list.go b/cmd/cli/app/project/role/role_grant_list.go index 611ed696b0..b56b0ae26e 100644 --- a/cmd/cli/app/project/role/role_grant_list.go +++ b/cmd/cli/app/project/role/role_grant_list.go @@ -4,7 +4,6 @@ package role import ( - "context" "fmt" "io" "strings" @@ -12,7 +11,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "github.com/mindersec/minder/cmd/cli/app" "github.com/mindersec/minder/internal/util" @@ -27,15 +25,26 @@ var grantListCmd = &cobra.Command{ Short: "List role grants within a given project", Long: `The minder project role grant list command lists all role grants on a particular project.`, - RunE: cli.GRPCClientWrapRunE(GrantListCommand), + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return cli.MessageAndError("Error binding flags", err) + } + return nil + }, + RunE: GrantListCommand, } // GrantListCommand is the command for listing grants -func GrantListCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { - client := minderv1.NewPermissionsServiceClient(conn) +func GrantListCommand(cmd *cobra.Command, _ []string) error { + client, cleanup, err := GetPermissionsClient(cmd) + if err != nil { + return cli.MessageAndError("Error getting client", err) + } + defer cleanup() project := viper.GetString("project") format := viper.GetString("output") + // Ensure the output format is supported if !app.IsOutputFormatSupported(format) { return cli.MessageAndError(fmt.Sprintf("Output format %s not supported", format), fmt.Errorf("invalid argument")) @@ -45,7 +54,7 @@ func GrantListCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn // See https://github.com/spf13/cobra/issues/340#issuecomment-374617413 cmd.SilenceUsage = true - resp, err := client.ListRoleAssignments(ctx, &minderv1.ListRoleAssignmentsRequest{ + resp, err := client.ListRoleAssignments(cmd.Context(), &minderv1.ListRoleAssignmentsRequest{ Context: &minderv1.Context{ Project: &project, }, @@ -74,11 +83,11 @@ func GrantListCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn } t.Render() if len(resp.Invitations) > 0 { - t := initializeTableForGrantListInvitations(cmd.OutOrStdout()) + t2 := initializeTableForGrantListInvitations(cmd.OutOrStdout()) for _, r := range resp.Invitations { - t.AddRow(r.Email, r.Role, r.SponsorDisplay, r.ExpiresAt.AsTime().Format(time.RFC3339)) + t2.AddRow(r.Email, r.Role, r.SponsorDisplay, r.ExpiresAt.AsTime().Format(time.RFC3339)) } - t.Render() + t2.Render() } else { cmd.Println("No pending invitations found.") } diff --git a/cmd/cli/app/project/role/role_grant_list_test.go b/cmd/cli/app/project/role/role_grant_list_test.go new file mode 100644 index 0000000000..620e61f34c --- /dev/null +++ b/cmd/cli/app/project/role/role_grant_list_test.go @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package role + +import ( + "context" + "testing" + + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" + mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" +) + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestRoleGrantListCommand(t *testing.T) { + const mockProjectID = "12345678-1234-1234-1234-123456789012" + + tests := []cli.CmdTestCase{ + { + Name: "grant list table output", + Args: []string{"project", "role", "grant", "list", "-j", mockProjectID, "-o", "table"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.ListRoleAssignmentsResponse{} + cli.LoadFixture(t, "mock_role_grant_list.json", mockResp) + + client.EXPECT(). + ListRoleAssignments(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "grant_list_table.txt", + }, + { + Name: "grant list json output", + Args: []string{"project", "role", "grant", "list", "-j", mockProjectID, "-o", "json"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.ListRoleAssignmentsResponse{} + cli.LoadFixture(t, "mock_role_grant_list.json", mockResp) + + client.EXPECT(). + ListRoleAssignments(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "grant_list_json.txt", + }, + { + Name: "grant list yaml output", + Args: []string{"project", "role", "grant", "list", "-o", "yaml"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.ListRoleAssignmentsResponse{} + cli.LoadFixture(t, "mock_role_grant_list.json", mockResp) + + client.EXPECT(). + ListRoleAssignments(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "grant_list_yaml.txt", + }, + { + Name: "server error handling", + Args: []string{"project", "role", "grant", "list"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + client.EXPECT(). + ListRoleAssignments(gomock.Any(), gomock.Any()). + Return(nil, status.Error(codes.Internal, "internal server error")). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + ExpectedError: "internal server error", + }, + } + + cli.RunCmdTests(t, tests, RoleCmd) +} diff --git a/cmd/cli/app/project/role/role_grant_test.go b/cmd/cli/app/project/role/role_grant_test.go new file mode 100644 index 0000000000..fd3ee615eb --- /dev/null +++ b/cmd/cli/app/project/role/role_grant_test.go @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package role + +import ( + "context" + "testing" + + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" + mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" +) + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestRoleGrantCommand(t *testing.T) { + const ( + projectSub = "00000000-0000-0000-0000-000000000001" + mockProjectID = "12345678-1234-1234-1234-123456789012" + ) + + tests := []cli.CmdTestCase{ + { + Name: "grant role table output", + Args: []string{"project", "role", "grant", "-s", projectSub, "-r", "admin", "-j", mockProjectID, "-o", "table"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.AssignRoleResponse{} + cli.LoadFixture(t, "mock_role_grant.json", mockResp) + + client.EXPECT(). + AssignRole(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "grant_table.txt", + }, + { + Name: "grant role json output", + Args: []string{"project", "role", "grant", "-s", projectSub, "-r", "admin", "-j", mockProjectID, "-o", "json"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.AssignRoleResponse{} + cli.LoadFixture(t, "mock_role_grant.json", mockResp) + + client.EXPECT(). + AssignRole(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "grant_json.txt", + }, + { + Name: "grant role yaml output", + Args: []string{"project", "role", "grant", "-s", projectSub, "-r", "admin", "-j", mockProjectID, "-o", "yaml"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.AssignRoleResponse{} + cli.LoadFixture(t, "mock_role_grant.json", mockResp) + + client.EXPECT(). + AssignRole(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "grant_yaml.txt", + }, + { + Name: "grant role via email invite table output", + Args: []string{"project", "role", "grant", "-e", "test@example.com", "-r", "viewer", "-j", mockProjectID, "-o", "table"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.AssignRoleResponse{} + cli.LoadFixture(t, "mock_role_grant_invite.json", mockResp) + + client.EXPECT(). + AssignRole(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "grant_invite_table.txt", + }, + { + Name: "fails when missing required flags (sub or email)", + Args: []string{"project", "role", "grant", "-r", "admin"}, + ExpectedError: "at least one of the flags in the group [sub email] is required", + }, + { + Name: "server error handling", + Args: []string{"project", "role", "grant", "-s", projectSub, "-r", "admin", "-j", mockProjectID}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + client.EXPECT(). + AssignRole(gomock.Any(), gomock.Any()). + Return(nil, status.Error(codes.Internal, "internal server error")). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + ExpectedError: "internal server error", + }, + } + + cli.RunCmdTests(t, tests, RoleCmd) +} diff --git a/cmd/cli/app/project/role/role_list.go b/cmd/cli/app/project/role/role_list.go index bfdca5a4e3..c15e6230dd 100644 --- a/cmd/cli/app/project/role/role_list.go +++ b/cmd/cli/app/project/role/role_list.go @@ -4,14 +4,12 @@ package role import ( - "context" "fmt" "io" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "github.com/mindersec/minder/cmd/cli/app" "github.com/mindersec/minder/internal/util" @@ -26,15 +24,26 @@ var listCmd = &cobra.Command{ Short: "List roles on a project within the minder control plane", Long: `The minder project role list command allows one to list roles available on a particular project.`, - RunE: cli.GRPCClientWrapRunE(ListCommand), + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return cli.MessageAndError("Error binding flags", err) + } + return nil + }, + RunE: ListCommand, } // ListCommand is the command for listing roles -func ListCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { - client := minderv1.NewPermissionsServiceClient(conn) +func ListCommand(cmd *cobra.Command, _ []string) error { + client, cleanup, err := GetPermissionsClient(cmd) + if err != nil { + return cli.MessageAndError("Error getting client", err) + } + defer cleanup() project := viper.GetString("project") format := viper.GetString("output") + // Ensure the output format is supported if !app.IsOutputFormatSupported(format) { return cli.MessageAndError(fmt.Sprintf("Output format %s not supported", format), fmt.Errorf("invalid argument")) @@ -44,7 +53,7 @@ func ListCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc // See https://github.com/spf13/cobra/issues/340#issuecomment-374617413 cmd.SilenceUsage = true - resp, err := client.ListRoles(ctx, &minderv1.ListRolesRequest{ + resp, err := client.ListRoles(cmd.Context(), &minderv1.ListRolesRequest{ Context: &minderv1.Context{ Project: &project, }, diff --git a/cmd/cli/app/project/role/role_list_test.go b/cmd/cli/app/project/role/role_list_test.go new file mode 100644 index 0000000000..93e26346f9 --- /dev/null +++ b/cmd/cli/app/project/role/role_list_test.go @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package role + +import ( + "context" + "testing" + + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" + mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" +) + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestRoleListCommand(t *testing.T) { + const ( + projectID = "12345678-1234-1234-1234-123456789012" + projectName = "mock-subject-123" + ) + + tests := []cli.CmdTestCase{ + { + Name: "list roles table output", + Args: []string{"project", "role", "list", "-j", projectID, "-o", "table"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.ListRolesResponse{} + cli.LoadFixture(t, "mock_role_list.json", mockResp) + + client.EXPECT(). + ListRoles(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "list_table.txt", + }, + { + Name: "list roles json output", + Args: []string{"project", "role", "list", "-j", projectName, "-o", "json"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.ListRolesResponse{} + cli.LoadFixture(t, "mock_role_list.json", mockResp) + + client.EXPECT(). + ListRoles(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "list_json.txt", + }, + { + Name: "list roles yaml output", + Args: []string{"project", "role", "list", "-o", "yaml"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.ListRolesResponse{} + cli.LoadFixture(t, "mock_role_list.json", mockResp) + + client.EXPECT(). + ListRoles(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "list_yaml.txt", + }, + { + Name: "server error handling", + Args: []string{"project", "role", "list"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + client.EXPECT(). + ListRoles(gomock.Any(), gomock.Any()). + Return(nil, status.Error(codes.Internal, "internal server error")). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + ExpectedError: "internal server error", + }, + } + + cli.RunCmdTests(t, tests, RoleCmd) +} diff --git a/cmd/cli/app/project/role/role_test.go b/cmd/cli/app/project/role/role_test.go new file mode 100644 index 0000000000..ce296e8b1d --- /dev/null +++ b/cmd/cli/app/project/role/role_test.go @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package role + +import ( + "testing" + + "github.com/mindersec/minder/internal/util/cli" +) + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestRoleRootCommand(t *testing.T) { + tests := []cli.CmdTestCase{ + { + Name: "role root command prints usage", + Args: []string{"project", "role"}, + GoldenFileName: "role_root_usage.txt", + }, + { + Name: "role help flag prints usage", + Args: []string{"project", "role", "-h"}, + GoldenFileName: "role_help_usage.txt", + }, + } + + cli.RunCmdTests(t, tests, RoleCmd) +} diff --git a/cmd/cli/app/project/role/role_update.go b/cmd/cli/app/project/role/role_update.go index dcb4b619f4..5430a1962d 100644 --- a/cmd/cli/app/project/role/role_update.go +++ b/cmd/cli/app/project/role/role_update.go @@ -4,13 +4,11 @@ package role import ( - "context" "fmt" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "github.com/mindersec/minder/cmd/cli/app" "github.com/mindersec/minder/internal/util" @@ -23,12 +21,22 @@ var updateCmd = &cobra.Command{ Short: "update a role to a subject on a project", Long: `The minder project role update command allows one to update a role to a user (subject) on a particular project.`, - RunE: cli.GRPCClientWrapRunE(UpdateCommand), + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return cli.MessageAndError("Error binding flags", err) + } + return nil + }, + RunE: UpdateCommand, } // UpdateCommand is the command for granting roles -func UpdateCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { - client := minderv1.NewPermissionsServiceClient(conn) +func UpdateCommand(cmd *cobra.Command, _ []string) error { + client, cleanup, err := GetPermissionsClient(cmd) + if err != nil { + return cli.MessageAndError("Error getting client", err) + } + defer cleanup() role := viper.GetString("role") project := viper.GetString("project") @@ -60,7 +68,7 @@ func UpdateCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *gr successMsg = "Invite updated successfully." } - resp, err := client.UpdateRole(ctx, req) + resp, err := client.UpdateRole(cmd.Context(), req) if err != nil { return cli.MessageAndError(failMsg, err) } diff --git a/cmd/cli/app/project/role/role_update_test.go b/cmd/cli/app/project/role/role_update_test.go new file mode 100644 index 0000000000..cde8e7f256 --- /dev/null +++ b/cmd/cli/app/project/role/role_update_test.go @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package role + +import ( + "context" + "testing" + + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" + mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" +) + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestRoleUpdateCommand(t *testing.T) { + const ( + projectSub = "00000000-0000-0000-0000-000000000001" + mockProjectID = "12345678-1234-1234-1234-123456789012" + ) + + tests := []cli.CmdTestCase{ + { + Name: "update role table output", + Args: []string{"project", "role", "update", "-s", projectSub, "-r", "admin", "-j", mockProjectID, "-o", "table"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.UpdateRoleResponse{} + cli.LoadFixture(t, "mock_role_update.json", mockResp) + + client.EXPECT(). + UpdateRole(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "update_table.txt", + }, + { + Name: "update role json output", + Args: []string{"project", "role", "update", "-s", projectSub, "-r", "admin", "-j", mockProjectID, "-o", "json"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.UpdateRoleResponse{} + cli.LoadFixture(t, "mock_role_update.json", mockResp) + + client.EXPECT(). + UpdateRole(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "update_json.txt", + }, + { + Name: "update role yaml output", + Args: []string{"project", "role", "update", "-s", projectSub, "-r", "admin", "-j", mockProjectID, "-o", "yaml"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.UpdateRoleResponse{} + cli.LoadFixture(t, "mock_role_update.json", mockResp) + + client.EXPECT(). + UpdateRole(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "update_yaml.txt", + }, + { + Name: "update role via email invite table output", + Args: []string{"project", "role", "update", "-e", "test@example.com", "-r", "admin", "-o", "table"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + mockResp := &minderv1.UpdateRoleResponse{} + cli.LoadFixture(t, "mock_role_update_invite.json", mockResp) + + client.EXPECT(). + UpdateRole(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + GoldenFileName: "update_invite_table.txt", + }, + { + Name: "fails when missing required flags (sub or email)", + Args: []string{"project", "role", "update", "-r", "admin"}, + ExpectedError: "at least one of the flags in the group [sub email] is required", + }, + { + Name: "server error handling", + Args: []string{"project", "role", "update", "-s", projectSub, "-r", "admin", "-j", mockProjectID}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockPermissionsServiceClient(ctrl) + + client.EXPECT(). + UpdateRole(gomock.Any(), gomock.Any()). + Return(nil, status.Error(codes.Internal, "internal server error")). + Times(1) + + return cli.WithRPCClient[minderv1.PermissionsServiceClient](context.Background(), client) + }, + ExpectedError: "internal server error", + }, + } + + cli.RunCmdTests(t, tests, RoleCmd) +} diff --git a/cmd/cli/app/project/role/testdata/deny_email_success.txt.golden b/cmd/cli/app/project/role/testdata/deny_email_success.txt.golden new file mode 100644 index 0000000000..804b99733a --- /dev/null +++ b/cmd/cli/app/project/role/testdata/deny_email_success.txt.golden @@ -0,0 +1 @@ +Invite deleted successfully. diff --git a/cmd/cli/app/project/role/testdata/deny_success.txt.golden b/cmd/cli/app/project/role/testdata/deny_success.txt.golden new file mode 100644 index 0000000000..98694db6f6 --- /dev/null +++ b/cmd/cli/app/project/role/testdata/deny_success.txt.golden @@ -0,0 +1 @@ +Denied role successfully. diff --git a/cmd/cli/app/project/role/testdata/grant_invite_table.txt.golden b/cmd/cli/app/project/role/testdata/grant_invite_table.txt.golden new file mode 100644 index 0000000000..0bfc29224f --- /dev/null +++ b/cmd/cli/app/project/role/testdata/grant_invite_table.txt.golden @@ -0,0 +1,10 @@ +Invite created successfully. + INVITEE │ ROLE │ SPONSOR │ EXPIRES AT +─────────────────────────────┼────────────┼───────────────────┼───────────────────────────────────── + test@example.com │ viewer │ Admin User │ 2026-05-01T12:00:00Z + +The invitee can accept it by running: + +minder auth invite accept MOCK-INVITE-CODE + +Or by visiting: https://minder.example.com/invite/MOCK-INVITE-CODE diff --git a/cmd/cli/app/project/role/testdata/grant_json.txt.golden b/cmd/cli/app/project/role/testdata/grant_json.txt.golden new file mode 100644 index 0000000000..b6e8cffd78 --- /dev/null +++ b/cmd/cli/app/project/role/testdata/grant_json.txt.golden @@ -0,0 +1,8 @@ +{ + "roleAssignment": { + "role": "admin", + "subject": "mock-subject-123", + "displayName": "Mock User", + "project": "00000000-0000-0000-0000-000000000001" + } +} diff --git a/cmd/cli/app/project/role/testdata/grant_list_json.txt.golden b/cmd/cli/app/project/role/testdata/grant_list_json.txt.golden new file mode 100644 index 0000000000..2bfa4d8024 --- /dev/null +++ b/cmd/cli/app/project/role/testdata/grant_list_json.txt.golden @@ -0,0 +1,18 @@ +{ + "roleAssignments": [ + { + "role": "admin", + "subject": "admin-subject-123", + "displayName": "Mock Admin", + "project": "00000000-0000-0000-0000-000000000001" + } + ], + "invitations": [ + { + "role": "viewer", + "email": "pending@example.com", + "expiresAt": "2026-05-01T12:00:00Z", + "sponsorDisplay": "Mock Admin" + } + ] +} diff --git a/cmd/cli/app/project/role/testdata/grant_list_table.txt.golden b/cmd/cli/app/project/role/testdata/grant_list_table.txt.golden new file mode 100644 index 0000000000..bb945e161d --- /dev/null +++ b/cmd/cli/app/project/role/testdata/grant_list_table.txt.golden @@ -0,0 +1,6 @@ + USER │ ROLE │ PROJECT +────────────────────────────────────────┼────────┼────────────────────────────────────────────────── + Mock Admin / admin-subject-123 │ admin │ 00000000-0000-0000-0000-000000000001 + INVITEE │ ROLE │ SPONSOR │ EXPIRES AT +────────────────────────────────┼───────────┼──────────────────┼──────────────────────────────────── + pending@example.com │ viewer │ Mock Admin │ 2026-05-01T12:00:00Z diff --git a/cmd/cli/app/project/role/testdata/grant_list_yaml.txt.golden b/cmd/cli/app/project/role/testdata/grant_list_yaml.txt.golden new file mode 100644 index 0000000000..e5407449bb --- /dev/null +++ b/cmd/cli/app/project/role/testdata/grant_list_yaml.txt.golden @@ -0,0 +1,11 @@ +invitations: + - email: pending@example.com + expiresAt: "2026-05-01T12:00:00Z" + role: viewer + sponsorDisplay: Mock Admin +roleAssignments: + - displayName: Mock Admin + project: 00000000-0000-0000-0000-000000000001 + role: admin + subject: admin-subject-123 + diff --git a/cmd/cli/app/project/role/testdata/grant_table.txt.golden b/cmd/cli/app/project/role/testdata/grant_table.txt.golden new file mode 100644 index 0000000000..b010421412 --- /dev/null +++ b/cmd/cli/app/project/role/testdata/grant_table.txt.golden @@ -0,0 +1 @@ +Granted role successfully. diff --git a/cmd/cli/app/project/role/testdata/grant_yaml.txt.golden b/cmd/cli/app/project/role/testdata/grant_yaml.txt.golden new file mode 100644 index 0000000000..7da10f57bf --- /dev/null +++ b/cmd/cli/app/project/role/testdata/grant_yaml.txt.golden @@ -0,0 +1,6 @@ +roleAssignment: + displayName: Mock User + project: 00000000-0000-0000-0000-000000000001 + role: admin + subject: mock-subject-123 + diff --git a/cmd/cli/app/project/role/testdata/list_json.txt.golden b/cmd/cli/app/project/role/testdata/list_json.txt.golden new file mode 100644 index 0000000000..38175808db --- /dev/null +++ b/cmd/cli/app/project/role/testdata/list_json.txt.golden @@ -0,0 +1,12 @@ +{ + "roles": [ + { + "name": "admin", + "description": "Administrator role with full access" + }, + { + "name": "viewer", + "description": "Read-only access role" + } + ] +} diff --git a/cmd/cli/app/project/role/testdata/list_table.txt.golden b/cmd/cli/app/project/role/testdata/list_table.txt.golden new file mode 100644 index 0000000000..845eaca230 --- /dev/null +++ b/cmd/cli/app/project/role/testdata/list_table.txt.golden @@ -0,0 +1,5 @@ + NAME │ DESCRIPTION +───────────────┼──────────────────────────────────────────────────────────────────────────────────── + admin │ Administrator role with full access +───────────────┼──────────────────────────────────────────────────────────────────────────────────── + viewer │ Read-only access role diff --git a/cmd/cli/app/project/role/testdata/list_yaml.txt.golden b/cmd/cli/app/project/role/testdata/list_yaml.txt.golden new file mode 100644 index 0000000000..49aeefb231 --- /dev/null +++ b/cmd/cli/app/project/role/testdata/list_yaml.txt.golden @@ -0,0 +1,6 @@ +roles: + - description: Administrator role with full access + name: admin + - description: Read-only access role + name: viewer + diff --git a/cmd/cli/app/project/role/testdata/role_help_usage.txt.golden b/cmd/cli/app/project/role/testdata/role_help_usage.txt.golden new file mode 100644 index 0000000000..8310536726 --- /dev/null +++ b/cmd/cli/app/project/role/testdata/role_help_usage.txt.golden @@ -0,0 +1,26 @@ +The minder role commands manage permissions within a minder control plane. + +Usage: + minder project role [flags] + minder project role [command] + +Available Commands: + deny Deny a role to a subject on a project within the minder control plane + grant Grant a role to a subject on a project within the minder control plane + list List roles on a project within the minder control plane + update update a role to a subject on a project + +Flags: + -h, --help help for role + -j, --project string ID of the project + +Global Flags: + --config string Config file (default is $PWD/config.yaml) + --grpc-host string Server host (default "api.custcodian.dev") + --grpc-insecure Allow establishing insecure connections + --grpc-port int Server port (default 443) + --identity-client string Identity server client ID (default "minder-cli") + --identity-url string Identity server issuer URL (default "https://auth.custcodian.dev") + -v, --verbose Output additional messages to STDERR + +Use "minder project role [command] --help" for more information about a command. diff --git a/cmd/cli/app/project/role/testdata/role_root_usage.txt.golden b/cmd/cli/app/project/role/testdata/role_root_usage.txt.golden new file mode 100644 index 0000000000..84fb64f63d --- /dev/null +++ b/cmd/cli/app/project/role/testdata/role_root_usage.txt.golden @@ -0,0 +1,24 @@ +Usage: + minder project role [flags] + minder project role [command] + +Available Commands: + deny Deny a role to a subject on a project within the minder control plane + grant Grant a role to a subject on a project within the minder control plane + list List roles on a project within the minder control plane + update update a role to a subject on a project + +Flags: + -h, --help help for role + -j, --project string ID of the project + +Global Flags: + --config string Config file (default is $PWD/config.yaml) + --grpc-host string Server host (default "api.custcodian.dev") + --grpc-insecure Allow establishing insecure connections + --grpc-port int Server port (default 443) + --identity-client string Identity server client ID (default "minder-cli") + --identity-url string Identity server issuer URL (default "https://auth.custcodian.dev") + -v, --verbose Output additional messages to STDERR + +Use "minder project role [command] --help" for more information about a command. diff --git a/cmd/cli/app/project/role/testdata/update_invite_table.txt.golden b/cmd/cli/app/project/role/testdata/update_invite_table.txt.golden new file mode 100644 index 0000000000..b721980fe9 --- /dev/null +++ b/cmd/cli/app/project/role/testdata/update_invite_table.txt.golden @@ -0,0 +1,8 @@ +Invite updated successfully. +Updated an invite for test@example.com to admin on 00000000-0000-0000-0000-000000000001 + +The invitee can accept it by running: + +minder auth invite accept MOCK-UPDATE-INVITE + +Or by visiting: https://minder.example.com/invite/MOCK-UPDATE-INVITE diff --git a/cmd/cli/app/project/role/testdata/update_json.txt.golden b/cmd/cli/app/project/role/testdata/update_json.txt.golden new file mode 100644 index 0000000000..3a0f05c123 --- /dev/null +++ b/cmd/cli/app/project/role/testdata/update_json.txt.golden @@ -0,0 +1,10 @@ +{ + "roleAssignments": [ + { + "role": "admin", + "subject": "mock-subject-123", + "displayName": "Mock User", + "project": "00000000-0000-0000-0000-000000000001" + } + ] +} diff --git a/cmd/cli/app/project/role/testdata/update_table.txt.golden b/cmd/cli/app/project/role/testdata/update_table.txt.golden new file mode 100644 index 0000000000..a8f99d01eb --- /dev/null +++ b/cmd/cli/app/project/role/testdata/update_table.txt.golden @@ -0,0 +1,4 @@ +Updated role successfully. + USER │ ROLE │ PROJECT +───────────────────────────────────────┼────────┼─────────────────────────────────────────────────── + Mock User / mock-subject-123 │ admin │ 00000000-0000-0000-0000-000000000001 diff --git a/cmd/cli/app/project/role/testdata/update_yaml.txt.golden b/cmd/cli/app/project/role/testdata/update_yaml.txt.golden new file mode 100644 index 0000000000..3b44762a22 --- /dev/null +++ b/cmd/cli/app/project/role/testdata/update_yaml.txt.golden @@ -0,0 +1,6 @@ +roleAssignments: + - displayName: Mock User + project: 00000000-0000-0000-0000-000000000001 + role: admin + subject: mock-subject-123 + diff --git a/cmd/cli/app/project/testdata/create_json.txt.golden b/cmd/cli/app/project/testdata/create_json.txt.golden new file mode 100644 index 0000000000..2383719a07 --- /dev/null +++ b/cmd/cli/app/project/testdata/create_json.txt.golden @@ -0,0 +1,10 @@ +{ + "project": { + "projectId": "12345678-1234-1234-1234-123456789012", + "name": "mock-project", + "description": "A newly created mock project", + "createdAt": "2026-04-24T14:38:18.810108Z", + "updatedAt": "2026-04-24T14:38:18.810108Z", + "displayName": "mock-project" + } +} diff --git a/cmd/cli/app/project/testdata/create_table.txt.golden b/cmd/cli/app/project/testdata/create_table.txt.golden new file mode 100644 index 0000000000..15e41892e1 --- /dev/null +++ b/cmd/cli/app/project/testdata/create_table.txt.golden @@ -0,0 +1,3 @@ + ID │ NAME +─────────────────────────────────────────────────────────────────────────┼────────────────────────── + 12345678-1234-1234-1234-123456789012 │ mock-project diff --git a/cmd/cli/app/project/testdata/create_yaml.txt.golden b/cmd/cli/app/project/testdata/create_yaml.txt.golden new file mode 100644 index 0000000000..21c276bd80 --- /dev/null +++ b/cmd/cli/app/project/testdata/create_yaml.txt.golden @@ -0,0 +1,8 @@ +project: + createdAt: "2026-04-24T14:38:18.810108Z" + description: A newly created mock project + displayName: mock-project + name: mock-project + projectId: 12345678-1234-1234-1234-123456789012 + updatedAt: "2026-04-24T14:38:18.810108Z" + diff --git a/cmd/cli/app/project/testdata/delete_success.txt.golden b/cmd/cli/app/project/testdata/delete_success.txt.golden new file mode 100644 index 0000000000..d72e97fb3d --- /dev/null +++ b/cmd/cli/app/project/testdata/delete_success.txt.golden @@ -0,0 +1 @@ +Successfully deleted project with id: 12345678-1234-1234-1234-123456789012 diff --git a/cmd/cli/app/project/testdata/list_json.txt.golden b/cmd/cli/app/project/testdata/list_json.txt.golden new file mode 100644 index 0000000000..2793470abc --- /dev/null +++ b/cmd/cli/app/project/testdata/list_json.txt.golden @@ -0,0 +1,28 @@ +{ + "projects": [ + { + "projectId": "00000000-0000-0000-0000-000000000001", + "name": "mock-project-alpha", + "description": "First mock project for testing.", + "createdAt": "2024-01-01T12:00:00Z", + "updatedAt": "2024-01-01T12:00:00Z", + "displayName": "mock-project-alpha" + }, + { + "projectId": "00000000-0000-0000-0000-000000000002", + "name": "mock-project-beta", + "description": "Second mock project for testing.", + "createdAt": "2024-01-02T12:00:00Z", + "updatedAt": "2024-01-02T12:00:00Z", + "displayName": "mock-project-beta" + }, + { + "projectId": "00000000-0000-0000-0000-000000000003", + "name": "mock-project-gamma", + "description": "Third mock project for testing.", + "createdAt": "2024-01-03T12:00:00Z", + "updatedAt": "2024-01-03T12:00:00Z", + "displayName": "mock-project-gamma" + } + ] +} diff --git a/cmd/cli/app/project/testdata/list_table.txt.golden b/cmd/cli/app/project/testdata/list_table.txt.golden new file mode 100644 index 0000000000..3ddb4b9542 --- /dev/null +++ b/cmd/cli/app/project/testdata/list_table.txt.golden @@ -0,0 +1,7 @@ + ID │ NAME +─────────────────────────────────────────────────────────────────┼────────────────────────────────── + 00000000-0000-0000-0000-000000000001 │ mock-project-alpha +─────────────────────────────────────────────────────────────────┼────────────────────────────────── + 00000000-0000-0000-0000-000000000002 │ mock-project-beta +─────────────────────────────────────────────────────────────────┼────────────────────────────────── + 00000000-0000-0000-0000-000000000003 │ mock-project-gamma diff --git a/cmd/cli/app/project/testdata/list_yaml.txt.golden b/cmd/cli/app/project/testdata/list_yaml.txt.golden new file mode 100644 index 0000000000..7c8772e156 --- /dev/null +++ b/cmd/cli/app/project/testdata/list_yaml.txt.golden @@ -0,0 +1,20 @@ +projects: + - createdAt: "2024-01-01T12:00:00Z" + description: First mock project for testing. + displayName: mock-project-alpha + name: mock-project-alpha + projectId: 00000000-0000-0000-0000-000000000001 + updatedAt: "2024-01-01T12:00:00Z" + - createdAt: "2024-01-02T12:00:00Z" + description: Second mock project for testing. + displayName: mock-project-beta + name: mock-project-beta + projectId: 00000000-0000-0000-0000-000000000002 + updatedAt: "2024-01-02T12:00:00Z" + - createdAt: "2024-01-03T12:00:00Z" + description: Third mock project for testing. + displayName: mock-project-gamma + name: mock-project-gamma + projectId: 00000000-0000-0000-0000-000000000003 + updatedAt: "2024-01-03T12:00:00Z" + diff --git a/cmd/cli/app/project/testdata/project_help_usage.txt.golden b/cmd/cli/app/project/testdata/project_help_usage.txt.golden new file mode 100644 index 0000000000..ae029a59b5 --- /dev/null +++ b/cmd/cli/app/project/testdata/project_help_usage.txt.golden @@ -0,0 +1,24 @@ +The minder project commands manage projects within a minder control plane. + +Usage: + minder project [flags] + minder project [command] + +Available Commands: + create Create a sub-project within a minder control plane + delete Delete a sub-project within a minder control plane + list List the projects available to you within a minder control plane + +Flags: + -h, --help help for project + +Global Flags: + --config string Config file (default is $PWD/config.yaml) + --grpc-host string Server host (default "api.custcodian.dev") + --grpc-insecure Allow establishing insecure connections + --grpc-port int Server port (default 443) + --identity-client string Identity server client ID (default "minder-cli") + --identity-url string Identity server issuer URL (default "https://auth.custcodian.dev") + -v, --verbose Output additional messages to STDERR + +Use "minder project [command] --help" for more information about a command. diff --git a/cmd/cli/app/project/testdata/project_root_usage.txt.golden b/cmd/cli/app/project/testdata/project_root_usage.txt.golden new file mode 100644 index 0000000000..c0c869b410 --- /dev/null +++ b/cmd/cli/app/project/testdata/project_root_usage.txt.golden @@ -0,0 +1,22 @@ +Usage: + minder project [flags] + minder project [command] + +Available Commands: + create Create a sub-project within a minder control plane + delete Delete a sub-project within a minder control plane + list List the projects available to you within a minder control plane + +Flags: + -h, --help help for project + +Global Flags: + --config string Config file (default is $PWD/config.yaml) + --grpc-host string Server host (default "api.custcodian.dev") + --grpc-insecure Allow establishing insecure connections + --grpc-port int Server port (default 443) + --identity-client string Identity server client ID (default "minder-cli") + --identity-url string Identity server issuer URL (default "https://auth.custcodian.dev") + -v, --verbose Output additional messages to STDERR + +Use "minder project [command] --help" for more information about a command. diff --git a/pkg/api/protobuf/go/minder/v1/mock/mock_permissions.go b/pkg/api/protobuf/go/minder/v1/mock/mock_permissions.go new file mode 100644 index 0000000000..c395d0b320 --- /dev/null +++ b/pkg/api/protobuf/go/minder/v1/mock/mock_permissions.go @@ -0,0 +1,143 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1 (interfaces: PermissionsServiceClient) +// +// Generated by this command: +// +// mockgen -package mockv1 -destination pkg/api/protobuf/go/minder/v1/mock/mock_permissions.go github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1 PermissionsServiceClient +// + +// Package mockv1 is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + v1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" + gomock "go.uber.org/mock/gomock" + grpc "google.golang.org/grpc" +) + +// MockPermissionsServiceClient is a mock of PermissionsServiceClient interface. +type MockPermissionsServiceClient struct { + ctrl *gomock.Controller + recorder *MockPermissionsServiceClientMockRecorder + isgomock struct{} +} + +// MockPermissionsServiceClientMockRecorder is the mock recorder for MockPermissionsServiceClient. +type MockPermissionsServiceClientMockRecorder struct { + mock *MockPermissionsServiceClient +} + +// NewMockPermissionsServiceClient creates a new mock instance. +func NewMockPermissionsServiceClient(ctrl *gomock.Controller) *MockPermissionsServiceClient { + mock := &MockPermissionsServiceClient{ctrl: ctrl} + mock.recorder = &MockPermissionsServiceClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPermissionsServiceClient) EXPECT() *MockPermissionsServiceClientMockRecorder { + return m.recorder +} + +// AssignRole mocks base method. +func (m *MockPermissionsServiceClient) AssignRole(ctx context.Context, in *v1.AssignRoleRequest, opts ...grpc.CallOption) (*v1.AssignRoleResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AssignRole", varargs...) + ret0, _ := ret[0].(*v1.AssignRoleResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AssignRole indicates an expected call of AssignRole. +func (mr *MockPermissionsServiceClientMockRecorder) AssignRole(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssignRole", reflect.TypeOf((*MockPermissionsServiceClient)(nil).AssignRole), varargs...) +} + +// ListRoleAssignments mocks base method. +func (m *MockPermissionsServiceClient) ListRoleAssignments(ctx context.Context, in *v1.ListRoleAssignmentsRequest, opts ...grpc.CallOption) (*v1.ListRoleAssignmentsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRoleAssignments", varargs...) + ret0, _ := ret[0].(*v1.ListRoleAssignmentsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRoleAssignments indicates an expected call of ListRoleAssignments. +func (mr *MockPermissionsServiceClientMockRecorder) ListRoleAssignments(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRoleAssignments", reflect.TypeOf((*MockPermissionsServiceClient)(nil).ListRoleAssignments), varargs...) +} + +// ListRoles mocks base method. +func (m *MockPermissionsServiceClient) ListRoles(ctx context.Context, in *v1.ListRolesRequest, opts ...grpc.CallOption) (*v1.ListRolesResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRoles", varargs...) + ret0, _ := ret[0].(*v1.ListRolesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRoles indicates an expected call of ListRoles. +func (mr *MockPermissionsServiceClientMockRecorder) ListRoles(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRoles", reflect.TypeOf((*MockPermissionsServiceClient)(nil).ListRoles), varargs...) +} + +// RemoveRole mocks base method. +func (m *MockPermissionsServiceClient) RemoveRole(ctx context.Context, in *v1.RemoveRoleRequest, opts ...grpc.CallOption) (*v1.RemoveRoleResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RemoveRole", varargs...) + ret0, _ := ret[0].(*v1.RemoveRoleResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RemoveRole indicates an expected call of RemoveRole. +func (mr *MockPermissionsServiceClientMockRecorder) RemoveRole(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRole", reflect.TypeOf((*MockPermissionsServiceClient)(nil).RemoveRole), varargs...) +} + +// UpdateRole mocks base method. +func (m *MockPermissionsServiceClient) UpdateRole(ctx context.Context, in *v1.UpdateRoleRequest, opts ...grpc.CallOption) (*v1.UpdateRoleResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpdateRole", varargs...) + ret0, _ := ret[0].(*v1.UpdateRoleResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateRole indicates an expected call of UpdateRole. +func (mr *MockPermissionsServiceClientMockRecorder) UpdateRole(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRole", reflect.TypeOf((*MockPermissionsServiceClient)(nil).UpdateRole), varargs...) +}