diff --git a/cmd/cloudx/client/client.go b/cmd/cloudx/client/client.go index b8b69487..98ca663b 100644 --- a/cmd/cloudx/client/client.go +++ b/cmd/cloudx/client/client.go @@ -30,6 +30,21 @@ func RegisterProjectFlag(f *flag.FlagSet) { f.String(projectFlag, "", "The project to use, either project ID or a (partial) slug.") } +// ProjectOrDefault returns the slug or ID the user set with the `--project` flag, or the default project, or prints a warning and returns an error +// if none was set. +func ProjectOrDefault(cmd *cobra.Command, h *CommandHelper) (string, error) { + if flag := flagx.MustGetString(cmd, projectFlag); flag == "" { + if id := h.GetDefaultProjectID(); id != "" { + return id, nil + } else { + _, _ = fmt.Fprintf(os.Stderr, "No project selected! Please use the flag --%s to specify one.\n", projectFlag) + return "", cmdx.FailSilently(cmd) + } + } else { + return flag, nil + } +} + func Client(cmd *cobra.Command) (*retryablehttp.Client, *AuthContext, *cloud.Project, error) { sc, err := NewCommandHelper(cmd) if err != nil { @@ -42,7 +57,11 @@ func Client(cmd *cobra.Command) (*retryablehttp.Client, *AuthContext, *cloud.Pro return nil, nil, nil, err } - projectOrSlug := flagx.MustGetString(cmd, projectFlag) + projectOrSlug, err := ProjectOrDefault(cmd, sc) + if err != nil { + return nil, nil, nil, cmdx.FailSilently(cmd) + } + p, err := sc.GetProject(projectOrSlug) if err != nil { return nil, nil, nil, err diff --git a/cmd/cloudx/client/handler.go b/cmd/cloudx/client/handler.go index a94dc23e..fcd43d9a 100644 --- a/cmd/cloudx/client/handler.go +++ b/cmd/cloudx/client/handler.go @@ -152,6 +152,19 @@ func NewCommandHelper(cmd *cobra.Command) (*CommandHelper, error) { }, nil } +func (h *CommandHelper) GetDefaultProjectID() string { + conf, err := h.readConfig() + if err != nil { + return "" + } + + if conf.SelectedProject != uuid.Nil { + return conf.SelectedProject.String() + } + + return "" +} + func (h *CommandHelper) SetDefaultProject(id string) error { conf, err := h.readConfig() if err != nil { @@ -455,11 +468,6 @@ func (h *CommandHelper) Authenticate() (*AuthContext, error) { return nil, err } - var retry bool - if retry { - _, _ = fmt.Fprintln(h.VerboseErrWriter, "Unable to Authenticate you, please try again.") - } - if signIn { ac, err = h.signin(c, "") if err != nil { @@ -555,7 +563,7 @@ func (h *CommandHelper) GetProject(projectOrSlug string) (*cloud.Project, error) return project, nil } -func (h *CommandHelper) CreateProject(name string) (*cloud.Project, error) { +func (h *CommandHelper) CreateProject(name string, setDefault bool) (*cloud.Project, error) { ac, err := h.EnsureContext() if err != nil { return nil, err @@ -571,8 +579,8 @@ func (h *CommandHelper) CreateProject(name string) (*cloud.Project, error) { return nil, handleError("unable to list projects", res, err) } - if err := h.SetDefaultProject(project.Id); err != nil { - return nil, err + if def := h.GetDefaultProjectID(); setDefault || def == "" { + _ = h.SetDefaultProject(project.Id) } return project, nil @@ -662,6 +670,7 @@ func (h *CommandHelper) PatchProject(id string, raw []json.RawMessage, add, repl if err != nil { return nil, err } + return res, nil } @@ -732,6 +741,7 @@ func (h *CommandHelper) UpdateProject(id string, name string, configs []json.Raw if err != nil { return nil, err } + return res, nil } diff --git a/cmd/cloudx/client/handler_test.go b/cmd/cloudx/client/handler_test.go index 1352b092..bb5bf716 100644 --- a/cmd/cloudx/client/handler_test.go +++ b/cmd/cloudx/client/handler_test.go @@ -58,6 +58,205 @@ func TestCommandHelper(t *testing.T) { return ¬YetLoggedIn } + t.Run("func=SetDefaultProject", func(t *testing.T) { + t.Parallel() + configDir := testhelpers.NewConfigDir(t) + testhelpers.RegisterAccount(t, configDir) + otherId := testhelpers.CreateProject(t, configDir) + defaultId := testhelpers.CreateProject(t, configDir) + testhelpers.SetDefaultProject(t, configDir, defaultId) + + cmdBase := client.CommandHelper{ + ConfigLocation: configDir, + } + + t.Run("can change the selected project", func(t *testing.T) { + cmd := cmdBase + current := cmd.GetDefaultProjectID() + assert.Equal(t, current, defaultId) + + err := cmd.SetDefaultProject(otherId) + assert.NoError(t, err) + + selected := cmd.GetDefaultProjectID() + assert.Equal(t, selected, otherId) + }) + }) + + t.Run("func=ListProjects", func(t *testing.T) { + t.Parallel() + configDir := testhelpers.NewConfigDir(t) + testhelpers.RegisterAccount(t, configDir) + + cmdBase := client.CommandHelper{ + ConfigLocation: configDir, + } + + t.Run("With no projects returns empty list", func(t *testing.T) { + cmd := cmdBase + + projects, err := cmd.ListProjects() + + require.NoError(t, err) + require.Empty(t, projects) + }) + + t.Run("With some projects returns list of projects", func(t *testing.T) { + cmd := cmdBase + project_name1 := "new_project_name1" + project_name2 := "new_project_name2" + + project1, err := cmd.CreateProject(project_name1, false) + require.NoError(t, err) + project2, err := cmd.CreateProject(project_name2, false) + require.NoError(t, err) + + projects, err := cmd.ListProjects() + + require.NoError(t, err) + assert.Len(t, projects, 2) + assert.ElementsMatch(t, []string{projects[0].Id, projects[1].Id}, []string{project1.Id, project2.Id}) + }) + }) + + t.Run("func=CreateProject", func(t *testing.T) { + t.Parallel() + configDir := testhelpers.NewConfigDir(t) + testhelpers.RegisterAccount(t, configDir) + + cmdBase := client.CommandHelper{ + ConfigLocation: configDir, + } + + t.Run("creates project and sets default project", func(t *testing.T) { + cmd := cmdBase + project_name := "new_project_name" + + project, err := cmd.CreateProject(project_name, true) + require.NoError(t, err) + assert.Equal(t, project.Name, project_name) + + defaultId := cmd.GetDefaultProjectID() + assert.Equal(t, project.Id, defaultId) + }) + + t.Run("creates two projects with different names", func(t *testing.T) { + cmd := cmdBase + project_name1 := "new_project_name1" + project_name2 := "new_project_name2" + + project1, err := cmd.CreateProject(project_name1, true) + require.NoError(t, err) + + project2, err := cmd.CreateProject(project_name2, false) + require.NoError(t, err) + + assert.NotEqual(t, project1.Id, project2.Id) + assert.NotEqual(t, project1.Name, project2.Name) + assert.NotEqual(t, project1.Slug, project2.Slug) + + defaultId := cmd.GetDefaultProjectID() + assert.Equal(t, project1.Id, defaultId) + }) + }) + + t.Run("func=Authenticate", func(t *testing.T) { + t.Parallel() + cmdBase := client.CommandHelper{ + ConfigLocation: testhelpers.NewConfigDir(t), + NoConfirm: true, + IsQuiet: false, + VerboseWriter: io.Discard, + VerboseErrWriter: io.Discard, + } + + t.Run("create new account", func(t *testing.T) { + cmd := cmdBase + + name := testhelpers.FakeName() + email := testhelpers.FakeEmail() + var r bytes.Buffer + _, _ = r.WriteString("n\n") // Do you want to sign in to an existing Ory Network account? [y/n]: n + _, _ = r.WriteString(email + "\n") // Email: FakeEmail() + _, _ = r.WriteString(name + "\n") // Name: FakeName() + _, _ = r.WriteString("n\n") // Subscribe to the Ory Security Newsletter to get platform and security updates? [y/n]: n + _, _ = r.WriteString("y\n") // I accept the Terms of Service [y/n]: y + cmd.Stdin = bufio.NewReader(&r) + + password := testhelpers.FakePassword() + cmd.PwReader = func() ([]byte, error) { return []byte(password), nil } + + authCtx, err := cmd.Authenticate() + + require.NoError(t, err) + require.NotNil(t, authCtx) + require.Equal(t, authCtx.IdentityTraits.Email, email) + }) + + t.Run("log into existing account", func(t *testing.T) { + cmd := cmdBase + + var r bytes.Buffer + _, _ = r.WriteString("y\n") // Do you want to sign in to an existing Ory Network account? [y/n]: y + _, _ = r.WriteString(email + "\n") // Email: FakeEmail() + cmd.Stdin = bufio.NewReader(&r) + + cmd.PwReader = func() ([]byte, error) { return []byte(password), nil } + + auth_ctx, err := cmd.Authenticate() + + require.NoError(t, err) + require.NotNil(t, auth_ctx) + require.Equal(t, auth_ctx.IdentityTraits.Email, email) + }) + + t.Run("retry login after wrong password", func(t *testing.T) { + cmd := cmdBase + + var r bytes.Buffer + _, _ = r.WriteString("y\n") // Do you want to sign in to an existing Ory Network account? [y/n]: y + _, _ = r.WriteString(email + "\n") // Email: FakeEmail() + _, _ = r.WriteString(email + "\n") // Email: FakeEmail() [RETRY] + cmd.Stdin = bufio.NewReader(&r) + + var retry = false + cmd.PwReader = func() ([]byte, error) { + if retry { + return []byte(password), nil + } + retry = true + return []byte("wrong"), nil + } + + auth_ctx, err := cmd.Authenticate() + + require.NoError(t, err) + require.NotNil(t, auth_ctx) + require.Equal(t, auth_ctx.IdentityTraits.Email, email) + }) + + t.Run("switch logged in account", func(t *testing.T) { + cmd := *loggedIn + + cmd.NoConfirm = false + cmd.IsQuiet = false + + var r bytes.Buffer + _, _ = r.WriteString("y\n") // You are signed in as \"%s\" already. Do you wish to authenticate with another account?: y + _, _ = r.WriteString("y\n") // Do you want to sign in to an existing Ory Network account? [y/n]: y + _, _ = r.WriteString(email + "\n") // Email: FakeEmail() + cmd.Stdin = bufio.NewReader(&r) + + cmd.PwReader = func() ([]byte, error) { return []byte(password), nil } + + auth_ctx, err := cmd.Authenticate() + + require.NoError(t, err) + require.NotNil(t, auth_ctx) + require.Equal(t, auth_ctx.IdentityTraits.Email, email) + }) + }) + t.Run("func=CreateAPIKey and DeleteApiKey", func(t *testing.T) { t.Run("is able to get project", func(t *testing.T) { name := "a test key" diff --git a/cmd/cloudx/identity/main_test.go b/cmd/cloudx/identity/main_test.go index bc87c619..d8f1ef3f 100644 --- a/cmd/cloudx/identity/main_test.go +++ b/cmd/cloudx/identity/main_test.go @@ -16,6 +16,6 @@ var ( ) func TestMain(m *testing.M) { - defaultConfig, defaultEmail, defaultPassword, defaultProject, defaultCmd = testhelpers.CreateDefaultAssets() + defaultConfig, defaultEmail, defaultPassword, _, defaultProject, defaultCmd = testhelpers.CreateDefaultAssets() testhelpers.RunAgainstStaging(m) } diff --git a/cmd/cloudx/oauth2/main_test.go b/cmd/cloudx/oauth2/main_test.go index 0612835c..0c9029ac 100644 --- a/cmd/cloudx/oauth2/main_test.go +++ b/cmd/cloudx/oauth2/main_test.go @@ -16,6 +16,6 @@ var ( ) func TestMain(m *testing.M) { - defaultConfig, defaultEmail, defaultPassword, defaultProject, defaultCmd = testhelpers.CreateDefaultAssets() + defaultConfig, defaultEmail, defaultPassword, _, defaultProject, defaultCmd = testhelpers.CreateDefaultAssets() testhelpers.RunAgainstStaging(m) } diff --git a/cmd/cloudx/project/create.go b/cmd/cloudx/project/create.go index 7d5a960f..ba4e4f34 100644 --- a/cmd/cloudx/project/create.go +++ b/cmd/cloudx/project/create.go @@ -15,6 +15,8 @@ import ( "github.com/ory/x/flagx" ) +const useProjectFlag = "use-project" + func NewCreateProjectCmd() *cobra.Command { cmd := &cobra.Command{ Use: "project", @@ -39,7 +41,8 @@ func NewCreateProjectCmd() *cobra.Command { } } - p, err := h.CreateProject(name) + use := flagx.MustGetBool(cmd, useProjectFlag) + p, err := h.CreateProject(name, use) if err != nil { return cmdx.PrintOpenAPIError(cmd, err) } @@ -51,6 +54,7 @@ func NewCreateProjectCmd() *cobra.Command { } cmd.Flags().StringP("name", "n", "", "The name of the project, required when quiet mode is used") + cmd.Flags().Bool(useProjectFlag, false, "Set the created project as the default.") cmdx.RegisterFormatFlags(cmd.Flags()) return cmd } diff --git a/cmd/cloudx/project/create_test.go b/cmd/cloudx/project/create_test.go index 2409206b..b55dfd38 100644 --- a/cmd/cloudx/project/create_test.go +++ b/cmd/cloudx/project/create_test.go @@ -16,18 +16,38 @@ import ( ) func TestCreateProject(t *testing.T) { + parseOutput := func(stdout string) (id string, slug string, name string) { + id = gjson.Get(stdout, "id").String() + slug = gjson.Get(stdout, "slug").String() + name = gjson.Get(stdout, "name").String() + return + } assertResult := func(t *testing.T, configDir string, stdout string, expectedName string) { - ac := testhelpers.ReadConfig(t, configDir) - assert.Equal(t, gjson.Get(stdout, "id").String(), ac.SelectedProject.String(), stdout) - assert.NotEmpty(t, gjson.Get(stdout, "slug").String(), stdout) - assert.Equal(t, expectedName, gjson.Get(stdout, "name").String(), stdout) + _, slug, name := parseOutput(stdout) + assert.NotEmpty(t, slug, stdout) + assert.Equal(t, expectedName, name, stdout) } t.Run("is able to create a project", func(t *testing.T) { + testhelpers.SetDefaultProject(t, defaultConfig, defaultProject) + name := testhelpers.TestProjectName() stdout, _, err := defaultCmd.Exec(nil, "create", "project", "--name", name, "--format", "json") require.NoError(t, err) assertResult(t, defaultConfig, stdout, name) + + assert.Equal(t, defaultProject, testhelpers.GetDefaultProject(t, defaultConfig)) + }) + + t.Run("is able to create a project and use the project as default", func(t *testing.T) { + name := testhelpers.TestProjectName() + + stdout, _, err := defaultCmd.Exec(nil, "create", "project", "--name", name, "--use-project", "--format", "json") + require.NoError(t, err) + assertResult(t, defaultConfig, stdout, name) + + id, _, _ := parseOutput(stdout) + assert.Equal(t, id, testhelpers.GetDefaultProject(t, defaultConfig)) }) t.Run("is able to create a project and use name from stdin", func(t *testing.T) { diff --git a/cmd/cloudx/project/get.go b/cmd/cloudx/project/get.go index 21e8650e..84254234 100644 --- a/cmd/cloudx/project/get.go +++ b/cmd/cloudx/project/get.go @@ -12,8 +12,8 @@ import ( func NewGetProjectCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "project ", - Args: cobra.ExactArgs(1), + Use: "project [id]", + Args: cobra.MaximumNArgs(1), Short: "Get the complete configuration of an Ory Network project.", Example: `$ ory get project ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 @@ -45,7 +45,11 @@ $ ory get project ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 --format json return err } - project, err := h.GetProject(args[0]) + id, err := getSelectedProjectId(h, args) + if err != nil { + return cmdx.PrintOpenAPIError(cmd, err) + } + project, err := h.GetProject(id) if err != nil { return cmdx.PrintOpenAPIError(cmd, err) } diff --git a/cmd/cloudx/project/get_identity_config.go b/cmd/cloudx/project/get_identity_config.go index b4ea7f08..72e1e76c 100644 --- a/cmd/cloudx/project/get_identity_config.go +++ b/cmd/cloudx/project/get_identity_config.go @@ -12,9 +12,9 @@ import ( func NewGetKratosConfigCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "identity-config ", + Use: "identity-config [project-id]", Aliases: []string{"ic", "kratos-config"}, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Short: "Get Ory Identities configuration.", Long: "Get the Ory Identities configuration for the specified Ory Network project.", Example: `$ ory get identity-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 --format yaml > identity-config.yaml @@ -35,7 +35,11 @@ $ ory get identity-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 --format json return err } - project, err := h.GetProject(args[0]) + id, err := getSelectedProjectId(h, args) + if err != nil { + return cmdx.PrintOpenAPIError(cmd, err) + } + project, err := h.GetProject(id) if err != nil { return cmdx.PrintOpenAPIError(cmd, err) } diff --git a/cmd/cloudx/project/get_oauth2_config.go b/cmd/cloudx/project/get_oauth2_config.go index 681447f4..258f6897 100644 --- a/cmd/cloudx/project/get_oauth2_config.go +++ b/cmd/cloudx/project/get_oauth2_config.go @@ -12,9 +12,9 @@ import ( func NewGetOAuth2ConfigCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "oauth2-config ", + Use: "oauth2-config [project-id]", Aliases: []string{"oc", "oauth2-config"}, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Short: "Get Ory OAuth2 & OpenID Connect configuration.", Long: "Get the Ory OAuth2 & OpenID Connect configuration for the specified Ory Network project.", Example: `$ ory get oauth2-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 --format yaml > oauth2-config.yaml @@ -43,7 +43,11 @@ $ ory get oauth2-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 --format json return err } - project, err := h.GetProject(args[0]) + id, err := getSelectedProjectId(h, args) + if err != nil { + return cmdx.PrintOpenAPIError(cmd, err) + } + project, err := h.GetProject(id) if err != nil { return cmdx.PrintOpenAPIError(cmd, err) } diff --git a/cmd/cloudx/project/get_permission_config.go b/cmd/cloudx/project/get_permission_config.go index c7e2303b..76455948 100644 --- a/cmd/cloudx/project/get_permission_config.go +++ b/cmd/cloudx/project/get_permission_config.go @@ -12,9 +12,9 @@ import ( func NewGetKetoConfigCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "permission-config ", + Use: "permission-config [project-id]", Aliases: []string{"pc", "keto-config"}, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Short: "Get Ory Permissions configuration.", Long: "Get the Ory Permissions configuration for the specified Ory Network project.", Example: `$ ory get permission-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 --format yaml > permission-config.yaml @@ -36,7 +36,11 @@ $ ory get permission-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 --format json return err } - project, err := h.GetProject(args[0]) + id, err := getSelectedProjectId(h, args) + if err != nil { + return cmdx.PrintOpenAPIError(cmd, err) + } + project, err := h.GetProject(id) if err != nil { return cmdx.PrintOpenAPIError(cmd, err) } diff --git a/cmd/cloudx/project/get_test.go b/cmd/cloudx/project/get_test.go index 7763ab64..0ef6c1b4 100644 --- a/cmd/cloudx/project/get_test.go +++ b/cmd/cloudx/project/get_test.go @@ -12,30 +12,36 @@ import ( ) func TestGetProject(t *testing.T) { - t.Run("is able to get project", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "get", "project", defaultProject, "--format", "json") + runWithProject(t, func(t *testing.T, exec execFunc, projectID string) { + stdout, _, err := exec(nil, "get", "project", "--format", "json") require.NoError(t, err) - assert.Equal(t, defaultProject, gjson.Get(stdout, "id").String()) + assert.Equal(t, projectID, gjson.Get(stdout, "id").String()) assert.NotEmpty(t, gjson.Get(stdout, "slug").String()) - }) + }, WithDefaultProject, WithPositionalProject) } func TestGetServiceConfig(t *testing.T) { t.Run("service=kratos", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "get", "kratos-config", defaultProject, "--format", "json") - require.NoError(t, err) - assert.True(t, gjson.Get(stdout, "selfservice.flows.error.ui_url").Exists()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "get", "kratos-config", "--format", "json") + require.NoError(t, err) + assert.True(t, gjson.Get(stdout, "selfservice.flows.error.ui_url").Exists()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("service=keto", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "get", "keto-config", defaultProject, "--format", "json") - require.NoError(t, err) - assert.True(t, gjson.Get(stdout, "namespaces").Exists(), stdout) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "get", "keto-config", "--format", "json") + require.NoError(t, err) + assert.True(t, gjson.Get(stdout, "namespaces").Exists(), stdout) + }, WithDefaultProject, WithPositionalProject) }) t.Run("service=hydra", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "get", "oauth2-config", defaultProject, "--format", "json") - require.NoError(t, err) - assert.True(t, gjson.Get(stdout, "oauth2").Exists(), stdout) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "get", "oauth2-config", "--format", "json") + require.NoError(t, err) + assert.True(t, gjson.Get(stdout, "oauth2").Exists(), stdout) + }, WithDefaultProject, WithPositionalProject) }) } diff --git a/cmd/cloudx/project/helper_test.go b/cmd/cloudx/project/helper_test.go new file mode 100644 index 00000000..50caf4e7 --- /dev/null +++ b/cmd/cloudx/project/helper_test.go @@ -0,0 +1,63 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package project_test + +import ( + "io" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/cli/cmd/cloudx/testhelpers" +) + +type execFunc func(stdin io.Reader, args ...string) (string, string, error) + +type RunWithProjectOption uint8 + +const ( + WithDefaultProject RunWithProjectOption = 1 << iota + WithPositionalProject + WithFlagProject +) + +func runWithProject(t *testing.T, test func(t *testing.T, exec execFunc, projectID string), opts ...RunWithProjectOption) { + for _, v := range opts { + switch v { + case WithDefaultProject: + t.Run("project via configured default", func(t *testing.T) { + testhelpers.SetDefaultProject(t, defaultConfig, extraProject) + + test(t, func(stdin io.Reader, args ...string) (string, string, error) { + return defaultCmd.Exec(stdin, args...) + }, extraProject) + + // make sure, the default wasn't changed implicitly + assert.Equal(t, extraProject, testhelpers.GetDefaultProject(t, defaultConfig)) + }) + case WithPositionalProject: + t.Run("explicit project via positional argument", func(t *testing.T) { + testhelpers.SetDefaultProject(t, defaultConfig, defaultProject) + + test(t, func(stdin io.Reader, args ...string) (string, string, error) { + return defaultCmd.Exec(stdin, append(args, extraProject)...) + }, extraProject) + + // make sure, the default wasn't changed implicitly + assert.Equal(t, defaultProject, testhelpers.GetDefaultProject(t, defaultConfig)) + }) + case WithFlagProject: + t.Run("explicit project via `--project` flag", func(t *testing.T) { + testhelpers.SetDefaultProject(t, defaultConfig, defaultProject) + + test(t, func(stdin io.Reader, args ...string) (string, string, error) { + return defaultCmd.Exec(stdin, append(args, "--project", extraProject)...) + }, extraProject) + + // make sure, the default wasn't changed implicitly + assert.Equal(t, defaultProject, testhelpers.GetDefaultProject(t, defaultConfig)) + }) + } + } +} diff --git a/cmd/cloudx/project/main_test.go b/cmd/cloudx/project/main_test.go index edbb8bc9..8d070b99 100644 --- a/cmd/cloudx/project/main_test.go +++ b/cmd/cloudx/project/main_test.go @@ -11,11 +11,11 @@ import ( ) var ( - defaultProject, defaultConfig, defaultEmail, defaultPassword string - defaultCmd *cmdx.CommandExecuter + extraProject, defaultProject, defaultConfig, defaultEmail, defaultPassword string + defaultCmd *cmdx.CommandExecuter ) func TestMain(m *testing.M) { - defaultConfig, defaultEmail, defaultPassword, defaultProject, defaultCmd = testhelpers.CreateDefaultAssets() + defaultConfig, defaultEmail, defaultPassword, extraProject, defaultProject, defaultCmd = testhelpers.CreateDefaultAssets() testhelpers.RunAgainstStaging(m) } diff --git a/cmd/cloudx/project/output.go b/cmd/cloudx/project/output.go index 16900e26..bee413b8 100644 --- a/cmd/cloudx/project/output.go +++ b/cmd/cloudx/project/output.go @@ -71,3 +71,29 @@ func (c *outputProjectCollection) Interface() interface{} { func (c *outputProjectCollection) Len() int { return len(c.projects) } + +type selectedProject struct { + ID string `json:"id"` +} + +func (i selectedProject) String() string { + return i.ID +} + +func (i *selectedProject) ProjectID() string { + return i.ID +} + +func (*selectedProject) Header() []string { + return []string{"ID"} +} + +func (i *selectedProject) Columns() []string { + return []string{ + i.ID, + } +} + +func (i *selectedProject) Interface() interface{} { + return i +} diff --git a/cmd/cloudx/project/patch.go b/cmd/cloudx/project/patch.go index c9c6f0f4..3a8cd6a7 100644 --- a/cmd/cloudx/project/patch.go +++ b/cmd/cloudx/project/patch.go @@ -17,8 +17,8 @@ import ( func NewProjectsPatchCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "project ", - Args: cobra.ExactArgs(1), + Use: "project [id]", + Args: cobra.MaximumNArgs(1), Short: "Patch the Ory Network project configuration.", Example: `ory patch project ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 \ --replace '/name="My new project name"' \ @@ -81,7 +81,11 @@ func runPatch(patchPrefixer func([]string) []string, filePrefixer func([]json.Ra return err } - p, err := h.PatchProject(args[0], configs, add, replace, remove) + id, err := getSelectedProjectId(h, args) + if err != nil { + return cmdx.PrintOpenAPIError(cmd, err) + } + p, err := h.PatchProject(id, configs, add, replace, remove) if err != nil { return cmdx.PrintOpenAPIError(cmd, err) } diff --git a/cmd/cloudx/project/patch_identity_config.go b/cmd/cloudx/project/patch_identity_config.go index bcf86a79..84b1a5d0 100644 --- a/cmd/cloudx/project/patch_identity_config.go +++ b/cmd/cloudx/project/patch_identity_config.go @@ -12,9 +12,9 @@ import ( func NewPatchKratosConfigCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "identity-config ", + Use: "identity-config [project-id]", Aliases: []string{"ic", "kratos-config"}, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Short: "Patch the Ory Identities configuration of the defined Ory Network project.", Example: `$ ory patch identity-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 \ --add '/courier/smtp={"from_name":"My new email name"}' \ diff --git a/cmd/cloudx/project/patch_identity_config_test.go b/cmd/cloudx/project/patch_identity_config_test.go index b803e4ad..8a1c79cb 100644 --- a/cmd/cloudx/project/patch_identity_config_test.go +++ b/cmd/cloudx/project/patch_identity_config_test.go @@ -6,35 +6,40 @@ package project_test import ( "testing" - "github.com/ory/cli/cmd/cloudx/testhelpers" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" ) func TestPatchKratosConfig(t *testing.T) { - project := testhelpers.CreateProject(t, defaultConfig) t.Run("is able to replace a key", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "kratos-config", project, "--format", "json", "--replace", `/selfservice/methods/password/enabled=false`) - require.NoError(t, err) - assert.False(t, gjson.Get(stdout, "selfservice.methods.password.enabled").Bool()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "kratos-config", "--format", "json", "--replace", `/selfservice/methods/password/enabled=false`) + require.NoError(t, err) + assert.False(t, gjson.Get(stdout, "selfservice.methods.password.enabled").Bool()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("is able to add a key", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "identity-config", project, "--format", "json", "--add", `/selfservice/methods/password/enabled=false`) - require.NoError(t, err) - assert.False(t, gjson.Get(stdout, "selfservice.methods.password.enabled").Bool()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "identity-config", "--format", "json", "--add", `/selfservice/methods/password/enabled=false`) + require.NoError(t, err) + assert.False(t, gjson.Get(stdout, "selfservice.methods.password.enabled").Bool()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("is able to add a key with string", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "ic", project, "--format", "json", "--replace", "/selfservice/flows/error/ui_url=\"https://example.com/error-ui\"") - require.NoError(t, err) - assert.Equal(t, "https://example.com/error-ui", gjson.Get(stdout, "selfservice.flows.error.ui_url").String()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "ic", "--format", "json", "--replace", "/selfservice/flows/error/ui_url=\"https://example.com/error-ui\"") + require.NoError(t, err) + assert.Equal(t, "https://example.com/error-ui", gjson.Get(stdout, "selfservice.flows.error.ui_url").String()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("fails if no opts are given", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "ic", project, "--format", "json") - require.Error(t, err, stdout) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "ic", "--format", "json") + require.Error(t, err, stdout) + }, WithDefaultProject, WithPositionalProject, WithFlagProject) }) } diff --git a/cmd/cloudx/project/patch_oauth2_config.go b/cmd/cloudx/project/patch_oauth2_config.go index 1b564f12..52bb59d2 100644 --- a/cmd/cloudx/project/patch_oauth2_config.go +++ b/cmd/cloudx/project/patch_oauth2_config.go @@ -13,9 +13,9 @@ import ( func NewPatchOAuth2ConfigCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "oauth2-config ", + Use: "oauth2-config [project-id]", Aliases: []string{"oc", "hydra-config"}, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Short: "Patch the Ory OAuth2 & OpenID Connect configuration of the specified Ory Network project.", Example: `$ ory patch oauth2-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 \ --replace '/strategies/access_token="jwt"' \ diff --git a/cmd/cloudx/project/patch_oauth2_config_test.go b/cmd/cloudx/project/patch_oauth2_config_test.go index d34f52ea..18576afe 100644 --- a/cmd/cloudx/project/patch_oauth2_config_test.go +++ b/cmd/cloudx/project/patch_oauth2_config_test.go @@ -6,35 +6,40 @@ package project_test import ( "testing" - "github.com/ory/cli/cmd/cloudx/testhelpers" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" ) func TestPatchHydraConfig(t *testing.T) { - project := testhelpers.CreateProject(t, defaultConfig) t.Run("is able to replace a key", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "hydra-config", project, "--format", "json", "--replace", `/strategies/access_token="jwt"`) - require.NoError(t, err) - assert.Equal(t, "jwt", gjson.Get(stdout, "strategies.access_token").String()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "hydra-config", "--format", "json", "--replace", `/strategies/access_token="jwt"`) + require.NoError(t, err) + assert.Equal(t, "jwt", gjson.Get(stdout, "strategies.access_token").String()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("is able to add a key", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "oauth2-config", project, "--format", "json", "--add", `/ttl/login_consent_request="1h"`) - require.NoError(t, err) - assert.Equal(t, "1h0m0s", gjson.Get(stdout, "ttl.login_consent_request").String()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "oauth2-config", "--format", "json", "--add", `/ttl/login_consent_request="1h"`) + require.NoError(t, err) + assert.Equal(t, "1h0m0s", gjson.Get(stdout, "ttl.login_consent_request").String()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("is able to add a key with string", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "oc", project, "--format", "json", "--replace", `/ttl/refresh_token="2h"`) - require.NoError(t, err) - assert.Equal(t, "2h0m0s", gjson.Get(stdout, "ttl.refresh_token").String()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "oc", "--format", "json", "--replace", `/ttl/refresh_token="2h"`) + require.NoError(t, err) + assert.Equal(t, "2h0m0s", gjson.Get(stdout, "ttl.refresh_token").String()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("fails if no opts are given", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "oc", project, "--format", "json") - require.Error(t, err, stdout) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "oc", "--format", "json") + require.Error(t, err, stdout) + }, WithDefaultProject, WithPositionalProject, WithFlagProject) }) } diff --git a/cmd/cloudx/project/patch_permission_config.go b/cmd/cloudx/project/patch_permission_config.go index f01fc413..798726c3 100644 --- a/cmd/cloudx/project/patch_permission_config.go +++ b/cmd/cloudx/project/patch_permission_config.go @@ -12,9 +12,9 @@ import ( func NewPatchKetoConfigCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "permission-config ", + Use: "permission-config [project-id]", Aliases: []string{"pc", "keto-config"}, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Short: "Patch the Ory Permissions configuration of the specified Ory Network project.", Example: `$ ory patch permission-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 \ --add '/namespaces=[{"name":"files", "id": 2}]' \ diff --git a/cmd/cloudx/project/patch_permission_config_test.go b/cmd/cloudx/project/patch_permission_config_test.go index 43e7f118..09e18aee 100644 --- a/cmd/cloudx/project/patch_permission_config_test.go +++ b/cmd/cloudx/project/patch_permission_config_test.go @@ -6,35 +6,43 @@ package project_test import ( "testing" - "github.com/ory/cli/cmd/cloudx/testhelpers" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" ) func TestPatchPermissionConfig(t *testing.T) { - project := testhelpers.CreateProject(t, defaultConfig) - t.Run("is able to replace a key", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "keto-config", project, "--format", "json", "--add", `/namespaces=[{"name":"files", "id": 2}]`) - require.NoError(t, err) - assert.Equal(t, "files", gjson.Get(stdout, "namespaces.0.name").String(), stdout) + t.Run("is able to replace a key using keto-config", func(t *testing.T) { + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "keto-config", "--format", "json", "--replace", `/namespaces=[{"name":"files", "id": 2}]`) + require.NoError(t, err) + assert.Equal(t, "files", gjson.Get(stdout, "namespaces.0.name").String(), stdout) + }, WithDefaultProject, WithPositionalProject) }) t.Run("is able to add a key using permission-config", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "permission-config", project, "--format", "json", "--add", `/namespaces/1={"name":"docs", "id": 3}`) - require.NoError(t, err) - assert.Equal(t, "docs", gjson.Get(stdout, "namespaces.1.name").String(), stdout) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + _, _, err := exec(nil, "patch", "permission-config", "--format", "json", "--replace", `/namespaces=[]`) + require.NoError(t, err) + + stdout, _, err := exec(nil, "patch", "permission-config", "--format", "json", "--add", `/namespaces/0={"name":"docs", "id": 3}`) + require.NoError(t, err) + assert.Equal(t, "docs", gjson.Get(stdout, "namespaces.0.name").String(), stdout) + }, WithDefaultProject, WithPositionalProject) }) - t.Run("is able to replace a key", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "pc", project, "--format", "json", "--replace", `/namespaces=[{"name":"people", "id": 4}]`) - require.NoError(t, err) - assert.Equal(t, "people", gjson.Get(stdout, "namespaces.0.name").String(), stdout) + t.Run("is able to replace a key using pc", func(t *testing.T) { + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "pc", "--format", "json", "--replace", `/namespaces=[{"name":"people", "id": 4}]`) + require.NoError(t, err) + assert.Equal(t, "people", gjson.Get(stdout, "namespaces.0.name").String(), stdout) + }, WithDefaultProject, WithPositionalProject) }) t.Run("fails if no opts are given", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "pc", project, "--format", "json") - require.Error(t, err, stdout) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "pc", "--format", "json") + require.Error(t, err, stdout) + }, WithDefaultProject, WithPositionalProject, WithFlagProject) }) } diff --git a/cmd/cloudx/project/patch_test.go b/cmd/cloudx/project/patch_test.go index 471115b2..ea06b57d 100644 --- a/cmd/cloudx/project/patch_test.go +++ b/cmd/cloudx/project/patch_test.go @@ -6,70 +6,81 @@ package project_test import ( "testing" - "github.com/ory/cli/cmd/cloudx/testhelpers" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" ) func TestPatchProject(t *testing.T) { - project := testhelpers.CreateProject(t, defaultConfig) t.Run("is able to replace a key", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "project", project, "--format", "json", "--replace", `/services/identity/config/selfservice/methods/password/enabled=false`) - require.NoError(t, err) - assert.False(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.password.enabled").Bool()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "project", "--format", "json", "--replace", `/services/identity/config/selfservice/methods/password/enabled=false`) + require.NoError(t, err) + assert.False(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.password.enabled").Bool()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("is able to add a key", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "project", project, "--format", "json", "--add", `/services/identity/config/selfservice/methods/password/enabled=false`) - require.NoError(t, err) - assert.False(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.password.enabled").Bool()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "project", "--format", "json", "--add", `/services/identity/config/selfservice/methods/password/enabled=false`) + require.NoError(t, err) + assert.False(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.password.enabled").Bool()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("is able to add a key with string", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "project", project, "--format", "json", "--replace", "/services/identity/config/selfservice/flows/error/ui_url=\"https://example.com/error-ui\"") - require.NoError(t, err) - assert.Equal(t, "https://example.com/error-ui", gjson.Get(stdout, "services.identity.config.selfservice.flows.error.ui_url").String()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "project", "--format", "json", "--replace", "/services/identity/config/selfservice/flows/error/ui_url=\"https://example.com/error-ui\"") + require.NoError(t, err) + assert.Equal(t, "https://example.com/error-ui", gjson.Get(stdout, "services.identity.config.selfservice.flows.error.ui_url").String()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("is able to add a key with raw json", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "project", project, "--format", "json", "--replace", `/services/identity/config/selfservice/flows/error={"ui_url":"https://example.org/error-ui"}`) - require.NoError(t, err) - assert.Equal(t, "https://example.org/error-ui", gjson.Get(stdout, "services.identity.config.selfservice.flows.error.ui_url").String()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "project", "--format", "json", "--replace", `/services/identity/config/selfservice/flows/error={"ui_url":"https://example.org/error-ui"}`) + require.NoError(t, err) + assert.Equal(t, "https://example.org/error-ui", gjson.Get(stdout, "services.identity.config.selfservice.flows.error.ui_url").String()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("is able to remove a key", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "project", project, "--format", "json", "--remove", `/services/identity/config/selfservice/methods/password/enabled`) - require.NoError(t, err) - assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.password.enabled").Bool()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "project", "--format", "json", "--remove", `/services/identity/config/selfservice/methods/password/enabled`) + require.NoError(t, err) + assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.password.enabled").Bool()) + }, WithDefaultProject, WithPositionalProject) }) t.Run("fails if no opts are given", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "project", project, "--format", "json") - require.Error(t, err, stdout) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "project", "--format", "json") + require.Error(t, err, stdout) + }, WithDefaultProject, WithPositionalProject, WithFlagProject) }) t.Run("is able to update several keys", func(t *testing.T) { - stdout, _, err := defaultCmd.Exec(nil, "patch", "project", project, "--format", "json", - "--replace", `/services/identity/config/selfservice/methods/link/enabled=true`, - "--replace", `/services/identity/config/selfservice/methods/oidc/enabled=true`, - "--remove", `/services/identity/config/selfservice/methods/profile/enabled`, - "--remove", `/services/identity/config/selfservice/methods/password/enabled`, - "--add", `/services/identity/config/selfservice/methods/totp/enabled=true`, - "--add", `/services/identity/config/selfservice/methods/lookup_secret/enabled=true`, - "-f", "fixtures/patch/1.json", - "-f", "fixtures/patch/2.json", - ) - require.NoError(t, err) - assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.password.enabled").Bool()) - assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.profile.enabled").Bool()) - assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.link.enabled").Bool()) - assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.oidc.enabled").Bool()) - assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.totp.enabled").Bool()) - assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.lookup_secret.enabled").Bool()) - assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.webauthn.enabled").Bool()) - assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.webauthn.config.passwordless").Bool()) - assert.Equal(t, "some value", gjson.Get(stdout, "services.identity.config.selfservice.methods.webauthn.config.rp.display_name").String()) + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, _, err := exec(nil, "patch", "project", "--format", "json", + "--replace", `/services/identity/config/selfservice/methods/link/enabled=true`, + "--replace", `/services/identity/config/selfservice/methods/oidc/enabled=true`, + "--remove", `/services/identity/config/selfservice/methods/profile/enabled`, + "--remove", `/services/identity/config/selfservice/methods/password/enabled`, + "--add", `/services/identity/config/selfservice/methods/totp/enabled=true`, + "--add", `/services/identity/config/selfservice/methods/lookup_secret/enabled=true`, + "-f", "fixtures/patch/1.json", + "-f", "fixtures/patch/2.json", + ) + require.NoError(t, err) + assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.password.enabled").Bool()) + assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.profile.enabled").Bool()) + assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.link.enabled").Bool()) + assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.oidc.enabled").Bool()) + assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.totp.enabled").Bool()) + assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.lookup_secret.enabled").Bool()) + assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.webauthn.enabled").Bool()) + assert.True(t, gjson.Get(stdout, "services.identity.config.selfservice.methods.webauthn.config.passwordless").Bool()) + assert.Equal(t, "some value", gjson.Get(stdout, "services.identity.config.selfservice.methods.webauthn.config.rp.display_name").String()) + }, WithDefaultProject, WithPositionalProject) }) } diff --git a/cmd/cloudx/project/update.go b/cmd/cloudx/project/update.go index 7738ebc6..b7ecd4d8 100644 --- a/cmd/cloudx/project/update.go +++ b/cmd/cloudx/project/update.go @@ -18,8 +18,8 @@ import ( func NewProjectsUpdateCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "project ", - Args: cobra.ExactArgs(1), + Use: "project [id]", + Args: cobra.MaximumNArgs(1), Short: "Update Ory Network project service configuration", Example: `$ ory update project ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 \ --name \"my updated name\" \ @@ -121,7 +121,11 @@ func runUpdate(filePrefixer func([]json.RawMessage) ([]json.RawMessage, error), if n := cmd.Flags().Lookup("name"); n != nil { name = n.Value.String() } - p, err := h.UpdateProject(args[0], name, configs) + id, err := getSelectedProjectId(h, args) + if err != nil { + return cmdx.PrintOpenAPIError(cmd, err) + } + p, err := h.UpdateProject(id, name, configs) if err != nil { return cmdx.PrintOpenAPIError(cmd, err) } diff --git a/cmd/cloudx/project/update_identity_config.go b/cmd/cloudx/project/update_identity_config.go index 042070a2..0e8cbace 100644 --- a/cmd/cloudx/project/update_identity_config.go +++ b/cmd/cloudx/project/update_identity_config.go @@ -12,12 +12,12 @@ import ( func NewUpdateIdentityConfigCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "identity-config ", + Use: "identity-config [project-id]", Aliases: []string{ "ic", "kratos-config", }, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Short: "Update the Ory Identities configuration of the specified Ory Network project.", Example: `$ ory update identity-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 \ --file /path/to/config.json \ diff --git a/cmd/cloudx/project/update_namespace_config.go b/cmd/cloudx/project/update_namespace_config.go index c3e8d265..aef59115 100644 --- a/cmd/cloudx/project/update_namespace_config.go +++ b/cmd/cloudx/project/update_namespace_config.go @@ -44,7 +44,11 @@ class Example implements Namespace {} patch := fmt.Sprintf(`/services/permission/config/namespaces={"location": "base64://%s"}`, base64.StdEncoding.EncodeToString(data)) - project, err := h.GetProject(flagx.MustGetString(cmd, "project")) + projectOrSlug, err := client.ProjectOrDefault(cmd, h) + if err != nil { + return err + } + project, err := h.GetProject(projectOrSlug) if err != nil { return err } diff --git a/cmd/cloudx/project/update_namespace_config_test.go b/cmd/cloudx/project/update_namespace_config_test.go index 6e92ed95..f9a41694 100644 --- a/cmd/cloudx/project/update_namespace_config_test.go +++ b/cmd/cloudx/project/update_namespace_config_test.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/require" "github.com/tidwall/gjson" - "github.com/ory/cli/cmd/cloudx/testhelpers" "github.com/ory/x/fetcher" ) @@ -33,23 +32,24 @@ func writeFile(t *testing.T, content string) (path string) { } func TestUpdateNamespaceConfig(t *testing.T) { - project := testhelpers.CreateProject(t, defaultConfig) content := `class Default implements Namespace {}` config := writeFile(t, content) verbs := []string{"update", "patch"} for _, verb := range verbs { t.Run(fmt.Sprintf("is able to %q the namespace config", verb), func(t *testing.T) { - stdout, stderr, err := defaultCmd.Exec(nil, verb, "opl", "--project", project, "--format", "json", "--file", config) - require.NoError(t, err, stderr) - - if !testing.Short() { - // Don't download and compare the config in short mode, might not have internet everywhere - url := gjson.Get(stdout, "namespaces.location").String() - data, err := fetcher.NewFetcher().Fetch(url) - require.NoError(t, err, "could not download the config") - assert.Equal(t, content, data.String(), "the downloaded file does not match what we uploaded") - } + runWithProject(t, func(t *testing.T, exec execFunc, _ string) { + stdout, stderr, err := exec(nil, verb, "opl", "--format", "json", "--file", config) + require.NoError(t, err, stderr) + + if !testing.Short() { + // Don't download and compare the config in short mode, might not have internet everywhere + url := gjson.Get(stdout, "namespaces.location").String() + data, err := fetcher.NewFetcher().Fetch(url) + require.NoError(t, err, "could not download the config") + assert.Equal(t, content, data.String(), "the downloaded file does not match what we uploaded") + } + }, WithDefaultProject, WithFlagProject) }) } } diff --git a/cmd/cloudx/project/update_oauth2_config.go b/cmd/cloudx/project/update_oauth2_config.go index 0d1db9a2..bdfcf7fa 100644 --- a/cmd/cloudx/project/update_oauth2_config.go +++ b/cmd/cloudx/project/update_oauth2_config.go @@ -13,12 +13,12 @@ import ( func NewUpdateOAuth2ConfigCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "oauth2-config ", + Use: "oauth2-config [project-id]", Aliases: []string{ "oc", "hydra-config", }, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Short: "Update the Ory OAuth2 & OpenID Connect configuration of the specified Ory Network project.", Example: `$ ory update oauth2-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 \ --file /path/to/config.json \ diff --git a/cmd/cloudx/project/update_permission_config.go b/cmd/cloudx/project/update_permission_config.go index 190a92b9..ff270624 100644 --- a/cmd/cloudx/project/update_permission_config.go +++ b/cmd/cloudx/project/update_permission_config.go @@ -12,12 +12,12 @@ import ( func NewUpdatePermissionConfigCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "permission-config ", + Use: "permission-config [project-id]", Aliases: []string{ "pc", "keto-config", }, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Short: "Update Ory Permissions configuration of the specified Ory Network project.", Example: `$ ory update permission-config ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 \ --file /path/to/config.json \ diff --git a/cmd/cloudx/project/use.go b/cmd/cloudx/project/use.go new file mode 100644 index 00000000..5aec7319 --- /dev/null +++ b/cmd/cloudx/project/use.go @@ -0,0 +1,56 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package project + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/ory/cli/cmd/cloudx/client" + "github.com/ory/x/cmdx" +) + +func NewUseProjectCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "project [id]", + Args: cobra.MaximumNArgs(1), + Short: "Set the project as the default. When no id is provided, prints the currently used default project.", + Example: `$ ory use project ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 + +ID ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 + +$ ory use project ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 --format json + +{ + "id": "ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 +}`, + RunE: func(cmd *cobra.Command, args []string) error { + h, err := client.NewCommandHelper(cmd) + if err != nil { + return err + } + + var id string + if len(args) == 0 { + if id = h.GetDefaultProjectID(); id == "" { + _, _ = fmt.Println("No default project selected") + return nil + } + } else { + id = args[0] + err = h.SetDefaultProject(id) + if err != nil { + return cmdx.PrintOpenAPIError(cmd, err) + } + } + + cmdx.PrintRow(cmd, &selectedProject{ID: id}) + return nil + }, + } + + cmdx.RegisterFormatFlags(cmd.Flags()) + return cmd +} diff --git a/cmd/cloudx/project/use_test.go b/cmd/cloudx/project/use_test.go new file mode 100644 index 00000000..8628de2e --- /dev/null +++ b/cmd/cloudx/project/use_test.go @@ -0,0 +1,31 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package project_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/cli/cmd/cloudx/testhelpers" +) + +func TestUseProject(t *testing.T) { + t.Run("is able to use project", func(t *testing.T) { + testhelpers.SetDefaultProject(t, defaultConfig, defaultProject) + + stdout, _, err := defaultCmd.Exec(nil, "use", "project", extraProject, "--format", "json") + require.NoError(t, err) + assert.Equal(t, extraProject, gjson.Get(stdout, "id").String()) + }) + t.Run("is able to print default project", func(t *testing.T) { + testhelpers.SetDefaultProject(t, defaultConfig, defaultProject) + + stdout, _, err := defaultCmd.Exec(nil, "use", "project", "--format", "json") + require.NoError(t, err) + assert.Equal(t, defaultProject, gjson.Get(stdout, "id").String()) + }) +} diff --git a/cmd/cloudx/project/utils.go b/cmd/cloudx/project/utils.go index 09ca1502..2d42a2a1 100644 --- a/cmd/cloudx/project/utils.go +++ b/cmd/cloudx/project/utils.go @@ -6,13 +6,29 @@ package project import ( "encoding/json" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/tidwall/sjson" + "github.com/ory/cli/cmd/cloudx/client" cloud "github.com/ory/client-go" "github.com/ory/x/cmdx" ) +var defaultProjectNotSetError = errors.New("no project was specified") + +func getSelectedProjectId(h *client.CommandHelper, args []string) (string, error) { + if len(args) == 0 { + if id := h.GetDefaultProjectID(); id == "" { + return "", defaultProjectNotSetError + } else { + return id, nil + } + } else { + return args[0], nil + } +} + func prefixConfig(prefix string, s []string) []string { for k := range s { s[k] = prefix + s[k] diff --git a/cmd/cloudx/relationtuples/relationtuples_test.go b/cmd/cloudx/relationtuples/relationtuples_test.go index 5c8808d6..6fd90571 100644 --- a/cmd/cloudx/relationtuples/relationtuples_test.go +++ b/cmd/cloudx/relationtuples/relationtuples_test.go @@ -24,7 +24,7 @@ var ( ) func TestMain(m *testing.M) { - _, defaultEmail, defaultPassword, project, defaultCmd = testhelpers.CreateDefaultAssets() + _, defaultEmail, defaultPassword, _, project, defaultCmd = testhelpers.CreateDefaultAssets() testhelpers.RunAgainstStaging(m) } diff --git a/cmd/cloudx/testhelpers/testhelpers.go b/cmd/cloudx/testhelpers/testhelpers.go index f20e1529..738a629a 100644 --- a/cmd/cloudx/testhelpers/testhelpers.go +++ b/cmd/cloudx/testhelpers/testhelpers.go @@ -162,6 +162,35 @@ func CreateProject(t require.TestingT, configDir string) string { name := TestProjectName() stdout, stderr, err := cmd.Exec(nil, "create", "project", "--name", name, "--format", "json") require.NoError(t, err, "stdout: %s\nstderr: %s", stderr) + id := gjson.Get(stdout, "id").String() + return id +} + +func CreateAndUseProject(t require.TestingT, configDir string) string { + cmd := ConfigAwareCmd(configDir) + name := TestProjectName() + stdout, stderr, err := cmd.Exec(nil, "create", "project", "--name", name, "--use-project", "--format", "json") + require.NoError(t, err, "stdout: %s\nstderr: %s", stderr) + ac := ReadConfig(t, configDir) + id := gjson.Get(stdout, "id").String() + assert.Equal(t, ac.SelectedProject.String(), id) + return id +} + +func SetDefaultProject(t require.TestingT, configDir string, projectId string) { + cmd := ConfigAwareCmd(configDir) + stdout, stderr, err := cmd.Exec(nil, "use", "project", projectId, "--format", "json") + require.NoError(t, err, "stdout: %s\nstderr: %s", stderr) + ac := ReadConfig(t, configDir) + id := gjson.Get(stdout, "id").String() + assert.Equal(t, ac.SelectedProject.String(), id) + assert.Equal(t, projectId, id) +} + +func GetDefaultProject(t require.TestingT, configDir string) string { + cmd := ConfigAwareCmd(configDir) + stdout, stderr, err := cmd.Exec(nil, "use", "project", "--format", "json") + require.NoError(t, err, "stdout: %s\nstderr: %s", stderr) ac := ReadConfig(t, configDir) id := gjson.Get(stdout, "id").String() assert.Equal(t, ac.SelectedProject.String(), id) diff --git a/cmd/cloudx/testhelpers/testmain.go b/cmd/cloudx/testhelpers/testmain.go index 6b217ab3..11939f24 100644 --- a/cmd/cloudx/testhelpers/testmain.go +++ b/cmd/cloudx/testhelpers/testmain.go @@ -25,7 +25,7 @@ func UseStaging() { setEnvIfUnset("ORY_CLOUD_ORYAPIS_URL", "https://staging.oryapis.dev:443") } -func CreateDefaultAssets() (defaultConfig, defaultEmail, defaultPassword, defaultProject string, defaultCmd *cmdx.CommandExecuter) { +func CreateDefaultAssets() (defaultConfig, defaultEmail, defaultPassword, extraProject, defaultProject string, defaultCmd *cmdx.CommandExecuter) { UseStaging() t := testingT{} @@ -33,6 +33,7 @@ func CreateDefaultAssets() (defaultConfig, defaultEmail, defaultPassword, defaul defaultConfig = NewConfigDir(t) defaultEmail, defaultPassword = RegisterAccount(t, defaultConfig) + extraProject = CreateProject(t, defaultConfig) defaultProject = CreateProject(t, defaultConfig) defaultCmd = ConfigAwareCmd(defaultConfig) return diff --git a/cmd/cloudx/use.go b/cmd/cloudx/use.go new file mode 100644 index 00000000..ceadccd5 --- /dev/null +++ b/cmd/cloudx/use.go @@ -0,0 +1,30 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cloudx + +import ( + "github.com/spf13/cobra" + + "github.com/ory/cli/cmd/cloudx/client" + "github.com/ory/cli/cmd/cloudx/project" + "github.com/ory/x/cmdx" +) + +func NewUseCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "use", + Short: "Use a resource", + } + + cmd.AddCommand( + project.NewUseProjectCmd(), + ) + + client.RegisterConfigFlag(cmd.PersistentFlags()) + client.RegisterYesFlag(cmd.PersistentFlags()) + cmdx.RegisterNoiseFlags(cmd.PersistentFlags()) + cmdx.RegisterJSONFormatFlags(cmd.PersistentFlags()) + + return cmd +} diff --git a/cmd/root.go b/cmd/root.go index d588f87d..4749ed4b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -33,6 +33,7 @@ func NewRootCmd() *cobra.Command { jsonnet.NewLintCmd(), cloudx.NewDeleteCmd(), cloudx.NewGetCmd(), + cloudx.NewUseCmd(), cloudx.NewListCmd(), cloudx.NewImportCmd(), cloudx.NewPatchCmd(),