Repository: supabase/cli
Pull request #1249 (2023-06-26 11:23:21)

feat: implement command to create organization
## What kind of change does this PR introduce?

feature

## What is the new behavior?

```bash
supabase orgs create <name>
```

## Additional context

Add any other context or screenshots.


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

* **12** of **16**   **(75.0%)**  changed or added relevant lines in **3** files are covered.
* **8** unchanged lines in **2** files lost coverage.
* Overall coverage decreased (**-0.04%**) to **61.831%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [cmd/orgs.go](https://coveralls.io/builds/60954904/source?filename=cmd%2Forgs.go#L21) | 1 | 5 | 20.0%
<!-- | **Total:** | **12** | **16** | **75.0%** | -->

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

|  Totals | [![Coverage Status](https://coveralls.io/builds/60954904/badge)](https://coveralls.io/builds/60954904) |
| :-- | --: |
| Change from base [Build 5374711274](https://coveralls.io/builds/60948528): |  -0.04% |
| Covered Lines: | 4761 |
| Relevant Lines: | 7700 |

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


Patch:
diff --git a/cmd/orgs.go b/cmd/orgs.go
index a68933c2..5d0750db 100644
--- a/cmd/orgs.go
+++ b/cmd/orgs.go
@@ -1,8 +1,8 @@
 package cmd
 
 import (
-	"github.com/spf13/afero"
 	"github.com/spf13/cobra"
+	"github.com/supabase/cli/internal/orgs/create"
 	"github.com/supabase/cli/internal/orgs/list"
 )
 
@@ -18,12 +18,23 @@ var (
 		Short: "List all organizations",
 		Long:  "List all organizations the logged-in user belongs.",
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return list.Run(cmd.Context(), afero.NewOsFs())
+			return list.Run(cmd.Context())
+		},
+	}
+
+	orgsCreateCmd = &cobra.Command{
+		Use:   "create",
+		Short: "Create an organization",
+		Long:  "Create an organization for the logged-in user.",
+		Args:  cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return create.Run(cmd.Context(), args[0])
 		},
 	}
 )
 
 func init() {
 	orgsCmd.AddCommand(orgsListCmd)
+	orgsCmd.AddCommand(orgsCreateCmd)
 	rootCmd.AddCommand(orgsCmd)
 }
diff --git a/internal/orgs/create/create.go b/internal/orgs/create/create.go
new file mode 100644
index 00000000..7cddbc4e
--- /dev/null
+++ b/internal/orgs/create/create.go
@@ -0,0 +1,24 @@
+package create
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/api"
+)
+
+func Run(ctx context.Context, name string) error {
+	resp, err := utils.GetSupabase().CreateOrganizationWithResponse(ctx, api.CreateOrganizationJSONRequestBody{Name: name})
+	if err != nil {
+		return err
+	}
+
+	if resp.JSON201 == nil {
+		return errors.New("Unexpected error creating organization: " + string(resp.Body))
+	}
+
+	fmt.Println("Created organization:", resp.JSON201.Id)
+	return nil
+}
diff --git a/internal/orgs/list/list.go b/internal/orgs/list/list.go
index 8d1e04cb..a1d308bc 100644
--- a/internal/orgs/list/list.go
+++ b/internal/orgs/list/list.go
@@ -6,17 +6,11 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
 )
 
-type Organization struct {
-	Id   string `json:"id"`
-	Name string `json:"name"`
-}
-
-func Run(ctx context.Context, fsys afero.Fs) error {
+func Run(ctx context.Context) error {
 	resp, err := utils.GetSupabase().GetOrganizationsWithResponse(ctx)
 	if err != nil {
 		return err


Test patch:
diff --git a/internal/orgs/create/create_test.go b/internal/orgs/create/create_test.go
new file mode 100644
index 00000000..ac0304ae
--- /dev/null
+++ b/internal/orgs/create/create_test.go
@@ -0,0 +1,68 @@
+package create
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"testing"
+
+	"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 TestOrganizationCreateCommand(t *testing.T) {
+	orgName := "Test Organization"
+
+	t.Run("create an organization", func(t *testing.T) {
+		// 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).
+			Post("/v1/organizations").
+			Reply(http.StatusCreated).
+			JSON(api.OrganizationResponse{
+				Id:   "combined-fuchsia-lion",
+				Name: orgName,
+			})
+		// Run test
+		assert.NoError(t, Run(context.Background(), orgName))
+		// Validate api
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	t.Run("throws error on network error", func(t *testing.T) {
+		// 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).
+			Post("/v1/organizations").
+			ReplyError(errors.New("network error"))
+		// Run test
+		assert.Error(t, Run(context.Background(), orgName))
+		// Validate api
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	t.Run("throws error on server unavailable", func(t *testing.T) {
+		// 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).
+			Post("/v1/organizations").
+			Reply(http.StatusServiceUnavailable).
+			JSON(map[string]string{"message": "unavailable"})
+		// Run test
+		assert.Error(t, Run(context.Background(), orgName))
+		// Validate api
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+}
diff --git a/internal/orgs/list/list_test.go b/internal/orgs/list/list_test.go
index 804f0712..6542b456 100644
--- a/internal/orgs/list/list_test.go
+++ b/internal/orgs/list/list_test.go
@@ -3,19 +3,18 @@ package list
 import (
 	"context"
 	"errors"
+	"net/http"
 	"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 TestOrganizationListCommand(t *testing.T) {
 	t.Run("lists all organizations", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
 		// Setup valid access token
 		token := apitest.RandomAccessToken(t)
 		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
@@ -23,26 +22,20 @@ func TestOrganizationListCommand(t *testing.T) {
 		defer gock.OffAll()
 		gock.New(utils.DefaultApiHost).
 			Get("/v1/organizations").
-			Reply(200).
-			JSON([]Organization{
+			Reply(http.StatusOK).
+			JSON([]api.OrganizationResponse{
 				{
 					Id:   "combined-fuchsia-lion",
 					Name: "Test Organization",
 				},
 			})
 		// Run test
-		assert.NoError(t, Run(context.Background(), fsys))
+		assert.NoError(t, Run(context.Background()))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
-	t.Run("throws error on failure to load token", func(t *testing.T) {
-		assert.Error(t, Run(context.Background(), afero.NewMemMapFs()))
-	})
-
 	t.Run("throws error on network error", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
 		// Setup valid access token
 		token := apitest.RandomAccessToken(t)
 		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
@@ -52,14 +45,12 @@ func TestOrganizationListCommand(t *testing.T) {
 			Get("/v1/organizations").
 			ReplyError(errors.New("network error"))
 		// Run test
-		assert.Error(t, Run(context.Background(), fsys))
+		assert.Error(t, Run(context.Background()))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
 	t.Run("throws error on server unavailable", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
 		// Setup valid access token
 		token := apitest.RandomAccessToken(t)
 		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
@@ -67,28 +58,10 @@ func TestOrganizationListCommand(t *testing.T) {
 		defer gock.OffAll()
 		gock.New(utils.DefaultApiHost).
 			Get("/v1/organizations").
-			Reply(500).
+			Reply(http.StatusServiceUnavailable).
 			JSON(map[string]string{"message": "unavailable"})
 		// Run test
-		assert.Error(t, Run(context.Background(), fsys))
-		// Validate api
-		assert.Empty(t, apitest.ListUnmatchedRequests())
-	})
-
-	t.Run("throws error on malformed json", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// 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/organizations").
-			Reply(200).
-			JSON(map[string]string{})
-		// Run test
-		assert.Error(t, Run(context.Background(), fsys))
+		assert.Error(t, Run(context.Background()))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})