From e0dcb82cacc441aa4da6be33b8802f840f272201 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 26 Jun 2026 15:50:43 +0100 Subject: [PATCH 1/4] feat(update): add default-org subcommand to set user default organization Adds `kosli update default-org ORG-NAME` which calls PUT /api/v2/user/{org} to set the default organization for the currently authenticated user. --- cmd/kosli/root.go | 1 + cmd/kosli/update.go | 25 +++++++++++++ cmd/kosli/updateDefaultOrg.go | 58 ++++++++++++++++++++++++++++++ cmd/kosli/updateDefaultOrg_test.go | 55 ++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 cmd/kosli/update.go create mode 100644 cmd/kosli/updateDefaultOrg.go create mode 100644 cmd/kosli/updateDefaultOrg_test.go diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index 9b4ec0625..ed9bd668f 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -439,6 +439,7 @@ func newRootCmd(out, errOut io.Writer, args []string) (*cobra.Command, error) { newEvaluateCmd(out), newDeleteCmd(out), newRotateCmd(out), + newUpdateCmd(out), ) cobra.AddTemplateFunc("isBeta", isBeta) diff --git a/cmd/kosli/update.go b/cmd/kosli/update.go new file mode 100644 index 000000000..466ab7514 --- /dev/null +++ b/cmd/kosli/update.go @@ -0,0 +1,25 @@ +package main + +import ( + "io" + + "github.com/spf13/cobra" +) + +const updateDesc = `All Kosli update commands.` + +func newUpdateCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Aliases: []string{"u", "up"}, + Short: updateDesc, + Long: updateDesc, + } + + // Add subcommands + cmd.AddCommand( + newUpdateDefaultOrgCmd(out), + ) + + return cmd +} diff --git a/cmd/kosli/updateDefaultOrg.go b/cmd/kosli/updateDefaultOrg.go new file mode 100644 index 000000000..cfd53e972 --- /dev/null +++ b/cmd/kosli/updateDefaultOrg.go @@ -0,0 +1,58 @@ +package main + +import ( + "io" + "net/http" + "net/url" + + "github.com/kosli-dev/cli/internal/requests" + "github.com/spf13/cobra" +) + +const updateDefaultOrgShortDesc = `Set the default organization for the current user.` + +const updateDefaultOrgLongDesc = updateDefaultOrgShortDesc + +const updateDefaultOrgExample = ` +# set the default organization for the current user: +kosli update default-org yourOrgName \ + --api-token yourAPIToken +` + +func newUpdateDefaultOrgCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "default-org ORG-NAME", + Short: updateDefaultOrgShortDesc, + Long: updateDefaultOrgLongDesc, + Example: updateDefaultOrgExample, + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, args []string) error { + err := RequireGlobalFlags(global, []string{"ApiToken"}) + if err != nil { + return ErrorBeforePrintingUsage(cmd, err.Error()) + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + url, err := url.JoinPath(global.Host, "api/v2/user", args[0]) + if err != nil { + return err + } + + reqParams := &requests.RequestParams{ + Method: http.MethodPut, + URL: url, + DryRun: global.DryRun, + Token: global.ApiToken, + } + _, err = kosliClient.Do(reqParams) + if err == nil && !global.DryRun { + logger.Info("default organization is set to: %s", args[0]) + } + return err + }, + } + addDryRunFlag(cmd) + return cmd +} diff --git a/cmd/kosli/updateDefaultOrg_test.go b/cmd/kosli/updateDefaultOrg_test.go new file mode 100644 index 000000000..3a65b8397 --- /dev/null +++ b/cmd/kosli/updateDefaultOrg_test.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" +) + +type UpdateDefaultOrgCommandTestSuite struct { + suite.Suite + defaultKosliArguments string +} + +func (suite *UpdateDefaultOrgCommandTestSuite) SetupTest() { + global = &GlobalOpts{ + ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY", + Host: "http://localhost:8001", + } + suite.defaultKosliArguments = fmt.Sprintf(" --host %s --api-token %s", global.Host, global.ApiToken) +} + +func (suite *UpdateDefaultOrgCommandTestSuite) TestUpdateDefaultOrgCmd() { + tests := []cmdTestCase{ + { + name: "can set default organization", + cmd: fmt.Sprintf(`update default-org docs-cmd-test-user %s`, suite.defaultKosliArguments), + golden: "default organization is set to: docs-cmd-test-user\n", + }, + { + wantError: true, + name: "setting default org fails when no args are provided", + cmd: fmt.Sprintf(`update default-org %s`, suite.defaultKosliArguments), + golden: "Error: accepts 1 arg(s), received 0\n", + }, + { + wantError: true, + name: "setting default org fails when 2 args are provided", + cmd: fmt.Sprintf(`update default-org org1 org2 %s`, suite.defaultKosliArguments), + golden: "Error: accepts 1 arg(s), received 2\n", + }, + { + wantError: true, + name: "setting default org fails for non-existing org", + cmd: fmt.Sprintf(`update default-org non-existing-org-abc123 %s`, suite.defaultKosliArguments), + golden: "Error: Organization named 'non-existing-org-abc123' does not exist\n", + }, + } + + runTestCmd(suite.T(), tests) +} + +func TestUpdateDefaultOrgCommandTestSuite(t *testing.T) { + suite.Run(t, new(UpdateDefaultOrgCommandTestSuite)) +} From 8687db9a5a715338a825bf8b09aa3e6a87a9639f Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 26 Jun 2026 16:06:12 +0100 Subject: [PATCH 2/4] fix: update default-org testing --- cmd/kosli/updateDefaultOrg_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kosli/updateDefaultOrg_test.go b/cmd/kosli/updateDefaultOrg_test.go index 3a65b8397..173b2fe0f 100644 --- a/cmd/kosli/updateDefaultOrg_test.go +++ b/cmd/kosli/updateDefaultOrg_test.go @@ -43,7 +43,7 @@ func (suite *UpdateDefaultOrgCommandTestSuite) TestUpdateDefaultOrgCmd() { wantError: true, name: "setting default org fails for non-existing org", cmd: fmt.Sprintf(`update default-org non-existing-org-abc123 %s`, suite.defaultKosliArguments), - golden: "Error: Organization named 'non-existing-org-abc123' does not exist\n", + golden: "Error: Access denied to /api/v2/user/non-existing-org-abc123\n", }, } From df0caad43d9537d76f1c543f198bce662387338a Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 26 Jun 2026 16:23:04 +0100 Subject: [PATCH 3/4] chore: add longDesc for update defaulr-rg --- cmd/kosli/updateDefaultOrg.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/kosli/updateDefaultOrg.go b/cmd/kosli/updateDefaultOrg.go index cfd53e972..779d07881 100644 --- a/cmd/kosli/updateDefaultOrg.go +++ b/cmd/kosli/updateDefaultOrg.go @@ -11,7 +11,9 @@ import ( const updateDefaultOrgShortDesc = `Set the default organization for the current user.` -const updateDefaultOrgLongDesc = updateDefaultOrgShortDesc +const updateDefaultOrgLongDesc = updateDefaultOrgShortDesc + ` +The default organization is used by Kosli Web UI when logging in. +` const updateDefaultOrgExample = ` # set the default organization for the current user: From c878cf30186f35010b4036eb5b25730a4001c9f8 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 26 Jun 2026 16:24:01 +0100 Subject: [PATCH 4/4] chore: check for empty org-name and add Dry run testing --- cmd/kosli/updateDefaultOrg.go | 4 ++++ cmd/kosli/updateDefaultOrg_test.go | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/cmd/kosli/updateDefaultOrg.go b/cmd/kosli/updateDefaultOrg.go index 779d07881..eba1ab28f 100644 --- a/cmd/kosli/updateDefaultOrg.go +++ b/cmd/kosli/updateDefaultOrg.go @@ -34,6 +34,10 @@ func newUpdateDefaultOrgCmd(out io.Writer) *cobra.Command { return ErrorBeforePrintingUsage(cmd, err.Error()) } + if args[0] == "" { + return ErrorBeforePrintingUsage(cmd, "ORG-NAME argument is required") + } + return nil }, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/kosli/updateDefaultOrg_test.go b/cmd/kosli/updateDefaultOrg_test.go index 173b2fe0f..2036e5f4c 100644 --- a/cmd/kosli/updateDefaultOrg_test.go +++ b/cmd/kosli/updateDefaultOrg_test.go @@ -27,6 +27,12 @@ func (suite *UpdateDefaultOrgCommandTestSuite) TestUpdateDefaultOrgCmd() { cmd: fmt.Sprintf(`update default-org docs-cmd-test-user %s`, suite.defaultKosliArguments), golden: "default organization is set to: docs-cmd-test-user\n", }, + { + wantError: false, + name: "dry-run builds the right url without contacting the server", + cmd: fmt.Sprintf(`update default-org docs-cmd-test-user --dry-run %s`, suite.defaultKosliArguments), + goldenRegex: `the request would have been sent to: .*api/v2/user/docs-cmd-test-user`, + }, { wantError: true, name: "setting default org fails when no args are provided", @@ -39,6 +45,12 @@ func (suite *UpdateDefaultOrgCommandTestSuite) TestUpdateDefaultOrgCmd() { cmd: fmt.Sprintf(`update default-org org1 org2 %s`, suite.defaultKosliArguments), golden: "Error: accepts 1 arg(s), received 2\n", }, + { + wantError: true, + name: "setting default org fails when org name is empty", + cmd: fmt.Sprintf(`update default-org "" %s`, suite.defaultKosliArguments), + golden: "Error: ORG-NAME argument is required\nUsage: kosli update default-org ORG-NAME [flags]\n", + }, { wantError: true, name: "setting default org fails for non-existing org",