-
Notifications
You must be signed in to change notification settings - Fork 13
sc-235696/list projects #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f4c65bd
6c25ca4
d5d9150
6bc6f21
4e3a1fd
90fd9e8
5657dbf
c943717
2189e1c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package projects | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "ld-cli/internal/projects" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "github.com/spf13/viper" | ||
| ) | ||
|
|
||
| func NewListCmd() *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Return a list of projects", | ||
| Long: "Return a list of projects", | ||
| 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") | ||
| } | ||
|
|
||
| client := projects.NewClient( | ||
| viper.GetString("accessToken"), | ||
| viper.GetString("baseUri"), | ||
| ) | ||
| response, err := projects.ListProjects( | ||
| context.Background(), | ||
| client, | ||
| ) | ||
| if err != nil { | ||
| 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 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package projects | ||
|
|
||
| import ( | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| func NewProjectsCmd() *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "projects", | ||
| Short: "Make requests (list, create, etc.) on projects", | ||
| Long: "Make requests (list, create, etc.) on projects", | ||
| } | ||
|
|
||
| cmd.AddCommand(NewListCmd()) | ||
|
|
||
| return cmd | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,8 @@ import ( | |
|
|
||
| "github.com/spf13/cobra" | ||
| "github.com/spf13/viper" | ||
|
|
||
| "ld-cli/cmd/projects" | ||
| ) | ||
|
|
||
| var rootCmd = &cobra.Command{ | ||
|
|
@@ -21,31 +23,35 @@ func Execute() { | |
| } | ||
|
|
||
| func init() { | ||
| var accessToken string | ||
| var ( | ||
| accessToken string | ||
| baseURI string | ||
| ) | ||
|
|
||
| rootCmd.PersistentFlags().StringVarP( | ||
| &accessToken, | ||
| "baseUri", | ||
| "u", | ||
| "http://localhost:3000", | ||
| "LaunchDarkly base URI.", | ||
| "accessToken", | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alphabetized these. |
||
| "t", | ||
| "", | ||
| "LaunchDarkly personal access token", | ||
| ) | ||
| err := viper.BindPFlag("baseUri", rootCmd.PersistentFlags().Lookup("baseUri")) | ||
| err := viper.BindPFlag("accessToken", rootCmd.PersistentFlags().Lookup("accessToken")) | ||
| if err != nil { | ||
| os.Exit(1) | ||
| } | ||
| rootCmd.PersistentFlags().StringVarP( | ||
| &accessToken, | ||
| "accessToken", | ||
| "t", | ||
| "", | ||
| "LaunchDarkly personal access token with write-level access.", | ||
| &baseURI, | ||
| "baseUri", | ||
| "u", | ||
| "http://localhost:3000", | ||
| "LaunchDarkly base URI", | ||
| ) | ||
| err = viper.BindPFlag("accessToken", rootCmd.PersistentFlags().Lookup("accessToken")) | ||
| err = viper.BindPFlag("baseUri", rootCmd.PersistentFlags().Lookup("baseUri")) | ||
| if err != nil { | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| rootCmd.AddCommand(newHelloCmd()) | ||
| rootCmd.AddCommand(projects.NewProjectsCmd()) | ||
| rootCmd.AddCommand(setupCmd) | ||
| } | ||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package projects | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
|
|
||
| ldapi "github.com/launchdarkly/api-client-go/v14" | ||
| ) | ||
|
|
||
| type Client interface { | ||
| List(context.Context) (*ldapi.Projects, error) | ||
| } | ||
|
|
||
| type ProjectsClient struct { | ||
| client *ldapi.APIClient | ||
| } | ||
|
|
||
| func NewClient(accessToken string, baseURI string) ProjectsClient { | ||
| config := ldapi.NewConfiguration() | ||
| config.AddDefaultHeader("Authorization", accessToken) | ||
| config.Servers[0].URL = baseURI | ||
| client := ldapi.NewAPIClient(config) | ||
|
|
||
| return ProjectsClient{ | ||
| client: client, | ||
| } | ||
| } | ||
|
|
||
| func (c ProjectsClient) List(ctx context.Context) (*ldapi.Projects, error) { | ||
| projects, _, err := c.client.ProjectsApi. | ||
| GetProjects(ctx). | ||
| 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) | ||
| if err != nil { | ||
| // 401 - should return unauthorized type error with body(?) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll follow up with a PR to handle errors. |
||
| // 404 - should return not found type error with body | ||
| e, ok := err.(ldapi.GenericOpenAPIError) | ||
| if ok { | ||
| return e.Body(), err | ||
| } | ||
| return nil, err | ||
| } | ||
|
|
||
| projectsJSON, err := json.Marshal(projects) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return projectsJSON, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| package projects_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "ld-cli/internal/projects" | ||
| "testing" | ||
|
|
||
| ldapi "github.com/launchdarkly/api-client-go/v14" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func strPtr(s string) *string { | ||
| return &s | ||
| } | ||
|
|
||
| type GetProjectsResponse struct{} | ||
|
|
||
| type MockClient struct{} | ||
|
|
||
| func (c MockClient) List(ctx context.Context) (*ldapi.Projects, error) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll change this once we have better error handling. |
||
| totalCount := int32(1) | ||
|
|
||
| return &ldapi.Projects{ | ||
| Links: map[string]ldapi.Link{ | ||
| "last": { | ||
| Href: strPtr("/api/v2/projects?expand=environments&limit=1&offset=1"), | ||
| Type: strPtr("application/json"), | ||
| }, | ||
| "next": { | ||
| Href: strPtr("/api/v2/projects?expand=environments&limit=1&offset=0"), | ||
| Type: strPtr("application/json"), | ||
| }, | ||
| "self": { | ||
| Href: strPtr("/api/v2/projects?expand=environments&limit=1"), | ||
| Type: strPtr("application/json"), | ||
| }, | ||
| }, | ||
| Items: []ldapi.Project{ | ||
| { | ||
| Id: "000000000000000000000001", | ||
| Key: "test-project", | ||
| }, | ||
| }, | ||
| TotalCount: &totalCount, | ||
| }, nil | ||
| } | ||
|
|
||
| func TestListProjects(t *testing.T) { | ||
| t.Run("returns a paginated list of projects", func(t *testing.T) { | ||
| expected := `{ | ||
| "_links": { | ||
| "last": { | ||
| "href": "/api/v2/projects?expand=environments&limit=1&offset=1", | ||
| "type": "application/json" | ||
| }, | ||
| "next": { | ||
| "href": "/api/v2/projects?expand=environments&limit=1&offset=0", | ||
| "type": "application/json" | ||
| }, | ||
| "self": { | ||
| "href": "/api/v2/projects?expand=environments&limit=1", | ||
| "type": "application/json" | ||
| } | ||
| }, | ||
| "items": [ | ||
| { | ||
| "_id": "000000000000000000000001", | ||
| "_links": null, | ||
| "includeInSnippetByDefault": false, | ||
| "key": "test-project", | ||
| "name": "", | ||
| "tags": null | ||
| } | ||
| ], | ||
| "totalCount": 1 | ||
| }` | ||
| mockClient := MockClient{} | ||
|
|
||
| response, err := projects.ListProjects(context.Background(), mockClient) | ||
|
|
||
| require.NoError(t, err) | ||
| assert.JSONEq(t, expected, string(response)) | ||
| }) | ||
|
|
||
| t.Run("with invalid accessToken is forbidden", func(t *testing.T) { | ||
| }) | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit but do we care about imports ordering?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed this in another PR.