From 1d441ba45cf727382ec765f2a0c971b0cf67bb44 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Mon, 9 Feb 2026 16:37:34 +0800 Subject: [PATCH 1/2] chore: unit tests for vanity subdomain --- cmd/vanitySubdomains.go | 3 +- .../vanity_subdomains/activate/activate.go | 33 +++----- .../activate/activate_test.go | 76 +++++++++++++++++ internal/vanity_subdomains/check/check.go | 33 +++----- .../vanity_subdomains/check/check_test.go | 73 +++++++++++++++++ internal/vanity_subdomains/delete/delete.go | 21 ++--- .../vanity_subdomains/delete/delete_test.go | 54 ++++++++++++ internal/vanity_subdomains/get/get.go | 29 ++++--- internal/vanity_subdomains/get/get_test.go | 82 +++++++++++++++++++ 9 files changed, 334 insertions(+), 70 deletions(-) create mode 100644 internal/vanity_subdomains/activate/activate_test.go create mode 100644 internal/vanity_subdomains/check/check_test.go create mode 100644 internal/vanity_subdomains/delete/delete_test.go create mode 100644 internal/vanity_subdomains/get/get_test.go diff --git a/cmd/vanitySubdomains.go b/cmd/vanitySubdomains.go index dd3608c53e..3faab35f28 100644 --- a/cmd/vanitySubdomains.go +++ b/cmd/vanitySubdomains.go @@ -64,11 +64,12 @@ After the vanity subdomain is activated, your project's auth services will no lo func init() { vanityCmd.PersistentFlags().StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") vanityActivateCmd.Flags().StringVar(&desiredSubdomain, "desired-subdomain", "", "The desired vanity subdomain to use for your Supabase project.") + vanityActivateCmd.MarkFlagRequired("desired-subdomain") vanityCheckCmd.Flags().StringVar(&desiredSubdomain, "desired-subdomain", "", "The desired vanity subdomain to use for your Supabase project.") + vanityCheckCmd.MarkFlagRequired("desired-subdomain") vanityCmd.AddCommand(vanityGetCmd) vanityCmd.AddCommand(vanityCheckCmd) vanityCmd.AddCommand(vanityActivateCmd) vanityCmd.AddCommand(vanityDeleteCmd) - rootCmd.AddCommand(vanityCmd) } diff --git a/internal/vanity_subdomains/activate/activate.go b/internal/vanity_subdomains/activate/activate.go index 133ec7edd3..87eb748ea9 100644 --- a/internal/vanity_subdomains/activate/activate.go +++ b/internal/vanity_subdomains/activate/activate.go @@ -3,7 +3,7 @@ package activate import ( "context" "fmt" - "strings" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -12,26 +12,17 @@ import ( ) func Run(ctx context.Context, projectRef string, desiredSubdomain string, fsys afero.Fs) error { - // 1. Sanity checks. - subdomain := strings.TrimSpace(desiredSubdomain) - { - if len(subdomain) == 0 { - return errors.New("non-empty vanity subdomain expected") - } + resp, err := utils.GetSupabase().V1ActivateVanitySubdomainConfigWithResponse(ctx, projectRef, api.V1ActivateVanitySubdomainConfigJSONRequestBody{ + VanitySubdomain: desiredSubdomain, + }) + if err != nil { + return errors.Errorf("failed activate vanity subdomain: %w", err) + } else if resp.JSON201 == nil { + return errors.Errorf("unexpected activate vanity subdomain status %d: %s", resp.StatusCode(), string(resp.Body)) } - - // 2. create vanity subdomain - { - resp, err := utils.GetSupabase().V1ActivateVanitySubdomainConfigWithResponse(ctx, projectRef, api.V1ActivateVanitySubdomainConfigJSONRequestBody{ - VanitySubdomain: subdomain, - }) - if err != nil { - return errors.Errorf("failed activate vanity subdomain: %w", err) - } - if resp.JSON201 == nil { - return errors.New("failed to create vanity subdomain config: " + string(resp.Body)) - } - fmt.Printf("Activated vanity subdomain at %s\n", resp.JSON201.CustomDomain) - return nil + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON201) } + fmt.Printf("Activated vanity subdomain at %s\n", resp.JSON201.CustomDomain) + return nil } diff --git a/internal/vanity_subdomains/activate/activate_test.go b/internal/vanity_subdomains/activate/activate_test.go new file mode 100644 index 0000000000..fb3ea7f84d --- /dev/null +++ b/internal/vanity_subdomains/activate/activate_test.go @@ -0,0 +1,76 @@ +package activate + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "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/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestActivateSubdomain(t *testing.T) { + t.Run("actives vanity subdomain", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, "Activated vanity subdomain at example.com\n")) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusCreated). + JSON(api.ActivateVanitySubdomainResponse{ + CustomDomain: "example.com", + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `CustomDomain = "example.com" +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusCreated). + JSON(api.ActivateVanitySubdomainResponse{ + CustomDomain: "example.com", + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.ErrorContains(t, err, "unexpected activate vanity subdomain status 503:") + }) +} diff --git a/internal/vanity_subdomains/check/check.go b/internal/vanity_subdomains/check/check.go index 3901227071..a9ecb639cc 100644 --- a/internal/vanity_subdomains/check/check.go +++ b/internal/vanity_subdomains/check/check.go @@ -3,7 +3,7 @@ package check import ( "context" "fmt" - "strings" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -12,26 +12,17 @@ import ( ) func Run(ctx context.Context, projectRef string, desiredSubdomain string, fsys afero.Fs) error { - // 1. Sanity checks. - subdomain := strings.TrimSpace(desiredSubdomain) - { - if len(subdomain) == 0 { - return errors.New("non-empty vanity subdomain expected") - } + resp, err := utils.GetSupabase().V1CheckVanitySubdomainAvailabilityWithResponse(ctx, projectRef, api.V1CheckVanitySubdomainAvailabilityJSONRequestBody{ + VanitySubdomain: desiredSubdomain, + }) + if err != nil { + return errors.Errorf("failed to check vanity subdomain: %w", err) + } else if resp.JSON201 == nil { + return errors.Errorf("unexpected check vanity subdomain status %d: %s", resp.StatusCode(), string(resp.Body)) } - - // 2. check if the subdomain is available - { - resp, err := utils.GetSupabase().V1CheckVanitySubdomainAvailabilityWithResponse(ctx, projectRef, api.V1CheckVanitySubdomainAvailabilityJSONRequestBody{ - VanitySubdomain: subdomain, - }) - if err != nil { - return errors.Errorf("failed to check vanity subdomain: %w", err) - } - if resp.JSON201 == nil { - return errors.New("failed to check subdomain availability: " + string(resp.Body)) - } - fmt.Printf("Subdomain %s available: %+v\n", subdomain, resp.JSON201.Available) - return nil + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON201) } + fmt.Printf("Subdomain %s available: %+v\n", desiredSubdomain, resp.JSON201.Available) + return nil } diff --git a/internal/vanity_subdomains/check/check_test.go b/internal/vanity_subdomains/check/check_test.go new file mode 100644 index 0000000000..7702ab59c5 --- /dev/null +++ b/internal/vanity_subdomains/check/check_test.go @@ -0,0 +1,73 @@ +package check + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "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/internal/utils/flags" + "github.com/supabase/cli/pkg/api" +) + +func TestCheckSubdomain(t *testing.T) { + t.Run("checks subdomain availability", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, "Subdomain example.com available: true\n")) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/vanity-subdomain/check-availability"). + Reply(http.StatusCreated). + JSON(api.SubdomainAvailabilityResponse{ + Available: true, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, "Available = false\n")) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("v1/projects/" + flags.ProjectRef + "/vanity-subdomain/check-availability"). + Reply(http.StatusCreated). + JSON(api.SubdomainAvailabilityResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain/check-availability"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Post("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain/check-availability"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, "example.com", nil) + assert.ErrorContains(t, err, "unexpected check vanity subdomain status 503:") + }) +} diff --git a/internal/vanity_subdomains/delete/delete.go b/internal/vanity_subdomains/delete/delete.go index 3ee24d473d..969b63de7a 100644 --- a/internal/vanity_subdomains/delete/delete.go +++ b/internal/vanity_subdomains/delete/delete.go @@ -3,6 +3,8 @@ package delete import ( "context" "fmt" + "net/http" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -10,17 +12,12 @@ import ( ) func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { - // 1. Sanity checks. - // 2. delete config - { - resp, err := utils.GetSupabase().V1DeactivateVanitySubdomainConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to delete vanity subdomain: %w", err) - } - if resp.StatusCode() != 200 { - return errors.New("failed to delete vanity subdomain config; received: " + string(resp.Body)) - } - fmt.Println("Deleted vanity subdomain successfully.") - return nil + resp, err := utils.GetSupabase().V1DeactivateVanitySubdomainConfigWithResponse(ctx, projectRef) + if err != nil { + return errors.Errorf("failed to delete vanity subdomain: %w", err) + } else if resp.StatusCode() != http.StatusOK { + return errors.Errorf("unexpected delete vanity subdomain status %d: %s", resp.StatusCode(), string(resp.Body)) } + fmt.Fprintln(os.Stderr, "Deleted vanity subdomain successfully.") + return nil } diff --git a/internal/vanity_subdomains/delete/delete_test.go b/internal/vanity_subdomains/delete/delete_test.go new file mode 100644 index 0000000000..1a09295f60 --- /dev/null +++ b/internal/vanity_subdomains/delete/delete_test.go @@ -0,0 +1,54 @@ +package delete + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" +) + +func TestDeleteSubdomain(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("deletes vanity subdomain", func(t *testing.T) { + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusOK) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Delete("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected delete vanity subdomain status 503:") + }) +} diff --git a/internal/vanity_subdomains/get/get.go b/internal/vanity_subdomains/get/get.go index b466f18958..303e9edec1 100644 --- a/internal/vanity_subdomains/get/get.go +++ b/internal/vanity_subdomains/get/get.go @@ -3,6 +3,7 @@ package get import ( "context" "fmt" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -10,20 +11,18 @@ import ( ) func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { - // 1. Sanity checks. - // 2. get vanity subdomain config - { - response, err := utils.GetSupabase().V1GetVanitySubdomainConfigWithResponse(ctx, projectRef) - if err != nil { - return errors.Errorf("failed to get vanity subdomain: %w", err) - } - if response.JSON200 == nil { - return errors.Errorf("failed to obtain vanity subdomain config: %+v", string(response.Body)) - } - fmt.Printf("Status: %s\n", response.JSON200.Status) - if response.JSON200.CustomDomain != nil { - fmt.Printf("Vanity subdomain: %s\n", *response.JSON200.CustomDomain) - } - return nil + resp, err := utils.GetSupabase().V1GetVanitySubdomainConfigWithResponse(ctx, projectRef) + if err != nil { + return errors.Errorf("failed to get vanity subdomain: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected vanity subdomain status %d: %s", resp.StatusCode(), string(resp.Body)) } + if utils.OutputFormat.Value != utils.OutputPretty { + return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, *resp.JSON200) + } + fmt.Printf("Status: %s\n", resp.JSON200.Status) + if resp.JSON200.CustomDomain != nil { + fmt.Printf("Vanity subdomain: %s\n", *resp.JSON200.CustomDomain) + } + return nil } diff --git a/internal/vanity_subdomains/get/get_test.go b/internal/vanity_subdomains/get/get_test.go new file mode 100644 index 0000000000..8ec0428f3b --- /dev/null +++ b/internal/vanity_subdomains/get/get_test.go @@ -0,0 +1,82 @@ +package get + +import ( + "context" + "net/http" + "testing" + + "github.com/go-errors/errors" + "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/internal/utils/flags" + "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/cast" +) + +func TestGetSubdomain(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("get vanity subdomains", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, `Status: custom-domain-used +Vanity subdomain: example.com +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusOK). + JSON(api.VanitySubdomainConfigResponse{ + CustomDomain: cast.Ptr("example.com"), + Status: api.CustomDomainUsed, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("encodes toml output", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputToml + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(fstest.MockStdout(t, `Status = "not-used" +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusOK). + JSON(api.VanitySubdomainConfigResponse{ + Status: api.NotUsed, + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on network error", func(t *testing.T) { + errNetwork := errors.New("network error") + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + t.Run("throws error on service unavailable", func(t *testing.T) { + utils.OutputFormat.Value = utils.OutputEnv + t.Cleanup(func() { utils.OutputFormat.Value = utils.OutputPretty }) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/vanity-subdomain"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected vanity subdomain status 503:") + }) +} From 8bd8e0213155fde31ee6b81714246049ec5447a7 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Mon, 9 Feb 2026 16:50:31 +0800 Subject: [PATCH 2/2] chore: check flag error --- cmd/vanitySubdomains.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/vanitySubdomains.go b/cmd/vanitySubdomains.go index 3faab35f28..44d87e8d7a 100644 --- a/cmd/vanitySubdomains.go +++ b/cmd/vanitySubdomains.go @@ -64,9 +64,9 @@ After the vanity subdomain is activated, your project's auth services will no lo func init() { vanityCmd.PersistentFlags().StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") vanityActivateCmd.Flags().StringVar(&desiredSubdomain, "desired-subdomain", "", "The desired vanity subdomain to use for your Supabase project.") - vanityActivateCmd.MarkFlagRequired("desired-subdomain") + cobra.CheckErr(vanityActivateCmd.MarkFlagRequired("desired-subdomain")) vanityCheckCmd.Flags().StringVar(&desiredSubdomain, "desired-subdomain", "", "The desired vanity subdomain to use for your Supabase project.") - vanityCheckCmd.MarkFlagRequired("desired-subdomain") + cobra.CheckErr(vanityCheckCmd.MarkFlagRequired("desired-subdomain")) vanityCmd.AddCommand(vanityGetCmd) vanityCmd.AddCommand(vanityCheckCmd) vanityCmd.AddCommand(vanityActivateCmd)