From cfd7f06d6a6ae6acaf6796e745e77ff121bdb97e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 04:10:06 +0000 Subject: [PATCH 01/22] chore(deps): bump github.com/golangci/golangci-lint/v2 from 2.1.2 to 2.1.5 (#3482) chore(deps): bump github.com/golangci/golangci-lint/v2 Bumps [github.com/golangci/golangci-lint/v2](https://github.com/golangci/golangci-lint) from 2.1.2 to 2.1.5. - [Release notes](https://github.com/golangci/golangci-lint/releases) - [Changelog](https://github.com/golangci/golangci-lint/blob/main/CHANGELOG.md) - [Commits](https://github.com/golangci/golangci-lint/compare/v2.1.2...v2.1.5) --- updated-dependencies: - dependency-name: github.com/golangci/golangci-lint/v2 dependency-version: 2.1.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 7f4bf61a08..96c41c4478 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.2.1 github.com/go-xmlfmt/xmlfmt v1.1.3 github.com/golang-jwt/jwt/v5 v5.2.2 - github.com/golangci/golangci-lint/v2 v2.1.2 + github.com/golangci/golangci-lint/v2 v2.1.5 github.com/google/go-github/v62 v62.0.0 github.com/google/go-querystring v1.1.0 github.com/google/uuid v1.6.0 @@ -68,7 +68,6 @@ require ( github.com/Antonboom/nilnil v1.1.0 // indirect github.com/Antonboom/testifylint v1.6.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/Crocmagnon/fatcontext v0.7.2 // indirect github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect @@ -306,6 +305,7 @@ require ( gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.13.0 // indirect go-simpler.org/sloglint v0.11.0 // indirect + go.augendre.info/fatcontext v0.8.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect diff --git a/go.sum b/go.sum index b6e257b91d..b0a84118bc 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Crocmagnon/fatcontext v0.7.2 h1:BY5/dUhs2kuD3sDn7vZrgOneRib5EHk9GOiyK8Vg+14= -github.com/Crocmagnon/fatcontext v0.7.2/go.mod h1:OAZCUteH59eiddbJZ9/bF4ppC140jYD/hepU2FDkFk4= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k= @@ -416,8 +414,8 @@ github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUP github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= -github.com/golangci/golangci-lint/v2 v2.1.2 h1:bcOB+jVr4EYEgOEIskQIhtdxOpIGl+iOCwliG/hNPXw= -github.com/golangci/golangci-lint/v2 v2.1.2/go.mod h1:ApmXnYUmWDGu1CUZRkT3yzzFATmaViCY7BEtytG2AiU= +github.com/golangci/golangci-lint/v2 v2.1.5 h1:zDcxV8s7kgQW3cpQiVA633CZJnKN/0iEXibPDWO8sZo= +github.com/golangci/golangci-lint/v2 v2.1.5/go.mod h1:RGcjZLyl9fSVLqxdKMrknPlspC3TYETLoKXyRG06RDo= github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 h1:AkK+w9FZBXlU/xUmBtSJN1+tAI4FIvy5WtnUnY8e4p8= github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95/go.mod h1:k9mmcyWKSTMcPPvQUCfRWWQ9VHJ1U9Dc0R7kaXAgtnQ= github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= @@ -1022,6 +1020,8 @@ go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= go-simpler.org/sloglint v0.11.0 h1:JlR1X4jkbeaffiyjLtymeqmGDKBDO1ikC6rjiuFAOco= go-simpler.org/sloglint v0.11.0/go.mod h1:CFDO8R1i77dlciGfPEPvYke2ZMx4eyGiEIWkyeW2Pvw= +go.augendre.info/fatcontext v0.8.0 h1:2dfk6CQbDGeu1YocF59Za5Pia7ULeAM6friJ3LP7lmk= +go.augendre.info/fatcontext v0.8.0/go.mod h1:oVJfMgwngMsHO+KB2MdgzcO+RvtNdiCEOlWvSFtax/s= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= From fc12a4013fb9fd84680f960107489429a7e912f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 04:58:28 +0000 Subject: [PATCH 02/22] chore(deps): bump supabase/postgres from 17.4.1.019 to 17.4.1.020 in /pkg/config/templates (#3484) chore(deps): bump supabase/postgres in /pkg/config/templates Bumps supabase/postgres from 17.4.1.019 to 17.4.1.020. --- updated-dependencies: - dependency-name: supabase/postgres dependency-version: 17.4.1.020 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index 15e2e96a2c..252e808c95 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -1,5 +1,5 @@ # Exposed for updates by .github/dependabot.yml -FROM supabase/postgres:17.4.1.019 AS pg +FROM supabase/postgres:17.4.1.020 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit From 58ad8686b68468c4d8d03e9abec6965c27fdb317 Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Fri, 25 Apr 2025 23:16:51 +0800 Subject: [PATCH 03/22] fix: reuse flags for native bundler (#3485) --- internal/functions/deploy/bundle.go | 3 ++- pkg/function/bundle.go | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/functions/deploy/bundle.go b/internal/functions/deploy/bundle.go index 974838374e..1bdc23fb97 100644 --- a/internal/functions/deploy/bundle.go +++ b/internal/functions/deploy/bundle.go @@ -50,7 +50,7 @@ func (b *dockerBundler) Bundle(ctx context.Context, slug, entrypoint, importMap } hostOutputPath := filepath.Join(hostOutputDir, "output.eszip") // Create exec command - cmd := []string{"bundle", "--entrypoint", utils.ToDockerPath(entrypoint), "--output", utils.ToDockerPath(hostOutputPath), "--decorator", "tc39"} + cmd := []string{"bundle", "--entrypoint", utils.ToDockerPath(entrypoint), "--output", utils.ToDockerPath(hostOutputPath)} if len(importMap) > 0 { cmd = append(cmd, "--import-map", utils.ToDockerPath(importMap)) } @@ -60,6 +60,7 @@ func (b *dockerBundler) Bundle(ctx context.Context, slug, entrypoint, importMap if viper.GetBool("DEBUG") { cmd = append(cmd, "--verbose") } + cmd = append(cmd, function.BundleFlags...) env := []string{} if custom_registry := os.Getenv("NPM_CONFIG_REGISTRY"); custom_registry != "" { diff --git a/pkg/function/bundle.go b/pkg/function/bundle.go index 486bd4be66..27e4d7660e 100644 --- a/pkg/function/bundle.go +++ b/pkg/function/bundle.go @@ -29,8 +29,13 @@ func NewNativeBundler(tempDir string, fsys fs.FS) EszipBundler { } } -// Use a package private variable to allow testing without gosec complaining about G204 -var edgeRuntimeBin = "edge-runtime" +var ( + // Use a package private variable to allow testing without gosec complaining about G204 + edgeRuntimeBin = "edge-runtime" + BundleFlags = []string{ + "--decorator", "tc39", + } +) func (b *nativeBundler) Bundle(ctx context.Context, slug, entrypoint, importMap string, staticFiles []string, output io.Writer) (api.FunctionDeployMetadata, error) { meta := NewMetadata(slug, entrypoint, importMap, staticFiles) @@ -43,6 +48,7 @@ func (b *nativeBundler) Bundle(ctx context.Context, slug, entrypoint, importMap for _, staticFile := range staticFiles { args = append(args, "--static", staticFile) } + args = append(args, BundleFlags...) cmd := exec.CommandContext(ctx, edgeRuntimeBin, args...) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout From d745c729d4f8b77d4956b21cedf47c02813de892 Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Sat, 26 Apr 2025 01:53:05 +0800 Subject: [PATCH 04/22] fix: skip storage version when linking (#3488) --- internal/services/services.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/internal/services/services.go b/internal/services/services.go index 588dedfbc8..0d31891acc 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -80,7 +80,7 @@ func listRemoteImages(ctx context.Context, projectRef string) map[string]string return linked } api := tenant.NewTenantAPI(ctx, projectRef, keys.Anon) - wg.Add(3) + wg.Add(2) go func() { defer wg.Done() if version, err := api.GetGotrueVersion(ctx); err == nil { @@ -93,12 +93,6 @@ func listRemoteImages(ctx context.Context, projectRef string) map[string]string linked[utils.Config.Api.Image] = version } }() - go func() { - defer wg.Done() - if version, err := api.GetStorageVersion(ctx); err == nil { - linked[utils.Config.Storage.Image] = version - } - }() wg.Wait() return linked } From 27ca207b3924d25c8c83c235c7d89e96565d49dd Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Sun, 27 Apr 2025 20:12:58 +0800 Subject: [PATCH 05/22] fix: retry on error listing functions (#3490) * fix: retry on error listing functions * chore: update unit tests --- pkg/function/batch.go | 26 ++++++++++++++++++-------- pkg/function/batch_test.go | 18 +++++++----------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/pkg/function/batch.go b/pkg/function/batch.go index 102d74c5b1..32dbd75875 100644 --- a/pkg/function/batch.go +++ b/pkg/function/batch.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "net/http" "os" "strings" "time" @@ -22,19 +23,28 @@ const ( ) func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, filter ...func(string) bool) error { - var result []api.FunctionResponse - if resp, err := s.client.V1ListAllFunctionsWithResponse(ctx, s.project); err != nil { - return errors.Errorf("failed to list functions: %w", err) - } else if resp.JSON200 == nil { - return errors.Errorf("unexpected list functions status %d: %s", resp.StatusCode(), string(resp.Body)) - } else { - result = *resp.JSON200 + policy := backoff.WithContext(backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries), ctx) + result, err := backoff.RetryWithData(func() ([]api.FunctionResponse, error) { + resp, err := s.client.V1ListAllFunctionsWithResponse(ctx, s.project) + if err != nil { + return nil, errors.Errorf("failed to list functions: %w", err) + } else if resp.JSON200 == nil { + err = errors.Errorf("unexpected list functions status %d: %s", resp.StatusCode(), string(resp.Body)) + if resp.StatusCode() < http.StatusInternalServerError { + err = &backoff.PermanentError{Err: err} + } + return nil, err + } + return *resp.JSON200, nil + }, policy) + if err != nil { + return err } + policy.Reset() exists := make(map[string]struct{}, len(result)) for _, f := range result { exists[f.Slug] = struct{}{} } - policy := backoff.WithContext(backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries), ctx) var toUpdate []api.BulkUpdateFunctionBody OUTER: for slug, function := range functionConfig { diff --git a/pkg/function/batch_test.go b/pkg/function/batch_test.go index f92bbf3358..3830f41001 100644 --- a/pkg/function/batch_test.go +++ b/pkg/function/batch_test.go @@ -10,6 +10,7 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/supabase/cli/internal/testing/apitest" "github.com/supabase/cli/pkg/api" "github.com/supabase/cli/pkg/config" ) @@ -41,28 +42,23 @@ func TestUpsertFunctions(t *testing.T) { era.eszip = &MockBundler{} }) - t.Run("throws error on network failure", func(t *testing.T) { + t.Run("retries on network failure", func(t *testing.T) { // Setup mock api defer gock.OffAll() gock.New(mockApiHost). Get("/v1/projects/" + mockProject + "/functions"). ReplyError(errors.New("network error")) - // Run test - err := client.UpsertFunctions(context.Background(), nil) - // Check error - assert.ErrorContains(t, err, "network error") - }) - - t.Run("throws error on service unavailable", func(t *testing.T) { - // Setup mock api - defer gock.OffAll() gock.New(mockApiHost). Get("/v1/projects/" + mockProject + "/functions"). Reply(http.StatusServiceUnavailable) + gock.New(mockApiHost). + Get("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusBadRequest) // Run test err := client.UpsertFunctions(context.Background(), nil) // Check error - assert.ErrorContains(t, err, "unexpected list functions status 503:") + assert.ErrorContains(t, err, "unexpected list functions status 400:") + assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("retries on create failure", func(t *testing.T) { From bd05f59c8b77c82d2ffd55d350e7b364e82dff2d Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Sun, 27 Apr 2025 20:54:05 +0800 Subject: [PATCH 06/22] fix: retry bulk update endpoint on error (#3492) --- pkg/function/batch.go | 13 +++++--- pkg/function/batch_test.go | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/pkg/function/batch.go b/pkg/function/batch.go index 32dbd75875..8cb902bdb4 100644 --- a/pkg/function/batch.go +++ b/pkg/function/batch.go @@ -84,10 +84,15 @@ OUTER: policy.Reset() } if len(toUpdate) > 1 { - if resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, toUpdate); err != nil { - return errors.Errorf("failed to bulk update: %w", err) - } else if resp.JSON200 == nil { - return errors.Errorf("unexpected bulk update status %d: %s", resp.StatusCode(), string(resp.Body)) + if err := backoff.Retry(func() error { + if resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, toUpdate); err != nil { + return errors.Errorf("failed to bulk update: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected bulk update status %d: %s", resp.StatusCode(), string(resp.Body)) + } + return nil + }, policy); err != nil { + return err } } return nil diff --git a/pkg/function/batch_test.go b/pkg/function/batch_test.go index 3830f41001..81e151e8ab 100644 --- a/pkg/function/batch_test.go +++ b/pkg/function/batch_test.go @@ -42,6 +42,65 @@ func TestUpsertFunctions(t *testing.T) { era.eszip = &MockBundler{} }) + t.Run("deploys with bulk update", func(t *testing.T) { + // Setup mock api + defer gock.OffAll() + gock.New(mockApiHost). + Get("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusOK). + JSON([]api.FunctionResponse{{Slug: "test-a"}}) + gock.New(mockApiHost). + Patch("/v1/projects/" + mockProject + "/functions/test-a"). + Reply(http.StatusOK). + JSON(api.FunctionResponse{Slug: "test-a"}) + gock.New(mockApiHost). + Post("/v1/projects/" + mockProject + "/functions/test-b"). + Reply(http.StatusOK). + JSON(api.FunctionResponse{Slug: "test-b"}) + gock.New(mockApiHost). + Put("/v1/projects/" + mockProject + "/functions"). + ReplyError(errors.New("network error")) + gock.New(mockApiHost). + Put("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusServiceUnavailable) + gock.New(mockApiHost). + Put("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusOK). + JSON(api.V1BulkUpdateFunctionsResponse{}) + // Run test + err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ + "test-a": {}, + "test-b": {}, + }) + // Check error + assert.NoError(t, err) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("handles concurrent deploy", func(t *testing.T) { + // Setup mock api + defer gock.OffAll() + gock.New(mockApiHost). + Get("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusOK). + JSON([]api.FunctionResponse{}) + gock.New(mockApiHost). + Post("/v1/projects/" + mockProject + "/functions"). + Reply(http.StatusBadRequest). + BodyString("Duplicated function slug") + gock.New(mockApiHost). + Patch("/v1/projects/" + mockProject + "/functions/test"). + Reply(http.StatusOK). + JSON(api.FunctionResponse{Slug: "test"}) + // Run test + err := client.UpsertFunctions(context.Background(), config.FunctionConfig{ + "test": {}, + }) + // Check error + assert.NoError(t, err) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + t.Run("retries on network failure", func(t *testing.T) { // Setup mock api defer gock.OffAll() @@ -84,6 +143,7 @@ func TestUpsertFunctions(t *testing.T) { }) // Check error assert.NoError(t, err) + assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("retries on update failure", func(t *testing.T) { @@ -109,5 +169,6 @@ func TestUpsertFunctions(t *testing.T) { }) // Check error assert.NoError(t, err) + assert.Empty(t, apitest.ListUnmatchedRequests()) }) } From aea419c1185edfedd5b9717bbca1f4bee224b6f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 04:50:14 +0000 Subject: [PATCH 07/22] chore(deps): bump supabase/studio from 2025.04.21-sha-173cc56 to 2025.04.28-sha-dd03625 in /pkg/config/templates (#3494) chore(deps): bump supabase/studio in /pkg/config/templates Bumps supabase/studio from 2025.04.21-sha-173cc56 to 2025.04.28-sha-dd03625. --- updated-dependencies: - dependency-name: supabase/studio dependency-version: 2025.04.28-sha-dd03625 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index 252e808c95..f4ba73434b 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -5,7 +5,7 @@ FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit FROM postgrest/postgrest:v12.2.11 AS postgrest FROM supabase/postgres-meta:v0.88.9 AS pgmeta -FROM supabase/studio:2025.04.21-sha-173cc56 AS studio +FROM supabase/studio:2025.04.28-sha-dd03625 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy FROM supabase/edge-runtime:v1.67.4 AS edgeruntime FROM timberio/vector:0.28.1-alpine AS vector From 1b59e5ca4298d1c535e1ea14c8f25e1adf6a36b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 04:55:55 +0000 Subject: [PATCH 08/22] chore(deps): bump supabase/postgres from 17.4.1.020 to 17.4.1.021 in /pkg/config/templates (#3495) chore(deps): bump supabase/postgres in /pkg/config/templates Bumps supabase/postgres from 17.4.1.020 to 17.4.1.021. --- updated-dependencies: - dependency-name: supabase/postgres dependency-version: 17.4.1.021 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index f4ba73434b..db289eac23 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -1,5 +1,5 @@ # Exposed for updates by .github/dependabot.yml -FROM supabase/postgres:17.4.1.020 AS pg +FROM supabase/postgres:17.4.1.021 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit From 5f46c77f4bcc447f2dfc052e4f2bb20658f57c5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 04:55:36 +0000 Subject: [PATCH 09/22] chore(deps): bump supabase/storage-api from v1.22.7 to v1.22.8 in /pkg/config/templates (#3497) chore(deps): bump supabase/storage-api in /pkg/config/templates Bumps supabase/storage-api from v1.22.7 to v1.22.8. --- updated-dependencies: - dependency-name: supabase/storage-api dependency-version: v1.22.8 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index db289eac23..2fe49cec3f 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -12,7 +12,7 @@ FROM timberio/vector:0.28.1-alpine AS vector FROM supabase/supavisor:2.5.1 AS supavisor FROM supabase/gotrue:v2.171.0 AS gotrue FROM supabase/realtime:v2.34.47 AS realtime -FROM supabase/storage-api:v1.22.7 AS storage +FROM supabase/storage-api:v1.22.8 AS storage FROM supabase/logflare:1.12.0 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ From 730c0c5b4ddf09397d2fdd1c31d2614148ce9d2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 05:00:25 +0000 Subject: [PATCH 10/22] chore(deps): bump supabase/postgres from 17.4.1.021 to 17.4.1.022 in /pkg/config/templates (#3498) chore(deps): bump supabase/postgres in /pkg/config/templates Bumps supabase/postgres from 17.4.1.021 to 17.4.1.022. --- updated-dependencies: - dependency-name: supabase/postgres dependency-version: 17.4.1.022 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index 2fe49cec3f..01f1054e8b 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -1,5 +1,5 @@ # Exposed for updates by .github/dependabot.yml -FROM supabase/postgres:17.4.1.021 AS pg +FROM supabase/postgres:17.4.1.022 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit From dc8dddbf2296343ee6aecd0964cb7c015afb7db9 Mon Sep 17 00:00:00 2001 From: Lenny Date: Tue, 29 Apr 2025 12:50:07 -0400 Subject: [PATCH 11/22] fix: use correct env var name to pass auth jwks to storage container (#3502) --- internal/start/start.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/start/start.go b/internal/start/start.go index b13f88965d..609c395184 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -833,7 +833,7 @@ EOF "ANON_KEY=" + utils.Config.Auth.AnonKey.Value, "SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value, "AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, - fmt.Sprintf("AUTH_JWT_JWKS=%s", jwks), + fmt.Sprintf("JWT_JWKS=%s", jwks), fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database), fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit), "STORAGE_BACKEND=file", From cb77a674e84300605d65c17c01692075af5dae01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 04:40:20 +0000 Subject: [PATCH 12/22] chore(deps): bump supabase/storage-api from v1.22.8 to v1.22.9 in /pkg/config/templates (#3503) chore(deps): bump supabase/storage-api in /pkg/config/templates Bumps supabase/storage-api from v1.22.8 to v1.22.9. --- updated-dependencies: - dependency-name: supabase/storage-api dependency-version: v1.22.9 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index 01f1054e8b..5cbd6f0d45 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -12,7 +12,7 @@ FROM timberio/vector:0.28.1-alpine AS vector FROM supabase/supavisor:2.5.1 AS supavisor FROM supabase/gotrue:v2.171.0 AS gotrue FROM supabase/realtime:v2.34.47 AS realtime -FROM supabase/storage-api:v1.22.8 AS storage +FROM supabase/storage-api:v1.22.9 AS storage FROM supabase/logflare:1.12.0 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ From fdf87062b7085f99951496d590cfcf07b8bce967 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 04:37:30 +0000 Subject: [PATCH 13/22] chore(deps): bump supabase/storage-api from v1.22.9 to v1.22.11 in /pkg/config/templates (#3506) chore(deps): bump supabase/storage-api in /pkg/config/templates Bumps supabase/storage-api from v1.22.9 to v1.22.11. --- updated-dependencies: - dependency-name: supabase/storage-api dependency-version: v1.22.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index 5cbd6f0d45..cd3600a99f 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -12,7 +12,7 @@ FROM timberio/vector:0.28.1-alpine AS vector FROM supabase/supavisor:2.5.1 AS supavisor FROM supabase/gotrue:v2.171.0 AS gotrue FROM supabase/realtime:v2.34.47 AS realtime -FROM supabase/storage-api:v1.22.9 AS storage +FROM supabase/storage-api:v1.22.11 AS storage FROM supabase/logflare:1.12.0 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ From 1e43131950d4d5a6d69432dc1bd942fa61352a49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 04:34:20 +0000 Subject: [PATCH 14/22] chore(deps): bump supabase/postgres from 17.4.1.022 to 17.4.1.026 in /pkg/config/templates (#3508) chore(deps): bump supabase/postgres in /pkg/config/templates Bumps supabase/postgres from 17.4.1.022 to 17.4.1.026. --- updated-dependencies: - dependency-name: supabase/postgres dependency-version: 17.4.1.026 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index cd3600a99f..fb6a7ea869 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -1,5 +1,5 @@ # Exposed for updates by .github/dependabot.yml -FROM supabase/postgres:17.4.1.022 AS pg +FROM supabase/postgres:17.4.1.026 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit From 0bfa100c5bc3b2e5f09a69ccdd15c056c2650669 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 04:39:09 +0000 Subject: [PATCH 15/22] chore(deps): bump postgrest/postgrest from v12.2.11 to v12.2.12 in /pkg/config/templates (#3509) chore(deps): bump postgrest/postgrest in /pkg/config/templates Bumps postgrest/postgrest from v12.2.11 to v12.2.12. --- updated-dependencies: - dependency-name: postgrest/postgrest dependency-version: v12.2.12 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index fb6a7ea869..3ea4f8294d 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -3,7 +3,7 @@ FROM supabase/postgres:17.4.1.026 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit -FROM postgrest/postgrest:v12.2.11 AS postgrest +FROM postgrest/postgrest:v12.2.12 AS postgrest FROM supabase/postgres-meta:v0.88.9 AS pgmeta FROM supabase/studio:2025.04.28-sha-dd03625 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy From a2da52792da44583b143dd4a04bbdb683234f4f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 12:54:07 +0800 Subject: [PATCH 16/22] chore(deps): bump golangci/golangci-lint-action from 7 to 8 (#3515) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7 to 8. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v7...v8) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7cf2b30bc..21ea801823 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: # Linter requires no cache cache: false - - uses: golangci/golangci-lint-action@v7 + - uses: golangci/golangci-lint-action@v8 with: args: --timeout 3m --verbose From 0b283072af45a34ebc6fdff1d489c77562177903 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 05:34:32 +0000 Subject: [PATCH 17/22] chore(deps): bump supabase/studio from 2025.04.28-sha-dd03625 to 2025.05.05-sha-3c3fe9b in /pkg/config/templates (#3518) chore(deps): bump supabase/studio in /pkg/config/templates Bumps supabase/studio from 2025.04.28-sha-dd03625 to 2025.05.05-sha-3c3fe9b. --- updated-dependencies: - dependency-name: supabase/studio dependency-version: 2025.05.05-sha-3c3fe9b dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index 3ea4f8294d..b343f6dcb2 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -5,7 +5,7 @@ FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit FROM postgrest/postgrest:v12.2.12 AS postgrest FROM supabase/postgres-meta:v0.88.9 AS pgmeta -FROM supabase/studio:2025.04.28-sha-dd03625 AS studio +FROM supabase/studio:2025.05.05-sha-3c3fe9b AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy FROM supabase/edge-runtime:v1.67.4 AS edgeruntime FROM timberio/vector:0.28.1-alpine AS vector From 0759b2f614f131f0cf1b0bd9c6c8d7a80536ffad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 05:39:39 +0000 Subject: [PATCH 18/22] chore(deps): bump supabase/postgres from 17.4.1.026 to 17.4.1.027 in /pkg/config/templates (#3519) chore(deps): bump supabase/postgres in /pkg/config/templates Bumps supabase/postgres from 17.4.1.026 to 17.4.1.027. --- updated-dependencies: - dependency-name: supabase/postgres dependency-version: 17.4.1.027 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index b343f6dcb2..21196ea0fd 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -1,5 +1,5 @@ # Exposed for updates by .github/dependabot.yml -FROM supabase/postgres:17.4.1.026 AS pg +FROM supabase/postgres:17.4.1.027 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit From 776e47a1f9a75898833e6b9da5145e6ca2892e4c Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Mon, 5 May 2025 14:13:38 +0800 Subject: [PATCH 19/22] chore: remove keyring mock (#3517) * chore: remove keyring mock * chore: include files without tests in coverage --- .github/workflows/ci.yml | 5 +- internal/logout/logout_test.go | 2 +- internal/utils/credentials/store_mock.go | 68 ------------------------ 3 files changed, 4 insertions(+), 71 deletions(-) delete mode 100644 internal/utils/credentials/store_mock.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21ea801823..73a9b684dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,8 +21,9 @@ jobs: # Required by: internal/utils/credentials/keyring_test.go - uses: t1m0thyj/unlock-keyring@v1 - run: | - go run gotest.tools/gotestsum -- -race -v -count=1 -coverprofile=coverage.out \ - `go list ./... | grep -Ev 'cmd|docs|examples|pkg/api|tools'` + pkgs=$(go list ./pkg/... | grep -Ev 'pkg/api' | paste -sd ',' -) + go run gotest.tools/gotestsum -- -race -v -count=1 ./... \ + -coverpkg="./cmd/...,./internal/...,${pkgs}" -coverprofile=coverage.out - uses: coverallsapp/github-action@v2 with: diff --git a/internal/logout/logout_test.go b/internal/logout/logout_test.go index 42f9f8ead1..943f25bed2 100644 --- a/internal/logout/logout_test.go +++ b/internal/logout/logout_test.go @@ -34,7 +34,7 @@ func TestLogoutCommand(t *testing.T) { }) t.Run("removes all Supabase CLI credentials", func(t *testing.T) { - t.Cleanup(credentials.MockInit()) + keyring.MockInit() require.NoError(t, credentials.StoreProvider.Set(utils.AccessTokenKey, token)) require.NoError(t, credentials.StoreProvider.Set("project1", "password1")) require.NoError(t, credentials.StoreProvider.Set("project2", "password2")) diff --git a/internal/utils/credentials/store_mock.go b/internal/utils/credentials/store_mock.go deleted file mode 100644 index 32715fc375..0000000000 --- a/internal/utils/credentials/store_mock.go +++ /dev/null @@ -1,68 +0,0 @@ -package credentials - -import ( - "github.com/zalando/go-keyring" -) - -type mockProvider struct { - mockStore map[string]map[string]string - mockError error -} - -// Get retrieves the password for a project from the mock store. -func (m *mockProvider) Get(project string) (string, error) { - if m.mockError != nil { - return "", m.mockError - } - if pass, ok := m.mockStore[namespace][project]; ok { - return pass, nil - } - return "", keyring.ErrNotFound -} - -// Set stores the password for a project in the mock store. -func (m *mockProvider) Set(project, password string) error { - if m.mockError != nil { - return m.mockError - } - if m.mockStore == nil { - m.mockStore = make(map[string]map[string]string) - } - if m.mockStore[namespace] == nil { - m.mockStore[namespace] = make(map[string]string) - } - m.mockStore[namespace][project] = password - return nil -} - -// Delete removes the password for a project from the mock store. -func (m *mockProvider) Delete(project string) error { - if m.mockError != nil { - return m.mockError - } - if _, ok := m.mockStore[namespace][project]; ok { - delete(m.mockStore[namespace], project) - return nil - } - return keyring.ErrNotFound -} - -// DeleteAll removes all passwords from the mock store. -func (m *mockProvider) DeleteAll() error { - if m.mockError != nil { - return m.mockError - } - delete(m.mockStore, namespace) - return nil -} - -func MockInit() func() { - oldStore := StoreProvider - teardown := func() { - StoreProvider = oldStore - } - StoreProvider = &mockProvider{ - mockStore: map[string]map[string]string{}, - } - return teardown -} From 5d1fa9c5889fba2d85ef7c3a6a49a6bdffbb617a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 14:27:19 +0800 Subject: [PATCH 20/22] chore(deps): bump github.com/golangci/golangci-lint/v2 from 2.1.5 to 2.1.6 (#3516) chore(deps): bump github.com/golangci/golangci-lint/v2 Bumps [github.com/golangci/golangci-lint/v2](https://github.com/golangci/golangci-lint) from 2.1.5 to 2.1.6. - [Release notes](https://github.com/golangci/golangci-lint/releases) - [Changelog](https://github.com/golangci/golangci-lint/blob/main/CHANGELOG.md) - [Commits](https://github.com/golangci/golangci-lint/compare/v2.1.5...v2.1.6) --- updated-dependencies: - dependency-name: github.com/golangci/golangci-lint/v2 dependency-version: 2.1.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 96c41c4478..193d1f07b7 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.2.1 github.com/go-xmlfmt/xmlfmt v1.1.3 github.com/golang-jwt/jwt/v5 v5.2.2 - github.com/golangci/golangci-lint/v2 v2.1.5 + github.com/golangci/golangci-lint/v2 v2.1.6 github.com/google/go-github/v62 v62.0.0 github.com/google/go-querystring v1.1.0 github.com/google/uuid v1.6.0 @@ -74,7 +74,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect - github.com/alecthomas/chroma/v2 v2.16.0 // indirect + github.com/alecthomas/chroma/v2 v2.17.2 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect github.com/alexkohler/nakedret/v2 v2.0.6 // indirect github.com/alexkohler/prealloc v1.0.0 // indirect @@ -280,7 +280,7 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tdakkota/asciicheck v0.4.1 // indirect - github.com/tetafro/godot v1.5.0 // indirect + github.com/tetafro/godot v1.5.1 // indirect github.com/theupdateframework/notary v0.7.0 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect @@ -303,7 +303,7 @@ require ( github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark-emoji v1.0.5 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect - go-simpler.org/musttag v0.13.0 // indirect + go-simpler.org/musttag v0.13.1 // indirect go-simpler.org/sloglint v0.11.0 // indirect go.augendre.info/fatcontext v0.8.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/go.sum b/go.sum index b0a84118bc..62d3f9c73c 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMz github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA= -github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= +github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI= +github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= @@ -414,8 +414,8 @@ github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUP github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= -github.com/golangci/golangci-lint/v2 v2.1.5 h1:zDcxV8s7kgQW3cpQiVA633CZJnKN/0iEXibPDWO8sZo= -github.com/golangci/golangci-lint/v2 v2.1.5/go.mod h1:RGcjZLyl9fSVLqxdKMrknPlspC3TYETLoKXyRG06RDo= +github.com/golangci/golangci-lint/v2 v2.1.6 h1:LXqShFfAGM5BDzEOWD2SL1IzJAgUOqES/HRBsfKjI+w= +github.com/golangci/golangci-lint/v2 v2.1.6/go.mod h1:EPj+fgv4TeeBq3TcqaKZb3vkiV5dP4hHHKhXhEhzci8= github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 h1:AkK+w9FZBXlU/xUmBtSJN1+tAI4FIvy5WtnUnY8e4p8= github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95/go.mod h1:k9mmcyWKSTMcPPvQUCfRWWQ9VHJ1U9Dc0R7kaXAgtnQ= github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= @@ -949,8 +949,8 @@ github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw= -github.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/tetafro/godot v1.5.1 h1:PZnjCol4+FqaEzvZg5+O8IY2P3hfY9JzRBNPv1pEDS4= +github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc= @@ -1016,8 +1016,8 @@ gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= -go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= -go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= +go-simpler.org/musttag v0.13.1 h1:lw2sJyu7S1X8lc8zWUAdH42y+afdcCnHhWpnkWvd6vU= +go-simpler.org/musttag v0.13.1/go.mod h1:8r450ehpMLQgvpb6sg+hV5Ur47eH6olp/3yEanfG97k= go-simpler.org/sloglint v0.11.0 h1:JlR1X4jkbeaffiyjLtymeqmGDKBDO1ikC6rjiuFAOco= go-simpler.org/sloglint v0.11.0/go.mod h1:CFDO8R1i77dlciGfPEPvYke2ZMx4eyGiEIWkyeW2Pvw= go.augendre.info/fatcontext v0.8.0 h1:2dfk6CQbDGeu1YocF59Za5Pia7ULeAM6friJ3LP7lmk= From 679f87301fa95f892d71accf62d3fa1cca8fe898 Mon Sep 17 00:00:00 2001 From: egor <58992960+egor-romanov@users.noreply.github.com> Date: Mon, 5 May 2025 12:19:34 +0100 Subject: [PATCH 21/22] chore: add some utils tests (#3514) * chore: add parse db url tests * chore: add cloudflare basic tests * chore: add cred store tests * chore: add some tenant keys tests * chore: more utils tests * chore: add utils misc tests * chore: rm some files from coverage * chore: lint fixes * Update .github/workflows/ci.yml * chore: update unit tests * fix race in test * chore: rm wsl tests --------- Co-authored-by: Han Qiao Co-authored-by: Qiao Han --- internal/utils/cloudflare/dns_test.go | 128 +++++++++++++++++ internal/utils/config_test.go | 168 ++++++++++++++++++++++ internal/utils/container_output_test.go | 171 +++++++++++++++++++++++ internal/utils/credentials/store_test.go | 99 +++++++++++++ internal/utils/deno_test.go | 111 +++++++++++++++ internal/utils/flags/db_url_test.go | 118 ++++++++++++++++ internal/utils/misc_test.go | 111 +++++++++++++++ internal/utils/output_test.go | 141 +++++++++++++++++++ internal/utils/slice_test.go | 54 ++++++- internal/utils/tenant/client_test.go | 151 ++++++++++++++++++++ internal/utils/tenant/database_test.go | 88 ++++++++++++ 11 files changed, 1338 insertions(+), 2 deletions(-) create mode 100644 internal/utils/cloudflare/dns_test.go create mode 100644 internal/utils/config_test.go create mode 100644 internal/utils/container_output_test.go create mode 100644 internal/utils/credentials/store_test.go create mode 100644 internal/utils/flags/db_url_test.go create mode 100644 internal/utils/output_test.go create mode 100644 internal/utils/tenant/client_test.go create mode 100644 internal/utils/tenant/database_test.go diff --git a/internal/utils/cloudflare/dns_test.go b/internal/utils/cloudflare/dns_test.go new file mode 100644 index 0000000000..ee4d00c299 --- /dev/null +++ b/internal/utils/cloudflare/dns_test.go @@ -0,0 +1,128 @@ +package cloudflare + +import ( + "context" + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/supabase/cli/internal/testing/apitest" +) + +func TestDNSQuery(t *testing.T) { + t.Run("successfully queries A records", func(t *testing.T) { + api := NewCloudflareAPI() + defer gock.OffAll() + gock.New("https://1.1.1.1"). + Get("/dns-query"). + MatchParam("name", "example.com"). + MatchHeader("accept", "application/dns-json"). + Reply(http.StatusOK). + JSON(DNSResponse{ + Answer: []DNSAnswer{ + { + Name: "example.com", + Type: TypeA, + Ttl: 300, + Data: "93.184.216.34", + }, + }, + }) + + resp, err := api.DNSQuery(context.Background(), DNSParams{ + Name: "example.com", + }) + + assert.NoError(t, err) + assert.Len(t, resp.Answer, 1) + assert.Equal(t, "93.184.216.34", resp.Answer[0].Data) + assert.Equal(t, TypeA, resp.Answer[0].Type) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("successfully queries specific DNS type", func(t *testing.T) { + api := NewCloudflareAPI() + dnsType := TypeCNAME + defer gock.OffAll() + gock.New("https://1.1.1.1"). + Get("/dns-query"). + MatchParam("name", "www.example.com"). + MatchParam("type", "5"). // TypeCNAME = 5 + MatchHeader("accept", "application/dns-json"). + Reply(http.StatusOK). + JSON(DNSResponse{ + Answer: []DNSAnswer{ + { + Name: "www.example.com", + Type: TypeCNAME, + Ttl: 3600, + Data: "example.com", + }, + }, + }) + + resp, err := api.DNSQuery(context.Background(), DNSParams{ + Name: "www.example.com", + Type: &dnsType, + }) + + assert.NoError(t, err) + assert.Len(t, resp.Answer, 1) + assert.Equal(t, "example.com", resp.Answer[0].Data) + assert.Equal(t, TypeCNAME, resp.Answer[0].Type) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("handles network error", func(t *testing.T) { + api := NewCloudflareAPI() + defer gock.OffAll() + gock.New("https://1.1.1.1"). + Get("/dns-query"). + MatchParam("name", "example.com"). + ReplyError(gock.ErrCannotMatch) + + resp, err := api.DNSQuery(context.Background(), DNSParams{ + Name: "example.com", + }) + + assert.Error(t, err) + assert.Empty(t, resp.Answer) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("handles service unavailable", func(t *testing.T) { + api := NewCloudflareAPI() + defer gock.OffAll() + gock.New("https://1.1.1.1"). + Get("/dns-query"). + MatchParam("name", "example.com"). + Reply(http.StatusServiceUnavailable) + + resp, err := api.DNSQuery(context.Background(), DNSParams{ + Name: "example.com", + }) + + assert.ErrorContains(t, err, "status 503") + assert.Empty(t, resp.Answer) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("handles malformed response", func(t *testing.T) { + api := NewCloudflareAPI() + defer gock.OffAll() + gock.New("https://1.1.1.1"). + Get("/dns-query"). + MatchParam("name", "example.com"). + Reply(http.StatusOK). + JSON("invalid json") + + resp, err := api.DNSQuery(context.Background(), DNSParams{ + Name: "example.com", + }) + + assert.ErrorContains(t, err, "failed to parse") + assert.Empty(t, resp.Answer) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) +} diff --git a/internal/utils/config_test.go b/internal/utils/config_test.go new file mode 100644 index 0000000000..9ac835170f --- /dev/null +++ b/internal/utils/config_test.go @@ -0,0 +1,168 @@ +package utils + +import ( + "os" + "testing" + + "github.com/spf13/afero" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetId(t *testing.T) { + t.Run("generates container id", func(t *testing.T) { + Config.ProjectId = "test-project" + name := "test-service" + + id := GetId(name) + + assert.Equal(t, "supabase_test-service_test-project", id) + }) +} + +func TestUpdateDockerIds(t *testing.T) { + t.Run("updates all container ids", func(t *testing.T) { + Config.ProjectId = "test-project" + viper.Set("network-id", "custom-network") + defer viper.Reset() + + UpdateDockerIds() + + assert.Equal(t, "custom-network", NetId) + assert.Equal(t, "supabase_db_test-project", DbId) + assert.Equal(t, "supabase_kong_test-project", KongId) + assert.Equal(t, "supabase_auth_test-project", GotrueId) + assert.Equal(t, "supabase_inbucket_test-project", InbucketId) + assert.Equal(t, "supabase_realtime_test-project", RealtimeId) + assert.Equal(t, "supabase_rest_test-project", RestId) + assert.Equal(t, "supabase_storage_test-project", StorageId) + assert.Equal(t, "supabase_imgproxy_test-project", ImgProxyId) + assert.Equal(t, "supabase_differ_test-project", DifferId) + assert.Equal(t, "supabase_pg_meta_test-project", PgmetaId) + assert.Equal(t, "supabase_studio_test-project", StudioId) + assert.Equal(t, "supabase_edge_runtime_test-project", EdgeRuntimeId) + assert.Equal(t, "supabase_analytics_test-project", LogflareId) + assert.Equal(t, "supabase_vector_test-project", VectorId) + assert.Equal(t, "supabase_pooler_test-project", PoolerId) + }) + + t.Run("generates network id if not set", func(t *testing.T) { + Config.ProjectId = "test-project" + viper.Reset() + + UpdateDockerIds() + + assert.Equal(t, "supabase_network_test-project", NetId) + }) +} + +func TestInitConfig(t *testing.T) { + t.Run("creates new config file", func(t *testing.T) { + fsys := afero.NewMemMapFs() + params := InitParams{ + ProjectId: "test-project", + } + + err := InitConfig(params, fsys) + + assert.NoError(t, err) + exists, err := afero.Exists(fsys, ConfigPath) + assert.NoError(t, err) + assert.True(t, exists) + }) + + t.Run("creates config with orioledb", func(t *testing.T) { + fsys := afero.NewMemMapFs() + params := InitParams{ + ProjectId: "test-project", + UseOrioleDB: true, + } + + err := InitConfig(params, fsys) + + assert.NoError(t, err) + content, err := afero.ReadFile(fsys, ConfigPath) + assert.NoError(t, err) + assert.Contains(t, string(content), "15.1.0.150") + }) + + t.Run("fails if config exists and no overwrite", func(t *testing.T) { + fsys := afero.NewMemMapFs() + err := afero.WriteFile(fsys, ConfigPath, []byte("existing"), 0644) + require.NoError(t, err) + params := InitParams{ + ProjectId: "test-project", + } + + err = InitConfig(params, fsys) + + assert.ErrorIs(t, err, os.ErrExist) + }) + + t.Run("overwrites existing config when specified", func(t *testing.T) { + fsys := afero.NewMemMapFs() + err := afero.WriteFile(fsys, ConfigPath, []byte("existing"), 0644) + require.NoError(t, err) + params := InitParams{ + ProjectId: "test-project", + Overwrite: true, + } + + err = InitConfig(params, fsys) + + assert.NoError(t, err) + content, err := afero.ReadFile(fsys, ConfigPath) + assert.NoError(t, err) + assert.NotEqual(t, "existing", string(content)) + }) +} + +func TestGetApiUrl(t *testing.T) { + t.Run("returns external url when configured", func(t *testing.T) { + Config.Api.ExternalUrl = "https://api.example.com" + + url := GetApiUrl("/test") + + assert.Equal(t, "https://api.example.com/test", url) + }) + + t.Run("builds url from hostname and port", func(t *testing.T) { + Config.Hostname = "localhost" + Config.Api.Port = 8000 + Config.Api.ExternalUrl = "" + + url := GetApiUrl("/test") + + assert.Equal(t, "http://localhost:8000/test", url) + }) +} + +func TestRootFS(t *testing.T) { + t.Run("opens file from root fs", func(t *testing.T) { + fsys := afero.NewMemMapFs() + content := []byte("test content") + err := afero.WriteFile(fsys, "/test.txt", content, 0644) + require.NoError(t, err) + + rootfs := NewRootFS(fsys) + f, err := rootfs.Open("/test.txt") + assert.NoError(t, err) + defer f.Close() + + buf := make([]byte, len(content)) + n, err := f.Read(buf) + assert.NoError(t, err) + assert.Equal(t, len(content), n) + assert.Equal(t, content, buf) + }) + + t.Run("returns error for non-existent file", func(t *testing.T) { + fsys := afero.NewMemMapFs() + rootfs := NewRootFS(fsys) + + _, err := rootfs.Open("/non-existent.txt") + + assert.Error(t, err) + }) +} diff --git a/internal/utils/container_output_test.go b/internal/utils/container_output_test.go new file mode 100644 index 0000000000..cf3e529bb3 --- /dev/null +++ b/internal/utils/container_output_test.go @@ -0,0 +1,171 @@ +package utils + +import ( + "bytes" + "encoding/json" + "io" + "sync" + "testing" + + tea "github.com/charmbracelet/bubbletea" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/stdcopy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProcessDiffOutput(t *testing.T) { + t.Run("processes valid diff entries", func(t *testing.T) { + input := []DiffEntry{ + { + Type: "table", + Status: "Different", + DiffDdl: "ALTER TABLE test;", + GroupName: "public", + }, + { + Type: "extension", + Status: "Different", + DiffDdl: "CREATE EXTENSION test;", + GroupName: "public", + }, + } + inputBytes, err := json.Marshal(input) + require.NoError(t, err) + + output, err := ProcessDiffOutput(inputBytes) + + assert.NoError(t, err) + assert.Contains(t, string(output), "ALTER TABLE test;") + assert.Contains(t, string(output), "CREATE EXTENSION test;") + }) + + t.Run("filters out internal schemas", func(t *testing.T) { + input := []DiffEntry{ + { + Type: "table", + Status: "Different", + DiffDdl: "ALTER TABLE test;", + GroupName: "auth", + }, + { + Type: "extension", + Status: "Different", + DiffDdl: "CREATE EXTENSION test;", + GroupName: "auth", + }, + } + inputBytes, err := json.Marshal(input) + require.NoError(t, err) + + output, err := ProcessDiffOutput(inputBytes) + + assert.NoError(t, err) + assert.Nil(t, output) + }) +} + +func TestProcessPsqlOutput(t *testing.T) { + t.Run("processes psql output", func(t *testing.T) { + var buf bytes.Buffer + writer := stdcopy.NewStdWriter(&buf, stdcopy.Stdout) + _, err := writer.Write([]byte("test output\n")) + require.NoError(t, err) + + var lastLine *string + p := NewMockProgram(func(msg tea.Msg) { + if m, ok := msg.(PsqlMsg); ok { + lastLine = m + } + }) + + err = ProcessPsqlOutput(&buf, p) + + assert.NoError(t, err) + assert.Nil(t, lastLine) + }) + + t.Run("handles stderr output", func(t *testing.T) { + var buf bytes.Buffer + writer := stdcopy.NewStdWriter(&buf, stdcopy.Stderr) + _, err := writer.Write([]byte("error message\n")) + require.NoError(t, err) + + p := NewMockProgram(nil) + + err = ProcessPsqlOutput(&buf, p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "error message") + }) +} + +func TestProcessPullOutput(t *testing.T) { + t.Run("processes docker pull messages", func(t *testing.T) { + messages := []jsonmessage.JSONMessage{ + {Status: "Pulling from library/postgres"}, + {ID: "layer1", Status: "Pulling fs layer"}, + {ID: "layer1", Status: "Downloading", Progress: &jsonmessage.JSONProgress{Current: 50, Total: 100}}, + {ID: "layer2", Status: "Pulling fs layer"}, + {ID: "layer2", Status: "Downloading", Progress: &jsonmessage.JSONProgress{Current: 75, Total: 100}}, + } + + // Create a pipe to write messages + r, w := io.Pipe() + enc := json.NewEncoder(w) + go func() { + for _, msg := range messages { + err := enc.Encode(msg) + assert.Nil(t, err) + } + w.Close() + }() + + var status string + var progressFirst *float64 + var progress *float64 + p := NewMockProgram(func(msg tea.Msg) { + switch m := msg.(type) { + case StatusMsg: + status = string(m) + case ProgressMsg: + progress = m + if progressFirst == nil { + progressFirst = m + } + } + }) + + err := ProcessPullOutput(r, p) + + assert.NoError(t, err) + assert.Equal(t, "Pulling from library/postgres...", status) + assert.Equal(t, *progressFirst, 0.5) + assert.Nil(t, progress) + }) +} + +type MockProgram struct { + handler func(tea.Msg) + mu sync.Mutex // Add mutex for thread safety +} + +func NewMockProgram(handler func(tea.Msg)) *MockProgram { + return &MockProgram{ + handler: handler, + } +} + +func (m *MockProgram) Send(msg tea.Msg) { + if m.handler != nil { + m.mu.Lock() + m.handler(msg) + m.mu.Unlock() + } +} + +func (m *MockProgram) Start() error { + return nil +} + +func (m *MockProgram) Quit() {} diff --git a/internal/utils/credentials/store_test.go b/internal/utils/credentials/store_test.go new file mode 100644 index 0000000000..4bb62f6581 --- /dev/null +++ b/internal/utils/credentials/store_test.go @@ -0,0 +1,99 @@ +package credentials + +import ( + "testing" + + "github.com/go-errors/errors" + "github.com/stretchr/testify/assert" + "github.com/zalando/go-keyring" +) + +func TestKeyringStore(t *testing.T) { + t.Run("stores and retrieves password", func(t *testing.T) { + keyring.MockInit() + project := "test-project" + password := "test-password" + + err := StoreProvider.Set(project, password) + assert.NoError(t, err) + + retrieved, err := StoreProvider.Get(project) + assert.NoError(t, err) + assert.Equal(t, password, retrieved) + }) + + t.Run("returns error for non-existent project", func(t *testing.T) { + keyring.MockInit() + project := "non-existent" + + retrieved, err := StoreProvider.Get(project) + assert.ErrorIs(t, err, keyring.ErrNotFound) + assert.Empty(t, retrieved) + }) + + t.Run("deletes specific project password", func(t *testing.T) { + keyring.MockInit() + project := "test-project" + password := "test-password" + + err := StoreProvider.Set(project, password) + assert.NoError(t, err) + err = StoreProvider.Delete(project) + assert.NoError(t, err) + + _, err = StoreProvider.Get(project) + assert.ErrorIs(t, err, keyring.ErrNotFound) + }) + + t.Run("deletes all project passwords", func(t *testing.T) { + keyring.MockInit() + projects := []string{"project1", "project2"} + + for _, project := range projects { + err := StoreProvider.Set(project, "password") + assert.NoError(t, err) + } + + err := StoreProvider.DeleteAll() + assert.NoError(t, err) + + for _, project := range projects { + _, err := StoreProvider.Get(project) + assert.ErrorIs(t, err, keyring.ErrNotFound) + } + }) +} + +func TestKeyringErrors(t *testing.T) { + t.Run("handles Get error", func(t *testing.T) { + mockErr := errors.New("mock error") + keyring.MockInitWithError(mockErr) + + _, err := StoreProvider.Get("test") + assert.ErrorIs(t, err, mockErr) + }) + + t.Run("handles Set error", func(t *testing.T) { + mockErr := errors.New("mock error") + keyring.MockInitWithError(mockErr) + + err := StoreProvider.Set("test", "pass") + assert.ErrorIs(t, err, mockErr) + }) + + t.Run("handles Delete error", func(t *testing.T) { + mockErr := errors.New("mock error") + keyring.MockInitWithError(mockErr) + + err := StoreProvider.Delete("test") + assert.ErrorIs(t, err, mockErr) + }) + + t.Run("handles DeleteAll error", func(t *testing.T) { + mockErr := errors.New("mock error") + keyring.MockInitWithError(mockErr) + + err := StoreProvider.DeleteAll() + assert.ErrorIs(t, err, mockErr) + }) +} diff --git a/internal/utils/deno_test.go b/internal/utils/deno_test.go index a20b3acacd..c5b140a8f7 100644 --- a/internal/utils/deno_test.go +++ b/internal/utils/deno_test.go @@ -1,6 +1,10 @@ package utils import ( + "context" + "os" + "path/filepath" + "runtime" "testing" "github.com/spf13/afero" @@ -34,3 +38,110 @@ import "./child/index.ts"` }) }) } + +func TestGetDenoPath(t *testing.T) { + t.Run("returns override path when set", func(t *testing.T) { + override := "/custom/path/to/deno" + DenoPathOverride = override + defer func() { DenoPathOverride = "" }() + + path, err := GetDenoPath() + + assert.NoError(t, err) + assert.Equal(t, override, path) + }) + + t.Run("returns default path", func(t *testing.T) { + home, err := os.UserHomeDir() + require.NoError(t, err) + expected := filepath.Join(home, ".supabase", "deno") + if runtime.GOOS == "windows" { + expected += ".exe" + } + + path, err := GetDenoPath() + + assert.NoError(t, err) + assert.Equal(t, expected, path) + }) +} + +func TestIsScriptModified(t *testing.T) { + t.Run("detects modified script", func(t *testing.T) { + fsys := afero.NewMemMapFs() + destPath := "/path/to/script.ts" + original := []byte("original content") + modified := []byte("modified content") + require.NoError(t, afero.WriteFile(fsys, destPath, modified, 0644)) + + isModified, err := isScriptModified(fsys, destPath, original) + + assert.NoError(t, err) + assert.True(t, isModified) + }) + + t.Run("detects unmodified script", func(t *testing.T) { + fsys := afero.NewMemMapFs() + destPath := "/path/to/script.ts" + content := []byte("test content") + require.NoError(t, afero.WriteFile(fsys, destPath, content, 0644)) + + isModified, err := isScriptModified(fsys, destPath, content) + + assert.NoError(t, err) + assert.False(t, isModified) + }) + + t.Run("handles non-existent script", func(t *testing.T) { + fsys := afero.NewMemMapFs() + destPath := "/path/to/script.ts" + content := []byte("test content") + + isModified, err := isScriptModified(fsys, destPath, content) + + assert.NoError(t, err) + assert.True(t, isModified) + }) +} + +func TestCopyDenoScripts(t *testing.T) { + t.Run("copies deno scripts", func(t *testing.T) { + fsys := afero.NewMemMapFs() + home, err := os.UserHomeDir() + require.NoError(t, err) + denoDir := filepath.Join(home, ".supabase") + require.NoError(t, fsys.MkdirAll(denoDir, 0755)) + + scripts, err := CopyDenoScripts(context.Background(), fsys) + + assert.NoError(t, err) + assert.NotNil(t, scripts) + extractExists, err := afero.Exists(fsys, scripts.ExtractPath) + assert.NoError(t, err) + assert.True(t, extractExists) + }) + + t.Run("skips copying unmodified scripts", func(t *testing.T) { + fsys := afero.NewMemMapFs() + home, err := os.UserHomeDir() + require.NoError(t, err) + scriptDir := filepath.Join(home, ".supabase", "denos") + require.NoError(t, fsys.MkdirAll(scriptDir, 0755)) + + scripts1, err := CopyDenoScripts(context.Background(), fsys) + require.NoError(t, err) + stat1, err := fsys.Stat(scripts1.ExtractPath) + require.NoError(t, err) + modTime1 := stat1.ModTime() + + // Second copy + scripts2, err := CopyDenoScripts(context.Background(), fsys) + require.NoError(t, err) + stat2, err := fsys.Stat(scripts2.ExtractPath) + require.NoError(t, err) + modTime2 := stat2.ModTime() + + // Verify file wasn't rewritten + assert.Equal(t, modTime1, modTime2) + }) +} diff --git a/internal/utils/flags/db_url_test.go b/internal/utils/flags/db_url_test.go new file mode 100644 index 0000000000..ba9676b5fc --- /dev/null +++ b/internal/utils/flags/db_url_test.go @@ -0,0 +1,118 @@ +package flags + +import ( + "os" + "testing" + + "github.com/spf13/afero" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/supabase/cli/internal/testing/apitest" + "github.com/supabase/cli/internal/utils" +) + +func TestParseDatabaseConfig(t *testing.T) { + t.Run("parses direct connection from db-url flag", func(t *testing.T) { + flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) + flagSet.String("db-url", "postgres://postgres:password@localhost:5432/postgres", "") + err := flagSet.Set("db-url", "postgres://admin:secret@db.example.com:6432/app") + assert.Nil(t, err) + + fsys := afero.NewMemMapFs() + + err = ParseDatabaseConfig(flagSet, fsys) + + assert.NoError(t, err) + assert.Equal(t, "db.example.com", DbConfig.Host) + assert.Equal(t, uint16(6432), DbConfig.Port) + assert.Equal(t, "admin", DbConfig.User) + assert.Equal(t, "secret", DbConfig.Password) + assert.Equal(t, "app", DbConfig.Database) + }) + + t.Run("parses local connection", func(t *testing.T) { + flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) + flagSet.Bool("local", false, "") + err := flagSet.Set("local", "true") + assert.Nil(t, err) + + fsys := afero.NewMemMapFs() + + utils.Config.Hostname = "localhost" + utils.Config.Db.Port = 54322 + utils.Config.Db.Password = "local-password" + + err = ParseDatabaseConfig(flagSet, fsys) + + assert.NoError(t, err) + assert.Equal(t, "localhost", DbConfig.Host) + assert.Equal(t, uint16(54322), DbConfig.Port) + assert.Equal(t, "postgres", DbConfig.User) + assert.Equal(t, "local-password", DbConfig.Password) + assert.Equal(t, "postgres", DbConfig.Database) + }) + + t.Run("parses linked connection", func(t *testing.T) { + flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) + flagSet.Bool("linked", false, "") + err := flagSet.Set("linked", "true") + assert.Nil(t, err) + + fsys := afero.NewMemMapFs() + + project := apitest.RandomProjectRef() + err = afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644) + require.NoError(t, err) + + err = ParseDatabaseConfig(flagSet, fsys) + + assert.NoError(t, err) + assert.Equal(t, utils.GetSupabaseDbHost(project), DbConfig.Host) + }) +} + +func TestPromptPassword(t *testing.T) { + t.Run("returns user input when provided", func(t *testing.T) { + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + go func() { + defer w.Close() + _, err := w.Write([]byte("test-password")) + assert.Nil(t, err) + }() + + password := PromptPassword(r) + + assert.Equal(t, "test-password", password) + }) + + t.Run("generates password when input is empty", func(t *testing.T) { + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + go func() { + defer w.Close() + _, err := w.Write([]byte("")) + assert.Nil(t, err) + }() + + password := PromptPassword(r) + + assert.Len(t, password, PASSWORD_LENGTH) + assert.NotEqual(t, "", password) + }) +} + +func TestGetDbConfigOptionalPassword(t *testing.T) { + t.Run("uses environment variable when available", func(t *testing.T) { + viper.Set("DB_PASSWORD", "env-password") + projectRef := apitest.RandomProjectRef() + + config := GetDbConfigOptionalPassword(projectRef) + + assert.Equal(t, "env-password", config.Password) + }) +} diff --git a/internal/utils/misc_test.go b/internal/utils/misc_test.go index 6472c2145f..b12d68c789 100644 --- a/internal/utils/misc_test.go +++ b/internal/utils/misc_test.go @@ -75,3 +75,114 @@ func TestProjectRoot(t *testing.T) { assert.Equal(t, cwd, path) }) } + +func TestShortContainerImageName(t *testing.T) { + t.Run("extracts short name from image", func(t *testing.T) { + input := "registry.supabase.com/postgres:15.1.0.99" + expected := "postgres" + + result := ShortContainerImageName(input) + + assert.Equal(t, expected, result) + }) +} + +func TestIsBranchNameReserved(t *testing.T) { + t.Run("identifies reserved names", func(t *testing.T) { + reserved := []string{"_current_branch", "main"} + for _, name := range reserved { + assert.True(t, IsBranchNameReserved(name), "Expected %s to be reserved", name) + } + }) + + t.Run("allows custom names", func(t *testing.T) { + allowed := []string{"my-feature", "test-branch-123"} + for _, name := range allowed { + assert.False(t, IsBranchNameReserved(name), "Expected %s to be allowed", name) + } + }) +} + +func TestValidateFunctionSlug(t *testing.T) { + t.Run("validates correct slugs", func(t *testing.T) { + valid := []string{ + "my-function", + "MyFunction", + "function_1", + "a123", + } + for _, slug := range valid { + err := ValidateFunctionSlug(slug) + assert.NoError(t, err, "Expected %s to be valid", slug) + } + }) + + t.Run("rejects invalid slugs", func(t *testing.T) { + invalid := []string{ + "1function", + "my function", + "function!", + "", + "-function", + "_function", + } + for _, slug := range invalid { + err := ValidateFunctionSlug(slug) + assert.ErrorIs(t, err, ErrInvalidSlug, "Expected %s to be invalid", slug) + } + }) +} + +func TestAssertProjectRefIsValid(t *testing.T) { + t.Run("validates correct refs", func(t *testing.T) { + validRef := "abcdefghijklmnopqrst" + err := AssertProjectRefIsValid(validRef) + assert.NoError(t, err) + }) + + t.Run("rejects invalid refs", func(t *testing.T) { + invalid := []string{ + "tooshort", + "toolongabcdefghijklmnopqrst", + "UPPERCASE", + "special-chars", + "123", + "", + } + for _, ref := range invalid { + err := AssertProjectRefIsValid(ref) + assert.ErrorIs(t, err, ErrInvalidRef, "Expected %s to be invalid", ref) + } + }) +} + +func TestWriteFile(t *testing.T) { + t.Run("writes file with directories", func(t *testing.T) { + fsys := afero.NewMemMapFs() + path := filepath.Join("deep", "nested", "dir", "file.txt") + content := []byte("test content") + + err := WriteFile(path, content, fsys) + + assert.NoError(t, err) + + written, err := afero.ReadFile(fsys, path) + assert.NoError(t, err) + assert.Equal(t, content, written) + }) + + t.Run("overwrites existing file", func(t *testing.T) { + fsys := afero.NewMemMapFs() + path := "test.txt" + original := []byte("original") + updated := []byte("updated") + require.NoError(t, afero.WriteFile(fsys, path, original, 0644)) + + err := WriteFile(path, updated, fsys) + + assert.NoError(t, err) + written, err := afero.ReadFile(fsys, path) + assert.NoError(t, err) + assert.Equal(t, updated, written) + }) +} diff --git a/internal/utils/output_test.go b/internal/utils/output_test.go new file mode 100644 index 0000000000..088136ebac --- /dev/null +++ b/internal/utils/output_test.go @@ -0,0 +1,141 @@ +package utils + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncodeOutput(t *testing.T) { + t.Run("encodes env format", func(t *testing.T) { + input := map[string]string{ + "DATABASE_URL": "postgres://user:pass@host:5432/db", + "API_KEY": "secret-key", + } + var buf bytes.Buffer + err := EncodeOutput(OutputEnv, &buf, input) + assert.NoError(t, err) + expected := "API_KEY=\"secret-key\"\nDATABASE_URL=\"postgres://user:pass@host:5432/db\"\n" + assert.Equal(t, expected, buf.String()) + }) + + t.Run("fails env format with invalid type", func(t *testing.T) { + input := map[string]int{"FOO": 123} + var buf bytes.Buffer + err := EncodeOutput(OutputEnv, &buf, input) + assert.ErrorContains(t, err, "value is not a map[string]string") + }) + + t.Run("encodes json format", func(t *testing.T) { + input := map[string]interface{}{ + "name": "test", + "config": map[string]interface{}{ + "port": 5432, + "enabled": true, + }, + } + var buf bytes.Buffer + err := EncodeOutput(OutputJson, &buf, input) + assert.NoError(t, err) + expected := `{ + "config": { + "enabled": true, + "port": 5432 + }, + "name": "test" +} +` + assert.Equal(t, expected, buf.String()) + }) + + t.Run("encodes yaml format", func(t *testing.T) { + input := map[string]interface{}{ + "name": "test", + "config": map[string]interface{}{ + "port": 5432, + "enabled": true, + }, + } + var buf bytes.Buffer + err := EncodeOutput(OutputYaml, &buf, input) + assert.NoError(t, err) + expected := `config: + enabled: true + port: 5432 +name: test +` + assert.Equal(t, expected, buf.String()) + }) + + t.Run("encodes toml format", func(t *testing.T) { + input := map[string]interface{}{ + "name": "test", + "config": map[string]interface{}{ + "port": 5432, + "enabled": true, + }, + } + var buf bytes.Buffer + err := EncodeOutput(OutputToml, &buf, input) + assert.NoError(t, err) + expected := `name = "test" + +[config] + enabled = true + port = 5432 +` + assert.Equal(t, expected, buf.String()) + }) + + t.Run("fails with unsupported format", func(t *testing.T) { + var buf bytes.Buffer + err := EncodeOutput("invalid", &buf, nil) + assert.ErrorContains(t, err, `Unsupported output encoding "invalid"`) + }) + + t.Run("handles complex nested structures", func(t *testing.T) { + input := map[string]interface{}{ + "database": map[string]interface{}{ + "connections": []map[string]interface{}{ + { + "host": "localhost", + "port": 5432, + }, + { + "host": "remote", + "port": 6543, + }, + }, + "settings": map[string]interface{}{ + "max_connections": 100, + "ssl_enabled": true, + }, + }, + } + var buf bytes.Buffer + err := EncodeOutput(OutputJson, &buf, input) + require.NoError(t, err) + expected := `{ + "database": { + "connections": [ + { + "host": "localhost", + "port": 5432 + }, + { + "host": "remote", + "port": 6543 + } + ], + "settings": { + "max_connections": 100, + "ssl_enabled": true + } + } +} +` + assert.Equal(t, expected, buf.String()) + }) +} diff --git a/internal/utils/slice_test.go b/internal/utils/slice_test.go index 81642c73ff..47bdea171f 100644 --- a/internal/utils/slice_test.go +++ b/internal/utils/slice_test.go @@ -7,9 +7,59 @@ import ( ) func TestSliceEqual(t *testing.T) { - assert.False(t, SliceEqual([]string{"a"}, []string{"b"})) + t.Run("different slices", func(t *testing.T) { + assert.False(t, SliceEqual([]string{"a"}, []string{"b"})) + }) + + t.Run("different lengths", func(t *testing.T) { + assert.False(t, SliceEqual([]string{"a"}, []string{"a", "b"})) + assert.False(t, SliceEqual([]string{"a", "b"}, []string{"a"})) + }) + + t.Run("equal slices", func(t *testing.T) { + assert.True(t, SliceEqual([]string{"a", "b"}, []string{"a", "b"})) + assert.True(t, SliceEqual([]int{1, 2, 3}, []int{1, 2, 3})) + }) + + t.Run("empty slices", func(t *testing.T) { + assert.True(t, SliceEqual([]string{}, []string{})) + }) } func TestSliceContains(t *testing.T) { - assert.False(t, SliceContains([]string{"a"}, "b")) + t.Run("not contains element", func(t *testing.T) { + assert.False(t, SliceContains([]string{"a"}, "b")) + }) + + t.Run("contains element", func(t *testing.T) { + assert.True(t, SliceContains([]string{"a", "b", "c"}, "b")) + assert.True(t, SliceContains([]int{1, 2, 3}, 2)) + }) + + t.Run("empty slice", func(t *testing.T) { + assert.False(t, SliceContains([]string{}, "a")) + }) +} + +func TestRemoveDuplicates(t *testing.T) { + t.Run("string slice", func(t *testing.T) { + input := []string{"a", "b", "a", "c", "b", "d"} + expected := []string{"a", "b", "c", "d"} + assert.Equal(t, expected, RemoveDuplicates(input)) + }) + + t.Run("int slice", func(t *testing.T) { + input := []int{1, 2, 2, 3, 1, 4, 3, 5} + expected := []int{1, 2, 3, 4, 5} + assert.Equal(t, expected, RemoveDuplicates(input)) + }) + + t.Run("empty slice", func(t *testing.T) { + assert.Empty(t, RemoveDuplicates([]string{})) + }) + + t.Run("no duplicates", func(t *testing.T) { + input := []int{1, 2, 3, 4, 5} + assert.Equal(t, input, RemoveDuplicates(input)) + }) } diff --git a/internal/utils/tenant/client_test.go b/internal/utils/tenant/client_test.go new file mode 100644 index 0000000000..6057f502f2 --- /dev/null +++ b/internal/utils/tenant/client_test.go @@ -0,0 +1,151 @@ +package tenant + +import ( + "context" + "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/pkg/api" +) + +func TestApiKey(t *testing.T) { + t.Run("creates api key from response", func(t *testing.T) { + resp := []api.ApiKeyResponse{ + {Name: "anon", ApiKey: "anon-key"}, + {Name: "service_role", ApiKey: "service-key"}, + } + + keys := NewApiKey(resp) + + assert.Equal(t, "anon-key", keys.Anon) + assert.Equal(t, "service-key", keys.ServiceRole) + assert.False(t, keys.IsEmpty()) + }) + + t.Run("handles empty response", func(t *testing.T) { + resp := []api.ApiKeyResponse{} + + keys := NewApiKey(resp) + + assert.Empty(t, keys.Anon) + assert.Empty(t, keys.ServiceRole) + assert.True(t, keys.IsEmpty()) + }) + + t.Run("handles partial response", func(t *testing.T) { + resp := []api.ApiKeyResponse{ + {Name: "anon", ApiKey: "anon-key"}, + } + + keys := NewApiKey(resp) + + assert.Equal(t, "anon-key", keys.Anon) + assert.Empty(t, keys.ServiceRole) + assert.False(t, keys.IsEmpty()) + }) +} + +func TestGetApiKeys(t *testing.T) { + t.Run("retrieves api keys successfully", func(t *testing.T) { + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + + defer gock.OffAll() + projectRef := apitest.RandomProjectRef() + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + projectRef + "/api-keys"). + Reply(http.StatusOK). + JSON([]api.ApiKeyResponse{ + {Name: "anon", ApiKey: "anon-key"}, + {Name: "service_role", ApiKey: "service-key"}, + }) + + keys, err := GetApiKeys(context.Background(), projectRef) + + assert.NoError(t, err) + assert.Equal(t, "anon-key", keys.Anon) + assert.Equal(t, "service-key", keys.ServiceRole) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("handles network error", func(t *testing.T) { + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + + defer gock.OffAll() + projectRef := apitest.RandomProjectRef() + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + projectRef + "/api-keys"). + ReplyError(gock.ErrCannotMatch) + + keys, err := GetApiKeys(context.Background(), projectRef) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get api keys") + assert.True(t, keys.IsEmpty()) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("handles unauthorized error", func(t *testing.T) { + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + + defer gock.OffAll() + projectRef := apitest.RandomProjectRef() + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + projectRef + "/api-keys"). + Reply(http.StatusUnauthorized). + JSON(map[string]string{"message": "unauthorized"}) + + keys, err := GetApiKeys(context.Background(), projectRef) + + assert.ErrorIs(t, err, ErrAuthToken) + assert.True(t, keys.IsEmpty()) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("handles missing anon key", func(t *testing.T) { + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + + defer gock.OffAll() + projectRef := apitest.RandomProjectRef() + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + projectRef + "/api-keys"). + Reply(http.StatusOK). + JSON([]api.ApiKeyResponse{}) // should this error if response has only service_role key? + + keys, err := GetApiKeys(context.Background(), projectRef) + + assert.ErrorIs(t, err, errMissingKey) + assert.True(t, keys.IsEmpty()) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) +} + +func TestNewTenantAPI(t *testing.T) { + t.Run("creates tenant api client", func(t *testing.T) { + projectRef := apitest.RandomProjectRef() + anonKey := "test-key" + + api := NewTenantAPI(context.Background(), projectRef, anonKey) + + assert.NotNil(t, api.Fetcher) + + defer gock.OffAll() + gock.New("https://"+utils.GetSupabaseHost(projectRef)). + Get("/test"). + MatchHeader("apikey", anonKey). + MatchHeader("User-Agent", "SupabaseCLI/"+utils.Version). + Reply(http.StatusOK) + + _, err := api.Send(context.Background(), http.MethodGet, "/test", nil) + + assert.NoError(t, err) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) +} diff --git a/internal/utils/tenant/database_test.go b/internal/utils/tenant/database_test.go new file mode 100644 index 0000000000..c6680f2b72 --- /dev/null +++ b/internal/utils/tenant/database_test.go @@ -0,0 +1,88 @@ +package tenant + +import ( + "context" + "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/pkg/api" +) + +func TestGetDatabaseVersion(t *testing.T) { + t.Run("retrieves database version successfully", func(t *testing.T) { + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + + defer gock.OffAll() + projectRef := apitest.RandomProjectRef() + gock.New(utils.DefaultApiHost). + Get("/v1/projects"). + Reply(http.StatusOK). + JSON([]api.V1ProjectWithDatabaseResponse{ + { + Id: projectRef, + Database: api.V1DatabaseResponse{ + Version: "14.1.0.99", + }, + }, + }) + + version, err := GetDatabaseVersion(context.Background(), projectRef) + + assert.NoError(t, err) + assert.Equal(t, "14.1.0.99", version) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("handles project not found", func(t *testing.T) { + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + + defer gock.OffAll() + projectRef := apitest.RandomProjectRef() + gock.New(utils.DefaultApiHost). + Get("/v1/projects"). + Reply(http.StatusOK). + JSON([]api.V1ProjectWithDatabaseResponse{ + { + Id: "different-project", + Database: api.V1DatabaseResponse{ + Version: "14.1.0.99", + }, + }, + }) + + version, err := GetDatabaseVersion(context.Background(), projectRef) + + assert.ErrorIs(t, err, errDatabaseVersion) + assert.Empty(t, version) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("handles empty database version", func(t *testing.T) { + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + + defer gock.OffAll() + projectRef := apitest.RandomProjectRef() + gock.New(utils.DefaultApiHost). + Get("/v1/projects"). + Reply(http.StatusOK). + JSON([]api.V1ProjectWithDatabaseResponse{ + { + Id: projectRef, + Database: api.V1DatabaseResponse{}, + }, + }) + + version, err := GetDatabaseVersion(context.Background(), projectRef) + + assert.ErrorIs(t, err, errDatabaseVersion) + assert.Empty(t, version) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) +} From 032c2cb8840252d739e21083f76fd40a44bc9ed5 Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Mon, 5 May 2025 21:06:38 +0800 Subject: [PATCH 22/22] fix: remove deprecated remote changes and commit (#3520) --- cmd/db.go | 10 +-- internal/db/remote/changes/changes.go | 47 ------------ internal/db/remote/commit/commit.go | 106 -------------------------- 3 files changed, 5 insertions(+), 158 deletions(-) delete mode 100644 internal/db/remote/changes/changes.go delete mode 100644 internal/db/remote/commit/commit.go diff --git a/cmd/db.go b/cmd/db.go index bcc7ac5e76..948f77bebe 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -18,8 +18,6 @@ import ( "github.com/supabase/cli/internal/db/lint" "github.com/supabase/cli/internal/db/pull" "github.com/supabase/cli/internal/db/push" - "github.com/supabase/cli/internal/db/remote/changes" - "github.com/supabase/cli/internal/db/remote/commit" "github.com/supabase/cli/internal/db/reset" "github.com/supabase/cli/internal/db/start" "github.com/supabase/cli/internal/db/test" @@ -175,7 +173,7 @@ var ( Short: "Show changes on the remote database", Long: "Show changes on the remote database since last migration.", RunE: func(cmd *cobra.Command, args []string) error { - return changes.Run(cmd.Context(), schema, flags.DbConfig, afero.NewOsFs()) + return diff.Run(cmd.Context(), schema, file, flags.DbConfig, diff.DiffSchemaMigra, afero.NewOsFs()) }, } @@ -184,7 +182,7 @@ var ( Use: "commit", Short: "Commit remote changes as a new migration", RunE: func(cmd *cobra.Command, args []string) error { - return commit.Run(cmd.Context(), schema, flags.DbConfig, afero.NewOsFs()) + return pull.Run(cmd.Context(), schema, flags.DbConfig, "remote_commit", afero.NewOsFs()) }, } @@ -304,10 +302,12 @@ func init() { dbCmd.AddCommand(dbPullCmd) // Build remote command remoteFlags := dbRemoteCmd.PersistentFlags() + remoteFlags.StringSliceVarP(&schema, "schema", "s", []string{}, "Comma separated list of schema to include.") remoteFlags.String("db-url", "", "Connect using the specified Postgres URL (must be percent-encoded).") + remoteFlags.Bool("linked", true, "Connect to the linked project.") + dbRemoteCmd.MarkFlagsMutuallyExclusive("db-url", "linked") remoteFlags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database.") cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", remoteFlags.Lookup("password"))) - remoteFlags.StringSliceVarP(&schema, "schema", "s", []string{}, "Comma separated list of schema to include.") dbRemoteCmd.AddCommand(dbRemoteChangesCmd) dbRemoteCmd.AddCommand(dbRemoteCommitCmd) dbCmd.AddCommand(dbRemoteCmd) diff --git a/internal/db/remote/changes/changes.go b/internal/db/remote/changes/changes.go deleted file mode 100644 index 12c39288be..0000000000 --- a/internal/db/remote/changes/changes.go +++ /dev/null @@ -1,47 +0,0 @@ -package changes - -import ( - "context" - "io" - - "github.com/jackc/pgconn" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/db/diff" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/migration" -) - -var output string - -func Run(ctx context.Context, schema []string, config pgconn.Config, fsys afero.Fs) error { - if err := utils.RunProgram(ctx, func(p utils.Program, ctx context.Context) error { - return run(p, ctx, schema, config, fsys) - }); err != nil { - return err - } - return diff.SaveDiff(output, "", fsys) -} - -func run(p utils.Program, ctx context.Context, schema []string, config pgconn.Config, fsys afero.Fs) (err error) { - // 1. Assert `supabase/migrations` and `schema_migrations` are in sync. - w := utils.StatusWriter{Program: p} - if len(schema) == 0 { - schema, err = loadSchema(ctx, config, w) - if err != nil { - return err - } - } - - // 2. Diff remote db (source) & shadow db (target) and print it. - output, err = diff.DiffDatabase(ctx, schema, config, w, fsys, diff.DiffSchemaMigra) - return err -} - -func loadSchema(ctx context.Context, config pgconn.Config, w io.Writer) ([]string, error) { - conn, err := utils.ConnectByConfigStream(ctx, config, w) - if err != nil { - return nil, err - } - defer conn.Close(context.Background()) - return migration.ListUserSchemas(ctx, conn) -} diff --git a/internal/db/remote/commit/commit.go b/internal/db/remote/commit/commit.go deleted file mode 100644 index 3e49346f6f..0000000000 --- a/internal/db/remote/commit/commit.go +++ /dev/null @@ -1,106 +0,0 @@ -package commit - -import ( - "context" - _ "embed" - "fmt" - "path/filepath" - - "github.com/go-errors/errors" - "github.com/jackc/pgconn" - "github.com/jackc/pgx/v4" - "github.com/spf13/afero" - "github.com/supabase/cli/internal/db/diff" - "github.com/supabase/cli/internal/db/dump" - "github.com/supabase/cli/internal/migration/list" - "github.com/supabase/cli/internal/migration/repair" - "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/pkg/migration" -) - -func Run(ctx context.Context, schema []string, config pgconn.Config, fsys afero.Fs) error { - if err := utils.RunProgram(ctx, func(p utils.Program, ctx context.Context) error { - return run(p, ctx, schema, config, fsys) - }); err != nil { - return err - } - fmt.Println("Finished " + utils.Aqua("supabase db remote commit") + ".") - return nil -} - -func run(p utils.Program, ctx context.Context, schema []string, config pgconn.Config, fsys afero.Fs) error { - // 1. Assert `supabase/migrations` and `schema_migrations` are in sync. - w := utils.StatusWriter{Program: p} - conn, err := utils.ConnectByConfigStream(ctx, config, w) - if err != nil { - return err - } - defer conn.Close(context.Background()) - if err := assertRemoteInSync(ctx, conn, fsys); err != nil { - return err - } - - // 2. Fetch remote schema changes - if len(schema) == 0 { - schema, err = migration.ListUserSchemas(ctx, conn) - if err != nil { - return err - } - } - timestamp := utils.GetCurrentTimestamp() - if err := fetchRemote(p, ctx, schema, timestamp, config, fsys); err != nil { - return err - } - - // 3. Insert a row to `schema_migrations` - return repair.UpdateMigrationTable(ctx, conn, []string{timestamp}, repair.Applied, false, fsys) -} - -func fetchRemote(p utils.Program, ctx context.Context, schema []string, timestamp string, config pgconn.Config, fsys afero.Fs) error { - path := filepath.Join(utils.MigrationsDir, timestamp+"_remote_commit.sql") - // Special case if this is the first migration - if migrations, err := migration.ListLocalMigrations(utils.MigrationsDir, afero.NewIOFS(fsys)); err != nil { - return err - } else if len(migrations) == 0 { - p.Send(utils.StatusMsg("Committing initial migration on remote database...")) - return dump.Run(ctx, path, config, nil, nil, false, false, false, false, false, fsys) - } - - w := utils.StatusWriter{Program: p} - // Diff remote db (source) & shadow db (target) and write it as a new migration. - output, err := diff.DiffDatabase(ctx, schema, config, w, fsys, diff.DiffSchemaMigra) - if err != nil { - return err - } - if len(output) == 0 { - return errors.New("No schema changes found") - } - return utils.WriteFile(path, []byte(output), fsys) -} - -func assertRemoteInSync(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error { - remoteMigrations, err := migration.ListRemoteMigrations(ctx, conn) - if err != nil { - return err - } - localMigrations, err := list.LoadLocalVersions(fsys) - if err != nil { - return err - } - - conflictErr := errors.New("The remote database's migration history is not in sync with the contents of " + utils.Bold(utils.MigrationsDir) + `. Resolve this by: -- Updating the project from version control to get the latest ` + utils.Bold(utils.MigrationsDir) + `, -- Pushing unapplied migrations with ` + utils.Aqua("supabase db push") + `, -- Or failing that, manually editing supabase_migrations.schema_migrations table with ` + utils.Aqua("supabase migration repair") + ".") - if len(remoteMigrations) != len(localMigrations) { - return conflictErr - } - - for i, remoteTimestamp := range remoteMigrations { - if localMigrations[i] != remoteTimestamp { - return conflictErr - } - } - - return nil -}