diff --git a/docs/auth0_client-grants.md b/docs/auth0_client-grants.md index c857d7651..5137bd3cd 100644 --- a/docs/auth0_client-grants.md +++ b/docs/auth0_client-grants.md @@ -12,6 +12,7 @@ Manage client grants. A client grant authorizes an application (client) to reque - [auth0 client-grants create](auth0_client-grants_create.md) - Create a new client grant - [auth0 client-grants delete](auth0_client-grants_delete.md) - Delete a client grant - [auth0 client-grants list](auth0_client-grants_list.md) - List your client grants +- [auth0 client-grants organizations](auth0_client-grants_organizations.md) - Manage organizations of a client grant - [auth0 client-grants show](auth0_client-grants_show.md) - Show a client grant - [auth0 client-grants update](auth0_client-grants_update.md) - Update a client grant diff --git a/docs/auth0_client-grants_create.md b/docs/auth0_client-grants_create.md index d5ebf308e..15d9766dc 100644 --- a/docs/auth0_client-grants_create.md +++ b/docs/auth0_client-grants_create.md @@ -65,6 +65,7 @@ auth0 client-grants create [flags] - [auth0 client-grants create](auth0_client-grants_create.md) - Create a new client grant - [auth0 client-grants delete](auth0_client-grants_delete.md) - Delete a client grant - [auth0 client-grants list](auth0_client-grants_list.md) - List your client grants +- [auth0 client-grants organizations](auth0_client-grants_organizations.md) - Manage organizations of a client grant - [auth0 client-grants show](auth0_client-grants_show.md) - Show a client grant - [auth0 client-grants update](auth0_client-grants_update.md) - Update a client grant diff --git a/docs/auth0_client-grants_delete.md b/docs/auth0_client-grants_delete.md index de3620332..d7213303a 100644 --- a/docs/auth0_client-grants_delete.md +++ b/docs/auth0_client-grants_delete.md @@ -50,6 +50,7 @@ auth0 client-grants delete [flags] - [auth0 client-grants create](auth0_client-grants_create.md) - Create a new client grant - [auth0 client-grants delete](auth0_client-grants_delete.md) - Delete a client grant - [auth0 client-grants list](auth0_client-grants_list.md) - List your client grants +- [auth0 client-grants organizations](auth0_client-grants_organizations.md) - Manage organizations of a client grant - [auth0 client-grants show](auth0_client-grants_show.md) - Show a client grant - [auth0 client-grants update](auth0_client-grants_update.md) - Update a client grant diff --git a/docs/auth0_client-grants_list.md b/docs/auth0_client-grants_list.md index ded8e1b92..427a1bbe3 100644 --- a/docs/auth0_client-grants_list.md +++ b/docs/auth0_client-grants_list.md @@ -59,6 +59,7 @@ auth0 client-grants list [flags] - [auth0 client-grants create](auth0_client-grants_create.md) - Create a new client grant - [auth0 client-grants delete](auth0_client-grants_delete.md) - Delete a client grant - [auth0 client-grants list](auth0_client-grants_list.md) - List your client grants +- [auth0 client-grants organizations](auth0_client-grants_organizations.md) - Manage organizations of a client grant - [auth0 client-grants show](auth0_client-grants_show.md) - Show a client grant - [auth0 client-grants update](auth0_client-grants_update.md) - Update a client grant diff --git a/docs/auth0_client-grants_organizations.md b/docs/auth0_client-grants_organizations.md new file mode 100644 index 000000000..0fa59251d --- /dev/null +++ b/docs/auth0_client-grants_organizations.md @@ -0,0 +1,13 @@ +--- +layout: default +has_toc: false +has_children: true +--- +# auth0 client-grants organizations + +Manage the organizations associated with a client grant. + +## Commands + +- [auth0 client-grants organizations list](auth0_client-grants_organizations_list.md) - List the organizations of a client grant + diff --git a/docs/auth0_client-grants_organizations_list.md b/docs/auth0_client-grants_organizations_list.md new file mode 100644 index 000000000..ffc3c3230 --- /dev/null +++ b/docs/auth0_client-grants_organizations_list.md @@ -0,0 +1,50 @@ +--- +layout: default +parent: auth0 client-grants organizations +has_toc: false +--- +# auth0 client-grants organizations list + +List the organizations associated with a client grant. + +## Usage +``` +auth0 client-grants organizations list [flags] +``` + +## Examples + +``` + auth0 client-grants organizations list + auth0 client-grants organizations ls + auth0 client-grants organizations list --number 100 + auth0 client-grants organizations ls -n 100 --json + auth0 client-grants organizations list --csv +``` + + +## Flags + +``` + --csv Output in csv format. + --json Output in json format. + --json-compact Output in compact json format. + -n, --number int Number of organizations to retrieve. Minimum 1, maximum 1000. (default 100) +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 client-grants organizations list](auth0_client-grants_organizations_list.md) - List the organizations of a client grant + + diff --git a/docs/auth0_client-grants_show.md b/docs/auth0_client-grants_show.md index a2755651e..32428e1c2 100644 --- a/docs/auth0_client-grants_show.md +++ b/docs/auth0_client-grants_show.md @@ -45,6 +45,7 @@ auth0 client-grants show [flags] - [auth0 client-grants create](auth0_client-grants_create.md) - Create a new client grant - [auth0 client-grants delete](auth0_client-grants_delete.md) - Delete a client grant - [auth0 client-grants list](auth0_client-grants_list.md) - List your client grants +- [auth0 client-grants organizations](auth0_client-grants_organizations.md) - Manage organizations of a client grant - [auth0 client-grants show](auth0_client-grants_show.md) - Show a client grant - [auth0 client-grants update](auth0_client-grants_update.md) - Update a client grant diff --git a/docs/auth0_client-grants_update.md b/docs/auth0_client-grants_update.md index 2a9df5898..99e0c7bf9 100644 --- a/docs/auth0_client-grants_update.md +++ b/docs/auth0_client-grants_update.md @@ -61,6 +61,7 @@ auth0 client-grants update [flags] - [auth0 client-grants create](auth0_client-grants_create.md) - Create a new client grant - [auth0 client-grants delete](auth0_client-grants_delete.md) - Delete a client grant - [auth0 client-grants list](auth0_client-grants_list.md) - List your client grants +- [auth0 client-grants organizations](auth0_client-grants_organizations.md) - Manage organizations of a client grant - [auth0 client-grants show](auth0_client-grants_show.md) - Show a client grant - [auth0 client-grants update](auth0_client-grants_update.md) - Update a client grant diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 321a01375..dd7623f31 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -140,7 +140,7 @@ var RequiredScopes = []string{ "read:anomaly_blocks", "delete:anomaly_blocks", "create:log_streams", "delete:log_streams", "read:log_streams", "update:log_streams", "create:actions", "delete:actions", "read:actions", "update:actions", - "create:organizations", "delete:organizations", "read:organizations", "update:organizations", "read:organization_members", "read:organization_member_roles", "read:organization_connections", + "create:organizations", "delete:organizations", "read:organizations", "update:organizations", "read:organization_members", "read:organization_member_roles", "read:organization_connections", "read:organization_client_grants", "read:prompts", "update:prompts", "read:attack_protection", "update:attack_protection", "read:event_streams", "create:event_streams", "update:event_streams", "delete:event_streams", "read:events", diff --git a/internal/auth/scopes_test.go b/internal/auth/scopes_test.go index 5284f8585..1a0a13e55 100644 --- a/internal/auth/scopes_test.go +++ b/internal/auth/scopes_test.go @@ -38,7 +38,7 @@ func TestRequiredScopes(t *testing.T) { "read:custom_domains", "create:custom_domains", "update:custom_domains", "delete:custom_domains", "read:client_keys", "read:logs", "read:tenant_settings", "read:anomaly_blocks", "delete:anomaly_blocks", - "read:organization_members", "read:organization_member_roles", + "read:organization_members", "read:organization_member_roles", "read:organization_client_grants", "read:prompts", "update:prompts", "read:attack_protection", "update:attack_protection", } diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index 93f383e18..5121fbaec 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -78,6 +78,7 @@ func NewAPI(m *management.Management) *API { type APIV3 struct { AttackProtectionBotDetection AttackProtectionBotDetectionAPIV3 ClientGrant ClientGrantAPIV3 + ClientGrantOrganization ClientGrantOrganizationAPIV3 Events EventsAPIV3 PhoneNotificationTemplate PhoneNotificationTemplateAPI } @@ -86,6 +87,7 @@ func NewAPIV3(m *managementv3.Management) *APIV3 { return &APIV3{ AttackProtectionBotDetection: m.AttackProtection.BotDetection, ClientGrant: m.ClientGrants, + ClientGrantOrganization: m.ClientGrants.Organizations, Events: m.Events, PhoneNotificationTemplate: m.Branding.Phone.Templates, } diff --git a/internal/auth0/client_grant.go b/internal/auth0/client_grant.go index 09df32585..dc4b3ce00 100644 --- a/internal/auth0/client_grant.go +++ b/internal/auth0/client_grant.go @@ -73,3 +73,25 @@ type ClientGrantAPIV3 interface { opts ...option.RequestOption, ) error } + +// ClientGrantOrganizationPage is the paginated response returned by the +// client-grant organizations endpoint. It is aliased here so the mock generator +// (which cannot parse instantiated generic types) sees a plain named type in the +// interface. +type ClientGrantOrganizationPage = core.Page[*string, *managementv3.Organization, *managementv3.ListClientGrantOrganizationsPaginatedResponseContent] + +// ClientGrantOrganizationAPIV3 is the interface for the +// /client-grants/{id}/organizations endpoint. +type ClientGrantOrganizationAPIV3 interface { + // List the organizations associated with a client grant. + // + // Required scope: `read:organization_client_grants` + // + // See: https://auth0.com/docs/api/management/v2/client-grants/get-client-grant-organizations + List( + ctx context.Context, + id string, + request *managementv3.ListClientGrantOrganizationsRequestParameters, + opts ...option.RequestOption, + ) (*ClientGrantOrganizationPage, error) +} diff --git a/internal/auth0/mock/client_grant_mock.go b/internal/auth0/mock/client_grant_mock.go index a47739932..5df60c662 100644 --- a/internal/auth0/mock/client_grant_mock.go +++ b/internal/auth0/mock/client_grant_mock.go @@ -135,3 +135,46 @@ func (mr *MockClientGrantAPIV3MockRecorder) Update(ctx, id, request interface{}, varargs := append([]interface{}{ctx, id, request}, opts...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockClientGrantAPIV3)(nil).Update), varargs...) } + +// MockClientGrantOrganizationAPIV3 is a mock of ClientGrantOrganizationAPIV3 interface. +type MockClientGrantOrganizationAPIV3 struct { + ctrl *gomock.Controller + recorder *MockClientGrantOrganizationAPIV3MockRecorder +} + +// MockClientGrantOrganizationAPIV3MockRecorder is the mock recorder for MockClientGrantOrganizationAPIV3. +type MockClientGrantOrganizationAPIV3MockRecorder struct { + mock *MockClientGrantOrganizationAPIV3 +} + +// NewMockClientGrantOrganizationAPIV3 creates a new mock instance. +func NewMockClientGrantOrganizationAPIV3(ctrl *gomock.Controller) *MockClientGrantOrganizationAPIV3 { + mock := &MockClientGrantOrganizationAPIV3{ctrl: ctrl} + mock.recorder = &MockClientGrantOrganizationAPIV3MockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClientGrantOrganizationAPIV3) EXPECT() *MockClientGrantOrganizationAPIV3MockRecorder { + return m.recorder +} + +// List mocks base method. +func (m *MockClientGrantOrganizationAPIV3) List(ctx context.Context, id string, request *management.ListClientGrantOrganizationsRequestParameters, opts ...option.RequestOption) (*auth0.ClientGrantOrganizationPage, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, id, request} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].(*auth0.ClientGrantOrganizationPage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockClientGrantOrganizationAPIV3MockRecorder) List(ctx, id, request interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, id, request}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockClientGrantOrganizationAPIV3)(nil).List), varargs...) +} diff --git a/internal/cli/client_grants.go b/internal/cli/client_grants.go index fbbc4a4fe..4fe60fbe4 100644 --- a/internal/cli/client_grants.go +++ b/internal/cli/client_grants.go @@ -146,6 +146,7 @@ func clientGrantsCmd(cli *cli) *cobra.Command { cmd.AddCommand(showClientGrantCmd(cli)) cmd.AddCommand(updateClientGrantCmd(cli)) cmd.AddCommand(deleteClientGrantCmd(cli)) + cmd.AddCommand(organizationsClientGrantCmd(cli)) return cmd } @@ -841,10 +842,13 @@ func (c *cli) mutableClientGrantPickerOptions(ctx context.Context) (pickerOption } func (c *cli) clientGrantPickerOptionsFiltered(ctx context.Context, excludeSystem bool) (pickerOptions, error) { - // Fetch only the first page, matching the apis/apps pickers. This keeps the - // picker fast to open on large tenants; if a grant is not on the first page, - // the user can pass its id directly. - page, err := c.apiv3.ClientGrant.List(ctx, &managementv3.ListClientGrantsRequestParameters{}) + // Fetch a single page of up to 100 grants. The API defaults to 50 per page, + // which silently drops grants past that on busy tenants; taking 100 keeps the + // common case selectable while still opening fast. If a grant is not on this + // page, the user can pass its id directly. + page, err := c.apiv3.ClientGrant.List(ctx, &managementv3.ListClientGrantsRequestParameters{ + Take: auth0.Int(100), + }) if err != nil { return nil, fmt.Errorf("failed to list client grants: %w", err) } diff --git a/internal/cli/client_grants_organizations.go b/internal/cli/client_grants_organizations.go new file mode 100644 index 000000000..2fd549678 --- /dev/null +++ b/internal/cli/client_grants_organizations.go @@ -0,0 +1,89 @@ +package cli + +import ( + "fmt" + + managementv3 "github.com/auth0/go-auth0/v3/management" + "github.com/spf13/cobra" + + "github.com/auth0/auth0-cli/internal/ansi" +) + +func organizationsClientGrantCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "organizations", + Short: "Manage organizations of a client grant", + Long: "Manage the organizations associated with a client grant.", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listOrganizationsClientGrantCmd(cli)) + + return cmd +} + +func listOrganizationsClientGrantCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + Number int + } + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Args: cobra.MaximumNArgs(1), + Short: "List the organizations of a client grant", + Long: "List the organizations associated with a client grant.", + Example: ` auth0 client-grants organizations list + auth0 client-grants organizations ls + auth0 client-grants organizations list --number 100 + auth0 client-grants organizations ls -n 100 --json + auth0 client-grants organizations list --csv`, + RunE: func(cmd *cobra.Command, args []string) error { + if inputs.Number < 1 || inputs.Number > 1000 { + return fmt.Errorf("number flag invalid, please pass a number between 1 and 1000") + } + + if len(args) == 0 { + if err := clientGrantID.Pick(cmd, &inputs.ID, cli.clientGrantPickerOptions); err != nil { + return err + } + } else { + inputs.ID = args[0] + } + + var organizations []*managementv3.Organization + if err := ansi.Waiting(func() error { + page, err := cli.apiv3.ClientGrantOrganization.List(cmd.Context(), inputs.ID, &managementv3.ListClientGrantOrganizationsRequestParameters{}) + if err != nil { + return err + } + + iter := page.Iterator() + for iter.Next(cmd.Context()) { + organizations = append(organizations, iter.Current()) + if len(organizations) >= inputs.Number { + break + } + } + return iter.Err() + }); err != nil { + return fmt.Errorf("failed to list organizations for client grant with ID %q: %w", inputs.ID, err) + } + + cli.renderer.ClientGrantOrganizationList(organizations) + + return nil + }, + } + + clientGrantNumber.Help = "Number of organizations to retrieve. Minimum 1, maximum 1000." + clientGrantNumber.RegisterInt(cmd, &inputs.Number, defaultPageSize) + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + cmd.Flags().BoolVar(&cli.csv, "csv", false, "Output in csv format.") + cmd.MarkFlagsMutuallyExclusive("json", "json-compact", "csv") + + return cmd +} diff --git a/internal/cli/client_grants_organizations_test.go b/internal/cli/client_grants_organizations_test.go new file mode 100644 index 000000000..c8a474608 --- /dev/null +++ b/internal/cli/client_grants_organizations_test.go @@ -0,0 +1,123 @@ +package cli + +import ( + "bytes" + "context" + "errors" + "io" + "testing" + + managementv3 "github.com/auth0/go-auth0/v3/management" + "github.com/auth0/go-auth0/v3/management/core" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/auth0/auth0-cli/internal/auth0" + "github.com/auth0/auth0-cli/internal/auth0/mock" + "github.com/auth0/auth0-cli/internal/display" +) + +func TestListOrganizationsClientGrantCmd(t *testing.T) { + terminalPage := func(organizations []*managementv3.Organization) *auth0.ClientGrantOrganizationPage { + return &auth0.ClientGrantOrganizationPage{ + Results: organizations, + NextPageFunc: func(context.Context) (*auth0.ClientGrantOrganizationPage, error) { + return nil, core.ErrNoPages + }, + } + } + + t.Run("lists the organizations of a client grant", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + clientGrantOrgAPI := mock.NewMockClientGrantOrganizationAPIV3(ctrl) + clientGrantOrgAPI.EXPECT(). + List(gomock.Any(), "cgr_1", gomock.Any()). + Return(terminalPage([]*managementv3.Organization{ + { + ID: auth0.String("org_1"), + Name: auth0.String("travel0"), + DisplayName: auth0.String("Travel0"), + }, + }), nil) + + stdout := &bytes.Buffer{} + cli := &cli{ + renderer: &display.Renderer{ + MessageWriter: io.Discard, + ResultWriter: stdout, + }, + apiv3: &auth0.APIV3{ClientGrantOrganization: clientGrantOrgAPI}, + } + + cmd := listOrganizationsClientGrantCmd(cli) + cmd.SetArgs([]string{"cgr_1"}) + + assert.NoError(t, cmd.Execute()) + expectTable(t, stdout.String(), + []string{"ID", "NAME", "DISPLAY NAME"}, + [][]string{ + {"org_1", "travel0", "Travel0"}, + }, + ) + }) + + t.Run("renders an empty state when the grant has no organizations", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + clientGrantOrgAPI := mock.NewMockClientGrantOrganizationAPIV3(ctrl) + clientGrantOrgAPI.EXPECT(). + List(gomock.Any(), "cgr_1", gomock.Any()). + Return(terminalPage(nil), nil) + + message := &bytes.Buffer{} + cli := &cli{ + renderer: &display.Renderer{ + MessageWriter: message, + ResultWriter: io.Discard, + }, + apiv3: &auth0.APIV3{ClientGrantOrganization: clientGrantOrgAPI}, + } + + cmd := listOrganizationsClientGrantCmd(cli) + cmd.SetArgs([]string{"cgr_1"}) + + assert.NoError(t, cmd.Execute()) + assert.Contains(t, message.String(), "No client grant organizations available.") + }) + + t.Run("wraps an API error with the grant id", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + clientGrantOrgAPI := mock.NewMockClientGrantOrganizationAPIV3(ctrl) + clientGrantOrgAPI.EXPECT(). + List(gomock.Any(), "cgr_1", gomock.Any()). + Return(nil, errors.New("api boom")) + + cli := &cli{ + renderer: &display.Renderer{ + MessageWriter: io.Discard, + ResultWriter: io.Discard, + }, + apiv3: &auth0.APIV3{ClientGrantOrganization: clientGrantOrgAPI}, + } + + cmd := listOrganizationsClientGrantCmd(cli) + cmd.SetArgs([]string{"cgr_1"}) + + assert.EqualError(t, cmd.Execute(), `failed to list organizations for client grant with ID "cgr_1": api boom`) + }) + + t.Run("rejects an out-of-range number", func(t *testing.T) { + cli := &cli{} + cli.noInput = true // Non-interactive mode. + + cmd := listOrganizationsClientGrantCmd(cli) + cmd.SetArgs([]string{"cgr_1", "--number", "1001"}) + + assert.EqualError(t, cmd.Execute(), "number flag invalid, please pass a number between 1 and 1000") + }) +} diff --git a/internal/display/client_grant_organizations.go b/internal/display/client_grant_organizations.go new file mode 100644 index 000000000..e7a7b64da --- /dev/null +++ b/internal/display/client_grant_organizations.go @@ -0,0 +1,54 @@ +package display + +import ( + "fmt" + + managementv3 "github.com/auth0/go-auth0/v3/management" + + "github.com/auth0/auth0-cli/internal/ansi" +) + +// clientGrantOrganizationView renders a single organization associated with a +// client grant as a row in the list table. +type clientGrantOrganizationView struct { + ID string + Name string + DisplayName string + + raw interface{} +} + +func (v *clientGrantOrganizationView) AsTableHeader() []string { + return []string{"ID", "Name", "Display Name"} +} + +func (v *clientGrantOrganizationView) AsTableRow() []string { + return []string{ansi.Faint(v.ID), v.Name, v.DisplayName} +} + +func (v *clientGrantOrganizationView) Object() interface{} { + return v.raw +} + +func (r *Renderer) ClientGrantOrganizationList(organizations []*managementv3.Organization) { + resource := "client grant organizations" + + r.Heading(fmt.Sprintf("%s (%d)", resource, len(organizations))) + + if len(organizations) == 0 { + r.EmptyState(resource, "This client grant is not associated with any organizations") + return + } + + var results []View + for _, organization := range organizations { + results = append(results, &clientGrantOrganizationView{ + ID: organization.GetID(), + Name: organization.GetName(), + DisplayName: organization.GetDisplayName(), + raw: organization, + }) + } + + r.Results(results) +} diff --git a/test/integration/client-grants-test-cases.yaml b/test/integration/client-grants-test-cases.yaml index 80c57badb..70ef9a482 100644 --- a/test/integration/client-grants-test-cases.yaml +++ b/test/integration/client-grants-test-cases.yaml @@ -130,11 +130,29 @@ tests: contains: - "--allow-any-organization can only be enabled when --organization-usage is 'allow' or 'require'" - 015 - delete client grant: + 015 - list client grant organizations: + command: auth0 client-grants organizations list $(./test/integration/scripts/get-client-grant-id.sh) --no-input + exit-code: 0 + + 016 - list client grant organizations with invalid number: + command: auth0 client-grants organizations list $(./test/integration/scripts/get-client-grant-id.sh) --number 1001 --no-input + exit-code: 1 + stderr: + contains: + - Number flag invalid, please pass a number between 1 and 1000 + + 016a - list client grant organizations rejects json and csv together: + command: auth0 client-grants organizations list $(./test/integration/scripts/get-client-grant-id.sh) --json --csv --no-input + exit-code: 1 + stderr: + contains: + - "any flags in the group [json json-compact csv] are set none of the others can be" + + 017 - delete client grant: command: auth0 client-grants delete $(./test/integration/scripts/get-client-grant-id.sh) --force --no-input exit-code: 0 - 016 - delete client grant with invalid id: + 018 - delete client grant with invalid id: command: auth0 client-grants delete this-client-grant-id-does-not-exist --force --no-input exit-code: 1 stderr: