Skip to content

Commit

Permalink
feat: list projects err handling (#21)
Browse files Browse the repository at this point in the history
Better error handling
  • Loading branch information
dbolson authored Mar 13, 2024
1 parent 44aa1b5 commit 2ab3668
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 35 deletions.
35 changes: 19 additions & 16 deletions cmd/projects/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,40 @@ package projects

import (
"context"
"errors"
"fmt"
"ld-cli/internal/projects"
"net/url"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"ld-cli/internal/errors"
"ld-cli/internal/projects"
)

func NewListCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Return a list of projects",
Long: "Return a list of projects",
RunE: runList,
Use: "list",
Short: "Return a list of projects",
Long: "Return a list of projects",
PreRunE: validate,
RunE: runList,
}

cmd.AddCommand()

return cmd
}

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

return nil
}

// runList fetches a list of projects.
func runList(cmd *cobra.Command, args []string) error {
client := projects.NewClient(
viper.GetString("accessToken"),
viper.GetString("baseUri"),
Expand All @@ -44,7 +48,6 @@ func runList(cmd *cobra.Command, args []string) error {
return err
}

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

return nil
Expand Down
42 changes: 34 additions & 8 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
package cmd

import (
"errors"
"fmt"
"os"

errs "ld-cli/internal/errors"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"ld-cli/cmd/projects"
)

var rootCmd = &cobra.Command{
Use: "ldcli",
Short: "LaunchDarkly CLI",
Long: "LaunchDarkly CLI to control your feature flags",
Use: "ldcli",
Short: "LaunchDarkly CLI",
Long: "LaunchDarkly CLI to control your feature flags",
Version: "0.0.1", // TODO: set this based on release or use `cmd.SetVersionTemplate(s string)`

// Handle errors differently based on type.
// We don't want to show the usage if the user has the right structure but invalid data such as
// the wrong key.
SilenceUsage: true,
SilenceErrors: true,
}

func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
switch {
case errors.Is(err, errs.ErrInvalidBaseURI):
fmt.Fprintln(os.Stderr, err.Error())
case errors.Is(err, errs.ErrUnauthorized):
fmt.Fprintln(os.Stderr, err.Error())
default:
fmt.Println(rootCmd.ErrPrefix(), err.Error())
fmt.Println(rootCmd.UsageString())
}
}
}

Expand All @@ -35,22 +54,29 @@ func init() {
"",
"LaunchDarkly personal access token",
)
err := viper.BindPFlag("accessToken", rootCmd.PersistentFlags().Lookup("accessToken"))
err := rootCmd.MarkPersistentFlagRequired("accessToken")
if err != nil {
panic(err)
}
err = viper.BindPFlag("accessToken", rootCmd.PersistentFlags().Lookup("accessToken"))
if err != nil {
os.Exit(1)
panic(err)
}

rootCmd.PersistentFlags().StringVarP(
&baseURI,
"baseUri",
"u",
"http://localhost:3000",
"https://app.launchdarkly.com",
"LaunchDarkly base URI",
)
err = viper.BindPFlag("baseUri", rootCmd.PersistentFlags().Lookup("baseUri"))
if err != nil {
os.Exit(1)
panic(err)
}

rootCmd.SetErrPrefix("")

rootCmd.AddCommand(projects.NewProjectsCmd())
rootCmd.AddCommand(setupCmd)
}
8 changes: 8 additions & 0 deletions internal/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package errors

import "errors"

var (
ErrInvalidBaseURI = errors.New("baseUri is invalid")
ErrUnauthorized = errors.New("You are not authorized to make this request.")
)
15 changes: 7 additions & 8 deletions internal/projects/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"encoding/json"

ldapi "github.com/launchdarkly/api-client-go/v14"

"ld-cli/internal/errors"
)

type Client interface {
Expand Down Expand Up @@ -32,22 +34,19 @@ func (c ProjectsClient) List(ctx context.Context) (*ldapi.Projects, error) {
Limit(2).
Execute()
if err != nil {
// TODO: make this nicer
return nil, err
}

return projects, nil
}

func ListProjects(ctx context.Context, client2 Client) ([]byte, error) {
projects, err := client2.List(ctx)
func ListProjects(ctx context.Context, client Client) ([]byte, error) {
projects, err := client.List(ctx)
if err != nil {
// 401 - should return unauthorized type error with body(?)
// 404 - should return not found type error with body
e, ok := err.(ldapi.GenericOpenAPIError)
if ok {
return e.Body(), err
if err.Error() == "401 Unauthorized" {
return nil, errors.ErrUnauthorized
}

return nil, err
}

Expand Down
20 changes: 17 additions & 3 deletions internal/projects/projects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package projects_test

import (
"context"
"ld-cli/internal/projects"
"errors"
"testing"

ldapi "github.com/launchdarkly/api-client-go/v14"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"ld-cli/internal/projects"
)

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

type GetProjectsResponse struct{}

type MockClient struct{}
type MockClient struct {
hasUnauthorizedErr bool
}

func (c MockClient) List(ctx context.Context) (*ldapi.Projects, error) {
if c.hasUnauthorizedErr {
return nil, errors.New("401 Unauthorized")
}

totalCount := int32(1)

return &ldapi.Projects{
Expand Down Expand Up @@ -85,5 +92,12 @@ func TestListProjects(t *testing.T) {
})

t.Run("with invalid accessToken is forbidden", func(t *testing.T) {
mockClient := MockClient{
hasUnauthorizedErr: true,
}

_, err := projects.ListProjects(context.Background(), mockClient)

assert.EqualError(t, err, "You are not authorized to make this request.")
})
}

0 comments on commit 2ab3668

Please sign in to comment.