Repository: supabase/cli
Pull request #1226 (2023-06-21 09:05:41)

feat(api): add api-keys command for projects
## What kind of change does this PR introduce?

Created `projects api-keys` sub-command. Added necessary test as well.  

## Additional context

I have improved the table rendering using charm/bubbles pkg. In terms of UX, it's better now and maybe we can implement the same everywhere else by adding a common styles file. Also here the keys are not getting word wrapped and I wasn't able to find a solution anywhere. Wdyt? Should we go with this or the same old glamour rendering?

![Screenshot 2023-06-21 210627](https://github.com/supabase/cli/assets/55504616/4cb6bbb2-8259-4372-b96c-ea18df499a44)



coveralls: ## Pull Request Test Coverage Report for [Build 5332185194](https://coveralls.io/builds/60864406)

* **31** of **38**   **(81.58%)**  changed or added relevant lines in **2** files are covered.
* **7** unchanged lines in **1** file lost coverage.
* Overall coverage increased (+**0.03%**) to **61.829%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [cmd/projects.go](https://coveralls.io/builds/60864406/source?filename=cmd%2Fprojects.go#L82) | 5 | 8 | 62.5%
| [internal/projects/apiKeys/api_keys.go](https://coveralls.io/builds/60864406/source?filename=internal%2Fprojects%2FapiKeys%2Fapi_keys.go#L39) | 26 | 30 | 86.67%
<!-- | **Total:** | **31** | **38** | **81.58%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/gen/keys/keys.go](https://coveralls.io/builds/60864406/source?filename=internal%2Fgen%2Fkeys%2Fkeys.go#L94) | 7 | 7.94% |
<!-- | **Total:** | **7** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/60864406/badge)](https://coveralls.io/builds/60864406) |
| :-- | --: |
| Change from base [Build 5327654295](https://coveralls.io/builds/60855313): |  0.03% |
| Covered Lines: | 4772 |
| Relevant Lines: | 7718 |

---
##### 💛  - [Coveralls](https://coveralls.io)

sweatybridge: > Also here the keys are not getting word wrapped and I wasn't able to find a solution anywhere. Wdyt? Should we go with this or the same old glamour rendering?

Bubbles doesn't seem to support word wrapping as it currently [truncates](https://github.com/charmbracelet/bubbles/blob/master/table/table.go#L396) cell content to `...` when it overflows. Not able to show the full key would make this command quite useless...

I do appreciate the new look though. Perhaps we can work on the styling on a separate PR.
sweatybridge: Thank you! The rest of the changes LGTM.
Ananya2001-an: Sorry for the unnecessary commits... I think we need a squash and merge here haha :)

Patch:
diff --git a/cmd/projects.go b/cmd/projects.go
index a887d328..39be58f5 100644
--- a/cmd/projects.go
+++ b/cmd/projects.go
@@ -11,9 +11,11 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 	"github.com/supabase/cli/internal/link"
+	"github.com/supabase/cli/internal/projects/apiKeys"
 	"github.com/supabase/cli/internal/projects/create"
 	"github.com/supabase/cli/internal/projects/list"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/internal/utils/flags"
 	"github.com/supabase/cli/pkg/api"
 )
 
@@ -72,6 +74,14 @@ var (
 			return list.Run(cmd.Context(), afero.NewOsFs())
 		},
 	}
+
+	projectsApiKeysCmd = &cobra.Command{
+		Use:   "api-keys",
+		Short: "List all API keys for a Supabase project",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return apiKeys.Run(cmd.Context(), flags.ProjectRef, afero.NewOsFs())
+		},
+	}
 )
 
 func init() {
@@ -90,9 +100,14 @@ func init() {
 	createFlags.Var(&region, "region", "Select a region close to you for the best performance.")
 	createFlags.Var(&plan, "plan", "Select a plan that suits your needs.")
 	cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", createFlags.Lookup("db-password")))
+
+	apiKeysFlags := projectsApiKeysCmd.Flags()
+	apiKeysFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.")
+
 	// Add commands to root
 	projectsCmd.AddCommand(projectsCreateCmd)
 	projectsCmd.AddCommand(projectsListCmd)
+	projectsCmd.AddCommand(projectsApiKeysCmd)
 	rootCmd.AddCommand(projectsCmd)
 }
 
diff --git a/internal/projects/apiKeys/api_keys.go b/internal/projects/apiKeys/api_keys.go
new file mode 100644
index 00000000..f9c7ee16
--- /dev/null
+++ b/internal/projects/apiKeys/api_keys.go
@@ -0,0 +1,32 @@
+package apiKeys
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/spf13/afero"
+	"github.com/supabase/cli/internal/migration/list"
+	"github.com/supabase/cli/internal/utils"
+)
+
+func Run(ctx context.Context, projectRef string, fsys afero.Fs) error {
+	resp, err := utils.GetSupabase().GetProjectApiKeysWithResponse(ctx, projectRef)
+	if err != nil {
+		return err
+	}
+
+	if resp.JSON200 == nil {
+		return errors.New("Unexpected error retrieving project api-keys: " + string(resp.Body))
+	}
+
+	table := `|NAME|KEY VALUE|
+|-|-|
+`
+	for _, api_key := range *resp.JSON200 {
+		table += fmt.Sprintf("|`%s`|`%x`|\n", strings.ReplaceAll(api_key.Name, "|", "\\|"), api_key.ApiKey)
+	}
+
+	return list.RenderTable(table)
+}


Test patch:
diff --git a/internal/projects/apiKeys/api_keys_test.go b/internal/projects/apiKeys/api_keys_test.go
new file mode 100644
index 00000000..db24bac0
--- /dev/null
+++ b/internal/projects/apiKeys/api_keys_test.go
@@ -0,0 +1,69 @@
+package apiKeys
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
+	"github.com/supabase/cli/internal/testing/apitest"
+	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/api"
+	"gopkg.in/h2non/gock.v1"
+)
+
+func TestProjectApiKeysCommand(t *testing.T) {
+	t.Run("lists all api-keys", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// Setup valid access token
+		token := apitest.RandomAccessToken(t)
+		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
+		// Flush pending mocks after test execution
+		defer gock.OffAll()
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/api-keys").
+			Reply(200).
+			JSON([]api.ApiKeyResponse{{
+				Name:   "Test ApiKey",
+				ApiKey: "dummy-api-key-value",
+			}})
+		// Run test
+		err := Run(context.Background(), project, fsys)
+		// Check error
+		assert.NoError(t, err)
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	t.Run("throws error on missing access token", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Run test
+		err := Run(context.Background(), "", fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Unexpected error retrieving project api-keys")
+	})
+
+	t.Run("throws error on network error", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// Setup valid access token
+		token := apitest.RandomAccessToken(t)
+		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
+		// Flush pending mocks after test execution
+		defer gock.OffAll()
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/api-keys").
+			ReplyError(errors.New("network error"))
+		// Run test
+		err := Run(context.Background(), project, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "network error")
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+}