Skip to content

Commit 2ab3668

Browse files
authored
feat: list projects err handling (#21)
Better error handling
1 parent 44aa1b5 commit 2ab3668

5 files changed

Lines changed: 85 additions & 35 deletions

File tree

cmd/projects/list.go

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,40 @@ package projects
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
7-
"ld-cli/internal/projects"
6+
"net/url"
87

98
"github.com/spf13/cobra"
109
"github.com/spf13/viper"
10+
11+
"ld-cli/internal/errors"
12+
"ld-cli/internal/projects"
1113
)
1214

1315
func NewListCmd() *cobra.Command {
1416
cmd := &cobra.Command{
15-
Use: "list",
16-
Short: "Return a list of projects",
17-
Long: "Return a list of projects",
18-
RunE: runList,
17+
Use: "list",
18+
Short: "Return a list of projects",
19+
Long: "Return a list of projects",
20+
PreRunE: validate,
21+
RunE: runList,
1922
}
2023

21-
cmd.AddCommand()
22-
2324
return cmd
2425
}
2526

26-
func runList(cmd *cobra.Command, args []string) error {
27-
// TODO: handle missing flags
28-
if viper.GetString("accessToken") == "" {
29-
return errors.New("accessToken required")
30-
}
31-
if viper.GetString("baseUri") == "" {
32-
return errors.New("baseUri required")
27+
// validate ensures the flags are valid before using them.
28+
func validate(cmd *cobra.Command, args []string) error {
29+
_, err := url.ParseRequestURI(viper.GetString("baseUri"))
30+
if err != nil {
31+
return errors.ErrInvalidBaseURI
3332
}
3433

34+
return nil
35+
}
36+
37+
// runList fetches a list of projects.
38+
func runList(cmd *cobra.Command, args []string) error {
3539
client := projects.NewClient(
3640
viper.GetString("accessToken"),
3741
viper.GetString("baseUri"),
@@ -44,7 +48,6 @@ func runList(cmd *cobra.Command, args []string) error {
4448
return err
4549
}
4650

47-
// TODO: should this return response and let caller output or pass in stdout-ish interface?
4851
fmt.Fprintf(cmd.OutOrStdout(), string(response)+"\n")
4952

5053
return nil

cmd/root.go

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
11
package cmd
22

33
import (
4+
"errors"
5+
"fmt"
46
"os"
57

8+
errs "ld-cli/internal/errors"
9+
610
"github.com/spf13/cobra"
711
"github.com/spf13/viper"
812

913
"ld-cli/cmd/projects"
1014
)
1115

1216
var rootCmd = &cobra.Command{
13-
Use: "ldcli",
14-
Short: "LaunchDarkly CLI",
15-
Long: "LaunchDarkly CLI to control your feature flags",
17+
Use: "ldcli",
18+
Short: "LaunchDarkly CLI",
19+
Long: "LaunchDarkly CLI to control your feature flags",
20+
Version: "0.0.1", // TODO: set this based on release or use `cmd.SetVersionTemplate(s string)`
21+
22+
// Handle errors differently based on type.
23+
// We don't want to show the usage if the user has the right structure but invalid data such as
24+
// the wrong key.
25+
SilenceUsage: true,
26+
SilenceErrors: true,
1627
}
1728

1829
func Execute() {
1930
err := rootCmd.Execute()
2031
if err != nil {
21-
os.Exit(1)
32+
switch {
33+
case errors.Is(err, errs.ErrInvalidBaseURI):
34+
fmt.Fprintln(os.Stderr, err.Error())
35+
case errors.Is(err, errs.ErrUnauthorized):
36+
fmt.Fprintln(os.Stderr, err.Error())
37+
default:
38+
fmt.Println(rootCmd.ErrPrefix(), err.Error())
39+
fmt.Println(rootCmd.UsageString())
40+
}
2241
}
2342
}
2443

@@ -35,22 +54,29 @@ func init() {
3554
"",
3655
"LaunchDarkly personal access token",
3756
)
38-
err := viper.BindPFlag("accessToken", rootCmd.PersistentFlags().Lookup("accessToken"))
57+
err := rootCmd.MarkPersistentFlagRequired("accessToken")
58+
if err != nil {
59+
panic(err)
60+
}
61+
err = viper.BindPFlag("accessToken", rootCmd.PersistentFlags().Lookup("accessToken"))
3962
if err != nil {
40-
os.Exit(1)
63+
panic(err)
4164
}
65+
4266
rootCmd.PersistentFlags().StringVarP(
4367
&baseURI,
4468
"baseUri",
4569
"u",
46-
"http://localhost:3000",
70+
"https://app.launchdarkly.com",
4771
"LaunchDarkly base URI",
4872
)
4973
err = viper.BindPFlag("baseUri", rootCmd.PersistentFlags().Lookup("baseUri"))
5074
if err != nil {
51-
os.Exit(1)
75+
panic(err)
5276
}
5377

78+
rootCmd.SetErrPrefix("")
79+
5480
rootCmd.AddCommand(projects.NewProjectsCmd())
5581
rootCmd.AddCommand(setupCmd)
5682
}

internal/errors/errors.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package errors
2+
3+
import "errors"
4+
5+
var (
6+
ErrInvalidBaseURI = errors.New("baseUri is invalid")
7+
ErrUnauthorized = errors.New("You are not authorized to make this request.")
8+
)

internal/projects/projects.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"encoding/json"
66

77
ldapi "github.com/launchdarkly/api-client-go/v14"
8+
9+
"ld-cli/internal/errors"
810
)
911

1012
type Client interface {
@@ -32,22 +34,19 @@ func (c ProjectsClient) List(ctx context.Context) (*ldapi.Projects, error) {
3234
Limit(2).
3335
Execute()
3436
if err != nil {
35-
// TODO: make this nicer
3637
return nil, err
3738
}
3839

3940
return projects, nil
4041
}
4142

42-
func ListProjects(ctx context.Context, client2 Client) ([]byte, error) {
43-
projects, err := client2.List(ctx)
43+
func ListProjects(ctx context.Context, client Client) ([]byte, error) {
44+
projects, err := client.List(ctx)
4445
if err != nil {
45-
// 401 - should return unauthorized type error with body(?)
46-
// 404 - should return not found type error with body
47-
e, ok := err.(ldapi.GenericOpenAPIError)
48-
if ok {
49-
return e.Body(), err
46+
if err.Error() == "401 Unauthorized" {
47+
return nil, errors.ErrUnauthorized
5048
}
49+
5150
return nil, err
5251
}
5352

internal/projects/projects_test.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ package projects_test
22

33
import (
44
"context"
5-
"ld-cli/internal/projects"
5+
"errors"
66
"testing"
77

88
ldapi "github.com/launchdarkly/api-client-go/v14"
9-
109
"github.com/stretchr/testify/assert"
1110
"github.com/stretchr/testify/require"
11+
12+
"ld-cli/internal/projects"
1213
)
1314

1415
func strPtr(s string) *string {
@@ -17,9 +18,15 @@ func strPtr(s string) *string {
1718

1819
type GetProjectsResponse struct{}
1920

20-
type MockClient struct{}
21+
type MockClient struct {
22+
hasUnauthorizedErr bool
23+
}
2124

2225
func (c MockClient) List(ctx context.Context) (*ldapi.Projects, error) {
26+
if c.hasUnauthorizedErr {
27+
return nil, errors.New("401 Unauthorized")
28+
}
29+
2330
totalCount := int32(1)
2431

2532
return &ldapi.Projects{
@@ -85,5 +92,12 @@ func TestListProjects(t *testing.T) {
8592
})
8693

8794
t.Run("with invalid accessToken is forbidden", func(t *testing.T) {
95+
mockClient := MockClient{
96+
hasUnauthorizedErr: true,
97+
}
98+
99+
_, err := projects.ListProjects(context.Background(), mockClient)
100+
101+
assert.EqualError(t, err, "You are not authorized to make this request.")
88102
})
89103
}

0 commit comments

Comments
 (0)