diff --git a/apps/cli-go/internal/orgs/create/create.go b/apps/cli-go/internal/orgs/create/create.go index 1d88630c18..0d8d13ab68 100644 --- a/apps/cli-go/internal/orgs/create/create.go +++ b/apps/cli-go/internal/orgs/create/create.go @@ -1,9 +1,12 @@ package create import ( + "bytes" "context" + "encoding/json" "fmt" "os" + "strings" "github.com/go-errors/errors" "github.com/supabase/cli/internal/orgs/list" @@ -11,8 +14,24 @@ import ( "github.com/supabase/cli/pkg/api" ) +type createOrganizationRequest struct { + Name string `json:"name"` + HeardFrom string `json:"heard_from,omitempty"` + Building string `json:"building,omitempty"` +} + +var newConsole = utils.NewConsole + func Run(ctx context.Context, name string) error { - resp, err := utils.GetSupabase().V1CreateAnOrganizationWithResponse(ctx, api.V1CreateAnOrganizationJSONRequestBody{Name: name}) + body, err := buildCreateOrganizationRequest(ctx, name) + if err != nil { + return err + } + payload, err := json.Marshal(body) + if err != nil { + return errors.Errorf("failed to encode organization request: %w", err) + } + resp, err := utils.GetSupabase().V1CreateAnOrganizationWithBodyWithResponse(ctx, "application/json", bytes.NewReader(payload)) if err != nil { return errors.Errorf("failed to create organization: %w", err) } else if resp.JSON201 == nil { @@ -26,3 +45,25 @@ func Run(ctx context.Context, name string) error { } return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON201) } + +func buildCreateOrganizationRequest(ctx context.Context, name string) (createOrganizationRequest, error) { + body := createOrganizationRequest{Name: name} + console := newConsole() + if utils.OutputFormat.Value != utils.OutputPretty || !console.IsTTY { + return body, nil + } + + heardFrom, err := console.PromptText(ctx, "Where did you hear about us? ") + if err != nil { + return body, err + } + body.HeardFrom = strings.TrimSpace(heardFrom) + + building, err := console.PromptText(ctx, "What are you building? ") + if err != nil { + return body, err + } + body.Building = strings.TrimSpace(building) + + return body, nil +} diff --git a/apps/cli-go/internal/orgs/create/create_test.go b/apps/cli-go/internal/orgs/create/create_test.go index f491bdc314..014c9d07c1 100644 --- a/apps/cli-go/internal/orgs/create/create_test.go +++ b/apps/cli-go/internal/orgs/create/create_test.go @@ -9,6 +9,7 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/testing/fstest" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/api" ) @@ -16,6 +17,11 @@ import ( func TestOrganizationCreateCommand(t *testing.T) { orgName := "Test Organization" + t.Cleanup(func() { + newConsole = utils.NewConsole + utils.OutputFormat.Value = utils.OutputPretty + }) + t.Run("create an organization", func(t *testing.T) { // Setup valid access token token := apitest.RandomAccessToken(t) @@ -24,6 +30,106 @@ func TestOrganizationCreateCommand(t *testing.T) { defer gock.OffAll() gock.New(utils.DefaultApiHost). Post("/v1/organizations"). + MatchType("json"). + JSON(createOrganizationRequest{Name: orgName}). + Reply(http.StatusCreated). + JSON(api.OrganizationResponseV1{ + Id: "combined-fuchsia-lion", + Name: orgName, + }) + // Run test + assert.NoError(t, Run(context.Background(), orgName)) + // Validate api + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("sends optional survey fields from interactive prompts", func(t *testing.T) { + // Setup valid access token + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + t.Cleanup(fstest.MockStdin(t, "GitHub\nAI coding assistant\n")) + newConsole = func() *utils.Console { + console := utils.NewConsole() + console.IsTTY = true + return console + } + t.Cleanup(func() { + newConsole = utils.NewConsole + }) + // Flush pending mocks after test execution + defer gock.OffAll() + gock.New(utils.DefaultApiHost). + Post("/v1/organizations"). + MatchType("json"). + JSON(createOrganizationRequest{ + Name: orgName, + HeardFrom: "GitHub", + Building: "AI coding assistant", + }). + Reply(http.StatusCreated). + JSON(api.OrganizationResponseV1{ + Id: "combined-fuchsia-lion", + Name: orgName, + }) + // Run test + assert.NoError(t, Run(context.Background(), orgName)) + // Validate api + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("omits blank survey prompt answers", func(t *testing.T) { + // Setup valid access token + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + t.Cleanup(fstest.MockStdin(t, "\n\n")) + newConsole = func() *utils.Console { + console := utils.NewConsole() + console.IsTTY = true + return console + } + t.Cleanup(func() { + newConsole = utils.NewConsole + }) + // Flush pending mocks after test execution + defer gock.OffAll() + gock.New(utils.DefaultApiHost). + Post("/v1/organizations"). + MatchType("json"). + JSON(createOrganizationRequest{Name: orgName}). + Reply(http.StatusCreated). + JSON(api.OrganizationResponseV1{ + Id: "combined-fuchsia-lion", + Name: orgName, + }) + // Run test + assert.NoError(t, Run(context.Background(), orgName)) + // Validate api + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("skips survey prompts for structured output", func(t *testing.T) { + // Setup valid access token + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + utils.OutputFormat.Value = utils.OutputJson + t.Cleanup(func() { + utils.OutputFormat.Value = utils.OutputPretty + }) + t.Cleanup(fstest.MockStdin(t, "GitHub\nAI coding assistant\n")) + newConsole = func() *utils.Console { + console := utils.NewConsole() + console.IsTTY = true + return console + } + t.Cleanup(func() { + newConsole = utils.NewConsole + }) + // Flush pending mocks after test execution + defer gock.OffAll() + gock.New(utils.DefaultApiHost). + Post("/v1/organizations"). + MatchType("json"). + JSON(createOrganizationRequest{Name: orgName}). Reply(http.StatusCreated). JSON(api.OrganizationResponseV1{ Id: "combined-fuchsia-lion", @@ -43,6 +149,8 @@ func TestOrganizationCreateCommand(t *testing.T) { defer gock.OffAll() gock.New(utils.DefaultApiHost). Post("/v1/organizations"). + MatchType("json"). + JSON(createOrganizationRequest{Name: orgName}). ReplyError(errors.New("network error")) // Run test assert.Error(t, Run(context.Background(), orgName)) @@ -58,6 +166,8 @@ func TestOrganizationCreateCommand(t *testing.T) { defer gock.OffAll() gock.New(utils.DefaultApiHost). Post("/v1/organizations"). + MatchType("json"). + JSON(createOrganizationRequest{Name: orgName}). Reply(http.StatusServiceUnavailable). JSON(map[string]string{"message": "unavailable"}) // Run test