From 714b9b1b83f5207c8b4d41fc88d81ad47ab77e2c Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Mon, 9 Feb 2026 18:05:02 +0800 Subject: [PATCH] chore: unit tests for pg config --- internal/postgresConfig/delete/delete.go | 17 ++-- internal/postgresConfig/delete/delete_test.go | 94 ++++++++++++++++++ internal/postgresConfig/get/get.go | 27 ++---- internal/postgresConfig/get/get_test.go | 85 +++++++++++++++++ internal/postgresConfig/update/update.go | 13 +-- internal/postgresConfig/update/update_test.go | 95 +++++++++++++++++++ 6 files changed, 298 insertions(+), 33 deletions(-) create mode 100644 internal/postgresConfig/delete/delete_test.go create mode 100644 internal/postgresConfig/get/get_test.go create mode 100644 internal/postgresConfig/update/update_test.go diff --git a/internal/postgresConfig/delete/delete.go b/internal/postgresConfig/delete/delete.go index 20b7b978f..86ecee630 100644 --- a/internal/postgresConfig/delete/delete.go +++ b/internal/postgresConfig/delete/delete.go @@ -35,14 +35,15 @@ func Run(ctx context.Context, projectRef string, configKeys []string, noRestart resp, err := utils.GetSupabase().V1UpdatePostgresConfigWithBodyWithResponse(ctx, projectRef, "application/json", bytes.NewReader(bts)) if err != nil { - return errors.Errorf("failed to update config overrides: %w", err) - } - if resp.JSON200 == nil { - if resp.StatusCode() == 400 { - return errors.Errorf("failed to update config overrides: %s (%s). This usually indicates that an unsupported or invalid config override was attempted. Please refer to https://supabase.com/docs/guides/platform/custom-postgres-config", resp.Status(), string(resp.Body)) - } - return errors.Errorf("failed to update config overrides: %s (%s)", resp.Status(), string(resp.Body)) + return errors.Errorf("failed to delete config overrides: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected delete config overrides status %d: %s", resp.StatusCode(), string(resp.Body)) } - return get.Run(ctx, projectRef, fsys) + var config map[string]any + err = json.Unmarshal(resp.Body, &config) + if err != nil { + return errors.Errorf("failed to unmarshal delete response: %w", err) + } + return get.PrintOutPostgresConfigOverrides(config) } diff --git a/internal/postgresConfig/delete/delete_test.go b/internal/postgresConfig/delete/delete_test.go new file mode 100644 index 000000000..e1bfa880b --- /dev/null +++ b/internal/postgresConfig/delete/delete_test.go @@ -0,0 +1,94 @@ +package delete + +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 TestDeleteConfig(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("deletes postgres config", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + Parameter | Value + -----------|------- + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + gock.New(utils.DefaultApiHost). + Put("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{}) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{"max_connections"}, true, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on missing project", 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 + "/config/database/postgres"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, false, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + 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 + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, false, 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 + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, false, nil) + assert.ErrorContains(t, err, "unexpected delete config overrides status 503:") + }) +} diff --git a/internal/postgresConfig/get/get.go b/internal/postgresConfig/get/get.go index 01a749ce4..e2142cf18 100644 --- a/internal/postgresConfig/get/get.go +++ b/internal/postgresConfig/get/get.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "io" "os" "strings" @@ -14,23 +13,18 @@ import ( ) func Run(ctx context.Context, projectRef string, fsys afero.Fs) error { - // 1. get current config config, err := GetCurrentPostgresConfig(ctx, projectRef) if err != nil { return err } - err = PrintOutPostgresConfigOverrides(config) - if err != nil { - return err - } - return nil + return PrintOutPostgresConfigOverrides(config) } func PrintOutPostgresConfigOverrides(config map[string]any) error { if utils.OutputFormat.Value != utils.OutputPretty { return utils.EncodeOutput(utils.OutputFormat.Value, os.Stdout, config) } - fmt.Println("- Custom Postgres Config -") + fmt.Fprintln(os.Stderr, "- Custom Postgres Config -") markdownTable := []string{ "|Parameter|Value|\n|-|-|\n", } @@ -43,26 +37,21 @@ func PrintOutPostgresConfigOverrides(config map[string]any) error { if err := utils.RenderTable(strings.Join(markdownTable, "")); err != nil { return err } - fmt.Println("- End of Custom Postgres Config -") + fmt.Fprintln(os.Stderr, "- End of Custom Postgres Config -") return nil } func GetCurrentPostgresConfig(ctx context.Context, projectRef string) (map[string]any, error) { - resp, err := utils.GetSupabase().V1GetPostgresConfig(ctx, projectRef) + resp, err := utils.GetSupabase().V1GetPostgresConfigWithResponse(ctx, projectRef) if err != nil { return nil, errors.Errorf("failed to retrieve Postgres config overrides: %w", err) - } - if resp.StatusCode != 200 { - return nil, errors.Errorf("error in retrieving Postgres config overrides: %s", resp.Status) - } - contents, err := io.ReadAll(resp.Body) - if err != nil { - return nil, errors.Errorf("failed to read response body: %w", err) + } else if resp.JSON200 == nil { + return nil, errors.Errorf("unexpected config overrides status %d: %s", resp.StatusCode(), string(resp.Body)) } var config map[string]any - err = json.Unmarshal(contents, &config) + err = json.Unmarshal(resp.Body, &config) if err != nil { - return nil, errors.Errorf("failed to unmarshal response body: %w. Contents were %s", err, contents) + return nil, errors.Errorf("failed to unmarshal response body: %w", err) } return config, nil } diff --git a/internal/postgresConfig/get/get_test.go b/internal/postgresConfig/get/get_test.go new file mode 100644 index 000000000..149c70758 --- /dev/null +++ b/internal/postgresConfig/get/get_test.go @@ -0,0 +1,85 @@ +package get + +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/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 TestPostgresConfig(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("get postgres config", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + Parameter | Value + -----------------|------- + max_connections | 100 + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + // 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, `max_connections = 100.0 +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Get("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + // 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 + "/config/database/postgres"). + 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 + "/config/database/postgres"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, nil) + assert.ErrorContains(t, err, "unexpected config overrides status 503:") + }) +} diff --git a/internal/postgresConfig/update/update.go b/internal/postgresConfig/update/update.go index 4d7e42087..ffb565ca0 100644 --- a/internal/postgresConfig/update/update.go +++ b/internal/postgresConfig/update/update.go @@ -67,13 +67,14 @@ func Run(ctx context.Context, projectRef string, values []string, replaceOverrid resp, err := utils.GetSupabase().V1UpdatePostgresConfigWithBodyWithResponse(ctx, projectRef, "application/json", bytes.NewReader(bts)) if err != nil { return errors.Errorf("failed to update config overrides: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected update config overrides status %d: %s", resp.StatusCode(), string(resp.Body)) } - if resp.JSON200 == nil { - if resp.StatusCode() == 400 { - return errors.Errorf("failed to update config overrides: %s (%s). This usually indicates that an unsupported or invalid config override was attempted. Please refer to https://supabase.com/docs/guides/platform/custom-postgres-config", resp.Status(), string(resp.Body)) - } - return errors.Errorf("failed to update config overrides: %s (%s)", resp.Status(), string(resp.Body)) + var config map[string]any + err = json.Unmarshal(resp.Body, &config) + if err != nil { + return errors.Errorf("failed to unmarshal update response: %w", err) } + return get.PrintOutPostgresConfigOverrides(config) } - return get.Run(ctx, projectRef, fsys) } diff --git a/internal/postgresConfig/update/update_test.go b/internal/postgresConfig/update/update_test.go new file mode 100644 index 000000000..f8b65de9c --- /dev/null +++ b/internal/postgresConfig/update/update_test.go @@ -0,0 +1,95 @@ +package update + +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/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 TestUpdatePostgresConfig(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("updates postgres config", func(t *testing.T) { + t.Cleanup(fstest.MockStdout(t, ` + + Parameter | Value + -----------------|------- + max_connections | 100 + +`)) + t.Cleanup(apitest.MockPlatformAPI(t)) + // Setup mock api + gock.New(utils.DefaultApiHost). + Put("v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{ + "max_connections=100", + "track_commit_timestamp=true", + "statement_timeout=600", + "wal_keep_size=1GB", + }, true, true, nil) + assert.NoError(t, err) + }) + + t.Run("throws error on missing key", func(t *testing.T) { + err := Run(context.Background(), flags.ProjectRef, []string{"value"}, true, true, nil) + assert.ErrorContains(t, err, "expected config value in key:value format") + }) + + t.Run("throws error on missing project", 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 + "/config/database/postgres"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, false, false, nil) + assert.ErrorIs(t, err, errNetwork) + }) + + 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 + "/config/database/postgres"). + Reply(http.StatusOK). + JSON(api.PostgresConfigResponse{ + MaxConnections: cast.Ptr(100), + }) + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + ReplyError(errNetwork) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, false, false, 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). + Put("/v1/projects/" + flags.ProjectRef + "/config/database/postgres"). + Reply(http.StatusServiceUnavailable) + // Run test + err := Run(context.Background(), flags.ProjectRef, []string{}, true, true, nil) + assert.ErrorContains(t, err, "unexpected update config overrides status 503:") + }) +}