Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions pkg/function/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"github.com/docker/go-units"
"github.com/go-errors/errors"
"github.com/supabase/cli/pkg/api"
"github.com/supabase/cli/pkg/cast"
"github.com/supabase/cli/pkg/config"
)

Expand Down Expand Up @@ -44,9 +43,9 @@ func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig con
return err
}
policy.Reset()
checksum := make(map[string]string, len(result))
for _, f := range result {
checksum[f.Slug] = cast.Val(f.EzbrSha256, "")
slugToIndex := make(map[string]int, len(result))
for i, f := range result {
slugToIndex[f.Slug] = i
}
var toUpdate api.BulkUpdateFunctionBody
OUTER:
Expand All @@ -69,13 +68,15 @@ OUTER:
bodyHash := sha256.Sum256(body.Bytes())
meta.SHA256 = hex.EncodeToString(bodyHash[:])
// Skip if function has not changed
if checksum[slug] == meta.SHA256 {
if i, exists := slugToIndex[slug]; exists && i >= 0 &&
result[i].EzbrSha256 != nil && *result[i].EzbrSha256 == meta.SHA256 &&
result[i].VerifyJwt != nil && *result[i].VerifyJwt == function.VerifyJWT {
fmt.Fprintln(os.Stderr, "No change found in Function:", slug)
continue
}
// Update if function already exists
upsert := func() (api.BulkUpdateFunctionBody, error) {
if _, ok := checksum[slug]; ok {
if _, exists := slugToIndex[slug]; exists {
return s.updateFunction(ctx, slug, meta, bytes.NewReader(body.Bytes()))
}
return s.createFunction(ctx, slug, meta, bytes.NewReader(body.Bytes()))
Expand All @@ -84,7 +85,7 @@ OUTER:
fmt.Fprintf(os.Stderr, "Deploying Function: %s (script size: %s)\n", slug, functionSize)
result, err := backoff.RetryNotifyWithData(upsert, policy, func(err error, d time.Duration) {
if strings.Contains(err.Error(), "Duplicated function slug") {
checksum[slug] = ""
slugToIndex[slug] = -1
}
})
if err != nil {
Expand Down
150 changes: 90 additions & 60 deletions pkg/function/batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/supabase/cli/pkg/api"
"github.com/supabase/cli/pkg/cast"
"github.com/supabase/cli/pkg/config"
)

Expand All @@ -29,19 +30,24 @@ func (b *MockBundler) Bundle(ctx context.Context, slug, entrypoint, importMap st
}, nil
}

func mockClient(t *testing.T) EdgeRuntimeAPI {
apiClient, err := api.NewClientWithResponses(mockApiHost)
require.NoError(t, err)
return NewEdgeRuntimeAPI(mockProject, *apiClient, func(era *EdgeRuntimeAPI) {
era.eszip = &MockBundler{}
})
}

const (
mockApiHost = "https://api.supabase.com"
mockProject = "test-project"
)

func TestUpsertFunctions(t *testing.T) {
apiClient, err := api.NewClientWithResponses(mockApiHost)
require.NoError(t, err)
client := NewEdgeRuntimeAPI(mockProject, *apiClient, func(era *EdgeRuntimeAPI) {
era.eszip = &MockBundler{}
})
client := mockClient(t)

t.Run("deploys with bulk update", func(t *testing.T) {
// t.Cleanup(mockClock(100 * time.Millisecond))
// Setup mock api
defer gock.OffAll()
gock.New(mockApiHost).
Expand All @@ -53,8 +59,8 @@ func TestUpsertFunctions(t *testing.T) {
Reply(http.StatusOK).
JSON(api.FunctionResponse{Slug: "test-a"})
gock.New(mockApiHost).
Post("/v1/projects/" + mockProject + "/functions/test-b").
Reply(http.StatusOK).
Post("/v1/projects/" + mockProject + "/functions").
Reply(http.StatusCreated).
JSON(api.FunctionResponse{Slug: "test-b"})
gock.New(mockApiHost).
Put("/v1/projects/" + mockProject + "/functions").
Expand All @@ -68,8 +74,30 @@ func TestUpsertFunctions(t *testing.T) {
JSON(api.V1BulkUpdateFunctionsResponse{})
// Run test
err := client.UpsertFunctions(context.Background(), config.FunctionConfig{
"test-a": {},
"test-b": {},
"test-a": {Enabled: true},
"test-b": {Enabled: true},
})
// Check error
assert.NoError(t, err)
assert.Empty(t, gock.Pending())
assert.Empty(t, gock.GetUnmatchedRequests())
})

t.Run("skips disabled and unchanged", 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",
VerifyJwt: cast.Ptr(true),
EzbrSha256: cast.Ptr("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
}})
// Run test
err := client.UpsertFunctions(context.Background(), config.FunctionConfig{
"test-a": {Enabled: true, VerifyJWT: true},
"test-b": {Enabled: false},
})
// Check error
assert.NoError(t, err)
Expand All @@ -94,7 +122,7 @@ func TestUpsertFunctions(t *testing.T) {
JSON(api.FunctionResponse{Slug: "test"})
// Run test
err := client.UpsertFunctions(context.Background(), config.FunctionConfig{
"test": {},
"test": {Enabled: true},
})
// Check error
assert.NoError(t, err)
Expand All @@ -121,58 +149,60 @@ func TestUpsertFunctions(t *testing.T) {
assert.Empty(t, gock.Pending())
assert.Empty(t, gock.GetUnmatchedRequests())
})
}

t.Run("retries on create failure", 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").
ReplyError(errors.New("network error"))
gock.New(mockApiHost).
Post("/v1/projects/" + mockProject + "/functions").
Reply(http.StatusServiceUnavailable)
gock.New(mockApiHost).
Post("/v1/projects/" + mockProject + "/functions").
Reply(http.StatusCreated).
JSON(api.FunctionResponse{Slug: "test"})
// Run test
err := client.UpsertFunctions(context.Background(), config.FunctionConfig{
"test": {},
})
// Check error
assert.NoError(t, err)
assert.Empty(t, gock.Pending())
assert.Empty(t, gock.GetUnmatchedRequests())
func TestCreateFunction(t *testing.T) {
client := mockClient(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").
ReplyError(errors.New("network error"))
gock.New(mockApiHost).
Post("/v1/projects/" + mockProject + "/functions").
Reply(http.StatusServiceUnavailable)
gock.New(mockApiHost).
Post("/v1/projects/" + mockProject + "/functions").
Reply(http.StatusCreated).
JSON(api.FunctionResponse{Slug: "test"})
// Run test
err := client.UpsertFunctions(context.Background(), config.FunctionConfig{
"test": {Enabled: true},
})
// Check error
assert.NoError(t, err)
assert.Empty(t, gock.Pending())
assert.Empty(t, gock.GetUnmatchedRequests())
}

t.Run("retries on update failure", 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"}})
gock.New(mockApiHost).
Patch("/v1/projects/" + mockProject + "/functions/test").
ReplyError(errors.New("network error"))
gock.New(mockApiHost).
Patch("/v1/projects/" + mockProject + "/functions/test").
Reply(http.StatusServiceUnavailable)
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, gock.Pending())
assert.Empty(t, gock.GetUnmatchedRequests())
func TestUpdateFunction(t *testing.T) {
client := mockClient(t)
// Setup mock api
defer gock.OffAll()
gock.New(mockApiHost).
Get("/v1/projects/" + mockProject + "/functions").
Reply(http.StatusOK).
JSON([]api.FunctionResponse{{Slug: "test"}})
gock.New(mockApiHost).
Patch("/v1/projects/" + mockProject + "/functions/test").
ReplyError(errors.New("network error"))
gock.New(mockApiHost).
Patch("/v1/projects/" + mockProject + "/functions/test").
Reply(http.StatusServiceUnavailable)
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": {Enabled: true},
})
// Check error
assert.NoError(t, err)
assert.Empty(t, gock.Pending())
assert.Empty(t, gock.GetUnmatchedRequests())
}