Repository: supabase/cli
Pull request #601 (2022-11-13 10:48:01)

chore: Use ESZIP format to deploy functions
## What kind of change does this PR introduce?

This changes `supabase function deploy` command to use ESZIP format.

* ESZIP building is done via a Deno script that's copied to `$HOME/.supabase/eszip`

* The older JS bundling is still supported and can be used via `--legacy-bundle` flag.

* With ESZIP, we no longer need to keep the Deno version pinned to 1.20.X. This commit contains changes to update Deno to `1.27.X` and also use a latest image of `functions-relay` (1.4.0), for locally serving functions.

TODO:
[x] Update Tests 


coveralls: ## Pull Request Test Coverage Report for [Build 3460871204](https://coveralls.io/builds/54182187)

* **73** of **112**   **(65.18%)**  changed or added relevant lines in **3** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.2%**) to **49.293%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [cmd/functions.go](https://coveralls.io/builds/54182187/source?filename=cmd%2Ffunctions.go#L62) | 1 | 2 | 50.0%
| [internal/functions/deploy/deploy.go](https://coveralls.io/builds/54182187/source?filename=internal%2Ffunctions%2Fdeploy%2Fdeploy.go#L44) | 40 | 56 | 71.43%
| [internal/utils/misc.go](https://coveralls.io/builds/54182187/source?filename=internal%2Futils%2Fmisc.go#L390) | 32 | 54 | 59.26%
<!-- | **Total:** | **73** | **112** | **65.18%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/54182187/badge)](https://coveralls.io/builds/54182187) |
| :-- | --: |
| Change from base [Build 3459637251](https://coveralls.io/builds/54176111): |  0.2% |
| Covered Lines: | 2718 |
| Relevant Lines: | 5514 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/cmd/functions.go b/cmd/functions.go
index 4c53e17e..d0e18e17 100644
--- a/cmd/functions.go
+++ b/cmd/functions.go
@@ -42,7 +42,8 @@ var (
 		},
 	}
 
-	noVerifyJWT bool
+	noVerifyJWT     bool
+	useLegacyBundle bool
 
 	functionsDeployCmd = &cobra.Command{
 		Use:   "deploy <Function name>",
@@ -58,7 +59,7 @@ var (
 				return err
 			}
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
-			return deploy.Run(ctx, args[0], projectRef, !noVerifyJWT, fsys)
+			return deploy.Run(ctx, args[0], projectRef, !noVerifyJWT, useLegacyBundle, fsys)
 		},
 	}
 
@@ -89,6 +90,7 @@ func init() {
 	functionsDeleteCmd.Flags().StringVar(&projectRef, "project-ref", "", "Project ref of the Supabase project.")
 	functionsDeployCmd.Flags().BoolVar(&noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
 	functionsDeployCmd.Flags().StringVar(&projectRef, "project-ref", "", "Project ref of the Supabase project.")
+	functionsDeployCmd.Flags().BoolVar(&useLegacyBundle, "legacy-bundle", true, "Use legacy bundling mechanism.")
 	functionsServeCmd.Flags().BoolVar(&noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
 	functionsServeCmd.Flags().StringVar(&envFilePath, "env-file", "", "Path to an env file to be populated to the Function environment.")
 	functionsCmd.AddCommand(functionsDeleteCmd)
diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go
index 21e56892..000dabb5 100644
--- a/internal/functions/deploy/deploy.go
+++ b/internal/functions/deploy/deploy.go
@@ -5,19 +5,24 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io"
 	"net/http"
 	"os"
 	"os/exec"
 	"path/filepath"
+	"strings"
 
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/pkg/api"
 )
 
-func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool, fsys afero.Fs) error {
+const EszipContentType = "application/vnd.denoland.eszip"
+
+func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool, useLegacyBundle bool, fsys afero.Fs) error {
 	// 1. Sanity checks.
 	projectRef := projectRefArg
+	var buildScriptPath string
 	{
 		if len(projectRefArg) == 0 {
 			ref, err := utils.LoadProjectRef(fsys)
@@ -34,10 +39,16 @@ func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool,
 		if err := utils.InstallOrUpgradeDeno(ctx, fsys); err != nil {
 			return err
 		}
+
+		var err error
+		buildScriptPath, err = utils.CopyEszipScripts(ctx, fsys)
+		if err != nil {
+			return err
+		}
 	}
 
 	// 2. Bundle Function.
-	var newFunctionBody string
+	var functionBody io.Reader
 	{
 		fmt.Println("Bundling " + utils.Bold(slug))
 		denoPath, err := utils.GetDenoPath()
@@ -55,7 +66,10 @@ func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool,
 			}
 		}
 
-		args := []string{"bundle", "--no-check=remote", "--quiet", filepath.Join(functionPath, "index.ts")}
+		args := []string{"run", "-A", buildScriptPath, filepath.Join(functionPath, "index.ts")}
+		if useLegacyBundle {
+			args = []string{"bundle", "--no-check=remote", "--quiet", filepath.Join(functionPath, "index.ts")}
+		}
 		cmd := exec.CommandContext(ctx, denoPath, args...)
 		var outBuf, errBuf bytes.Buffer
 		cmd.Stdout = &outBuf
@@ -64,14 +78,24 @@ func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool,
 			return fmt.Errorf("Error bundling function: %w\n%v", err, errBuf.String())
 		}
 
-		newFunctionBody = outBuf.String()
+		functionBody = &outBuf
 	}
 
 	// 3. Deploy new Function.
-	return deployFunction(ctx, projectRef, slug, newFunctionBody, verifyJWT)
+	return deployFunction(ctx, projectRef, slug, functionBody, verifyJWT, useLegacyBundle)
 }
 
-func deployFunction(ctx context.Context, projectRef, slug, newFunctionBody string, verifyJWT bool) error {
+func makeLegacyFunctionBody(functionBody io.Reader) (string, error) {
+	buf := new(strings.Builder)
+	_, err := io.Copy(buf, functionBody)
+	if err != nil {
+		return "", err
+	}
+
+	return buf.String(), nil
+}
+
+func deployFunction(ctx context.Context, projectRef, slug string, functionBody io.Reader, verifyJWT, useLegacyBundle bool) error {
 	var deployedFuncId string
 	{
 		resp, err := utils.GetSupabase().GetFunctionWithResponse(ctx, projectRef, slug, &api.GetFunctionParams{})
@@ -79,14 +103,32 @@ func deployFunction(ctx context.Context, projectRef, slug, newFunctionBody strin
 			return err
 		}
 
+		var functionBodyStr string
+		if useLegacyBundle {
+			functionBodyStr, err = makeLegacyFunctionBody(functionBody)
+			if err != nil {
+				return err
+			}
+		}
+
 		switch resp.StatusCode() {
 		case http.StatusNotFound: // Function doesn't exist yet, so do a POST
-			resp, err := utils.GetSupabase().CreateFunctionWithResponse(ctx, projectRef, &api.CreateFunctionParams{}, api.CreateFunctionBody{
-				Body:      newFunctionBody,
-				Name:      slug,
-				Slug:      slug,
-				VerifyJwt: &verifyJWT,
-			})
+			var resp *api.CreateFunctionResponse
+			var err error
+			if useLegacyBundle {
+				resp, err = utils.GetSupabase().CreateFunctionWithResponse(ctx, projectRef, &api.CreateFunctionParams{}, api.CreateFunctionJSONRequestBody{
+					Body:      functionBodyStr,
+					Name:      slug,
+					Slug:      slug,
+					VerifyJwt: &verifyJWT,
+				})
+			} else {
+				resp, err = utils.GetSupabase().CreateFunctionWithBodyWithResponse(ctx, projectRef, &api.CreateFunctionParams{
+					Slug:      &slug,
+					Name:      &slug,
+					VerifyJwt: &verifyJWT,
+				}, EszipContentType, functionBody)
+			}
 			if err != nil {
 				return err
 			}
@@ -95,10 +137,18 @@ func deployFunction(ctx context.Context, projectRef, slug, newFunctionBody strin
 			}
 			deployedFuncId = resp.JSON201.Id
 		case http.StatusOK: // Function already exists, so do a PATCH
-			resp, err := utils.GetSupabase().UpdateFunctionWithResponse(ctx, projectRef, slug, &api.UpdateFunctionParams{}, api.UpdateFunctionBody{
-				Body:      &newFunctionBody,
-				VerifyJwt: &verifyJWT,
-			})
+			var resp *api.UpdateFunctionResponse
+			var err error
+			if useLegacyBundle {
+				resp, err = utils.GetSupabase().UpdateFunctionWithResponse(ctx, projectRef, slug, &api.UpdateFunctionParams{}, api.UpdateFunctionJSONRequestBody{
+					Body:      &functionBodyStr,
+					VerifyJwt: &verifyJWT,
+				})
+			} else {
+				resp, err = utils.GetSupabase().UpdateFunctionWithBodyWithResponse(ctx, projectRef, slug, &api.UpdateFunctionParams{
+					VerifyJwt: &verifyJWT,
+				}, EszipContentType, functionBody)
+			}
 			if err != nil {
 				return err
 			}
diff --git a/internal/utils/eszip/build.ts b/internal/utils/eszip/build.ts
new file mode 100644
index 00000000..a33876d8
--- /dev/null
+++ b/internal/utils/eszip/build.ts
@@ -0,0 +1,45 @@
+import * as path from "https://deno.land/std@0.127.0/path/mod.ts";
+import { writeAll } from "https://deno.land/std@0.162.0/streams/conversion.ts";
+import { compress } from "https://deno.land/x/brotli@0.1.7/mod.ts";
+
+import { build } from "https://deno.land/x/eszip@v0.30.0/mod.ts";
+import { load } from "https://deno.land/x/eszip@v0.30.0/loader.ts";
+
+const virtualBasePath = "file:///src/";
+
+async function buildAndWrite(p: string) {
+  const entrypoint = new URL("index.ts", virtualBasePath).href;
+  const eszip = await build([entrypoint], async (specifier: string) => {
+    const url = new URL(specifier);
+    if (url.protocol === 'file:') {
+      console.error(specifier)
+      // if the path is `file:///*`, treat it as a path from parent directory
+      let actualPath = specifier.replace('file:///', `./${path.dirname(p)}/../`);
+      // if the path is `file:///src/*`, treat it as a relative path from current dir
+      if (specifier.startsWith(virtualBasePath)) {
+        actualPath = specifier.replace(virtualBasePath, `./${path.dirname(p)}/`);
+      }
+      const content = await Deno.readTextFile((actualPath));
+      return {
+        kind: "module",
+        specifier,
+        content
+      }
+    } 
+
+    return load(specifier);
+  });
+  // compress ESZIP payload using Brotli
+  const compressed = compress(eszip);
+
+  // add a marker frame to the start of the payload
+  const marker = new TextEncoder().encode("EZBR");
+
+  const combinedPayload = new Uint8Array(marker.length + compressed.length);
+  combinedPayload.set(marker);
+  combinedPayload.set(compressed, marker.length);
+
+  await writeAll(Deno.stdout, combinedPayload);
+}
+
+buildAndWrite(Deno.args[0]);
diff --git a/internal/utils/misc.go b/internal/utils/misc.go
index aac7f6bc..6619d0d2 100644
--- a/internal/utils/misc.go
+++ b/internal/utils/misc.go
@@ -4,10 +4,12 @@ import (
 	"archive/zip"
 	"bytes"
 	"context"
-	_ "embed"
+	"crypto/md5"
+	"embed"
 	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"net/http"
 	"os"
 	"os/exec"
@@ -32,7 +34,7 @@ const (
 	MigraImage     = "djrobstep/migra:3.0.1621480950"
 	PgmetaImage    = "supabase/postgres-meta:v0.50.2"
 	StudioImage    = "supabase/studio:0.22.08"
-	DenoRelayImage = "supabase/deno-relay:v1.2.4"
+	DenoRelayImage = "supabase/deno-relay:v1.4.0"
 	// Update initial schemas in internal/utils/templates/initial_schemas when
 	// updating any one of these.
 	GotrueImage   = "supabase/gotrue:v2.25.1"
@@ -87,6 +89,9 @@ var (
 	//go:embed templates/globals.sql
 	GlobalsSql string
 
+	//go:embed eszip/*
+	eszipEmbedDir embed.FS
+
 	AccessTokenPattern = regexp.MustCompile(`^sbp_[a-f0-9]{40}$`)
 	ProjectRefPattern  = regexp.MustCompile(`^[a-z]{20}$`)
 	PostgresUrlPattern = regexp.MustCompile(`^postgres(?:ql)?:\/\/postgres:(.*)@(.+)\/postgres$`)
@@ -299,7 +304,7 @@ func InstallOrUpgradeDeno(ctx context.Context, fsys afero.Fs) error {
 
 	if _, err := fsys.Stat(denoPath); err == nil {
 		// Upgrade Deno.
-		cmd := exec.CommandContext(ctx, denoPath, "upgrade", "--version", "1.20.3")
+		cmd := exec.CommandContext(ctx, denoPath, "upgrade", "--version", "1.27.1")
 		return cmd.Run()
 	} else if !errors.Is(err, os.ErrNotExist) {
 		return err
@@ -376,6 +381,80 @@ func InstallOrUpgradeDeno(ctx context.Context, fsys afero.Fs) error {
 	return nil
 }
 
+func isBuildScriptModified(fsys afero.Fs, buildScriptPath string) (bool, error) {
+	bs, err := afero.ReadFile(fsys, buildScriptPath)
+	if err != nil {
+		if errors.Is(err, fs.ErrNotExist) {
+			return true, nil
+		}
+		return false, err
+	}
+
+	es, err := fs.ReadFile(eszipEmbedDir, "eszip/build.ts")
+	if err != nil {
+		return false, err
+	}
+
+	// compare the md5 checksum of current build script with user's copy.
+	// if the checksums doesn't match, build script is modified.
+	return md5.Sum(bs) != md5.Sum(es), nil
+}
+
+// Copy ESZIP scripts needed for function deploy, returning the build script path or an error.
+func CopyEszipScripts(ctx context.Context, fsys afero.Fs) (string, error) {
+	denoPath, err := GetDenoPath()
+	if err != nil {
+		return "", err
+	}
+
+	denoDirPath := filepath.Dir(denoPath)
+	scriptDirPath := filepath.Join(denoDirPath, "eszip")
+	buildScriptPath := filepath.Join(scriptDirPath, "build.ts")
+
+	// make the script directory if not exist
+	if err := MkdirIfNotExistFS(fsys, scriptDirPath); err != nil {
+		return "", err
+	}
+
+	// check if the build script should be copied
+	modified, err := isBuildScriptModified(fsys, buildScriptPath)
+	if err != nil {
+		return "", err
+	}
+	if !modified {
+		return buildScriptPath, nil
+	}
+
+	// copy embed files to script directory
+	err = fs.WalkDir(eszipEmbedDir, "eszip", func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+
+		// skip copying the directory
+		if d.IsDir() {
+			return nil
+		}
+
+		contents, err := fs.ReadFile(eszipEmbedDir, path)
+		if err != nil {
+			return err
+		}
+
+		if err := afero.WriteFile(fsys, filepath.Join(denoDirPath, path), contents, 0666); err != nil {
+			return err
+		}
+
+		return nil
+	})
+
+	if err != nil {
+		return "", err
+	}
+
+	return filepath.Join(buildScriptPath), nil
+}
+
 func LoadAccessToken() (string, error) {
 	return LoadAccessTokenFS(afero.NewOsFs())
 }


Test patch:
diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go
index 251942d3..2dd05d46 100644
--- a/internal/functions/deploy/deploy_test.go
+++ b/internal/functions/deploy/deploy_test.go
@@ -9,6 +9,7 @@ import (
 	"log"
 	"net/http"
 	"os"
+	"strings"
 	"testing"
 
 	"github.com/spf13/afero"
@@ -22,7 +23,7 @@ import (
 
 func TestMain(m *testing.M) {
 	// Setup fake deno binary
-	if len(os.Args) > 1 && (os.Args[1] == "bundle" || os.Args[1] == "upgrade") {
+	if len(os.Args) > 1 && (os.Args[1] == "bundle" || os.Args[1] == "upgrade" || os.Args[1] == "run") {
 		msg := os.Getenv("TEST_DENO_ERROR")
 		if msg != "" {
 			fmt.Fprintln(os.Stderr, msg)
@@ -40,7 +41,7 @@ func TestMain(m *testing.M) {
 }
 
 func TestDeployCommand(t *testing.T) {
-	t.Run("deploys new function", func(t *testing.T) {
+	t.Run("deploys new function (legacy bundle)", func(t *testing.T) {
 		const slug = "test-func"
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
@@ -62,12 +63,68 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusCreated).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, project, false, fsys))
+		assert.NoError(t, Run(context.Background(), slug, project, false, true, fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
-	t.Run("updates deployed function", func(t *testing.T) {
+	t.Run("deploys new function (ESZIP)", func(t *testing.T) {
+		const slug = "test-func"
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// Setup valid access token
+		token := apitest.RandomAccessToken(t)
+		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
+		// Setup valid deno path
+		_, err := fsys.Create(utils.DenoPathOverride)
+		require.NoError(t, err)
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New("https://api.supabase.io").
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusNotFound)
+		gock.New("https://api.supabase.io").
+			Post("/v1/projects/" + project + "/functions").
+			Reply(http.StatusCreated).
+			JSON(api.FunctionResponse{Id: "1"})
+		// Run test
+		assert.NoError(t, Run(context.Background(), slug, project, false, false, fsys))
+		// Validate api
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	t.Run("updates deployed function (legacy bundle)", func(t *testing.T) {
+		const slug = "test-func"
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644))
+		// Setup valid access token
+		token := apitest.RandomAccessToken(t)
+		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
+		// Setup valid deno path
+		_, err := fsys.Create(utils.DenoPathOverride)
+		require.NoError(t, err)
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New("https://api.supabase.io").
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusOK).
+			JSON(api.FunctionResponse{Id: "1"})
+		gock.New("https://api.supabase.io").
+			Patch("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusOK).
+			JSON(api.FunctionResponse{Id: "1"})
+		// Run test
+		assert.NoError(t, Run(context.Background(), slug, "", true, true, fsys))
+		// Validate api
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	t.Run("updates deployed function (ESZIP)", func(t *testing.T) {
 		const slug = "test-func"
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
@@ -91,7 +148,7 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusOK).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, "", true, fsys))
+		assert.NoError(t, Run(context.Background(), slug, "", true, false, fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -102,7 +159,7 @@ func TestDeployCommand(t *testing.T) {
 		// Setup invalid project ref
 		require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte("test-project"), 0644))
 		// Run test
-		err := Run(context.Background(), "test-func", "", false, fsys)
+		err := Run(context.Background(), "test-func", "", false, true, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Invalid project ref format.")
 	})
@@ -111,7 +168,7 @@ func TestDeployCommand(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		// Run test
-		err := Run(context.Background(), "test-func", "test-project", false, fsys)
+		err := Run(context.Background(), "test-func", "test-project", false, true, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Invalid project ref format.")
 	})
@@ -122,7 +179,7 @@ func TestDeployCommand(t *testing.T) {
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Run test
-		err := Run(context.Background(), "@", project, false, fsys)
+		err := Run(context.Background(), "@", project, false, true, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Invalid Function name.")
 	})
@@ -133,7 +190,7 @@ func TestDeployCommand(t *testing.T) {
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Run test
-		err := Run(context.Background(), "test-func", project, false, fsys)
+		err := Run(context.Background(), "test-func", project, false, true, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "operation not permitted")
 	})
@@ -159,11 +216,37 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusOK).
 			Body(&body)
 		// Run test
-		err = Run(context.Background(), "test-func", project, false, fsys)
+		err = Run(context.Background(), "test-func", project, false, true, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Error bundling function: exit status 1\nbundle failed\n")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
+
+	t.Run("throws error on ESZIP failure", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// Setup deno error
+		t.Setenv("TEST_DENO_ERROR", "eszip failed")
+		var body bytes.Buffer
+		archive := zip.NewWriter(&body)
+		w, err := archive.Create("deno")
+		require.NoError(t, err)
+		_, err = w.Write([]byte("binary"))
+		require.NoError(t, err)
+		require.NoError(t, archive.Close())
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New("https://github.com").
+			Get("/denoland/deno/releases/latest/download/").
+			Reply(http.StatusOK).
+			Body(&body)
+
+		err = Run(context.Background(), "test-func", project, false, false, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Error bundling function: exit status 1\neszip failed\n")
+	})
 }
 
 func TestDeployFunction(t *testing.T) {
@@ -181,7 +264,7 @@ func TestDeployFunction(t *testing.T) {
 			Get("/v1/projects/" + project + "/functions/" + slug).
 			ReplyError(errors.New("network error"))
 		// Run test
-		err := deployFunction(context.Background(), project, slug, "body", true)
+		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
 		// Check error
 		assert.ErrorContains(t, err, "network error")
 	})
@@ -193,7 +276,7 @@ func TestDeployFunction(t *testing.T) {
 			Get("/v1/projects/" + project + "/functions/" + slug).
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := deployFunction(context.Background(), project, slug, "body", true)
+		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
 		// Check error
 		assert.ErrorContains(t, err, "Unexpected error deploying Function:")
 	})
@@ -208,7 +291,7 @@ func TestDeployFunction(t *testing.T) {
 			Post("/v1/projects/" + project + "/functions").
 			ReplyError(errors.New("network error"))
 		// Run test
-		err := deployFunction(context.Background(), project, slug, "body", true)
+		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
 		// Check error
 		assert.ErrorContains(t, err, "network error")
 	})
@@ -223,7 +306,7 @@ func TestDeployFunction(t *testing.T) {
 			Post("/v1/projects/" + project + "/functions").
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := deployFunction(context.Background(), project, slug, "body", true)
+		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
 		// Check error
 		assert.ErrorContains(t, err, "Failed to create a new Function on the Supabase project:")
 	})
@@ -239,7 +322,7 @@ func TestDeployFunction(t *testing.T) {
 			Patch("/v1/projects/" + project + "/functions/" + slug).
 			ReplyError(errors.New("network error"))
 		// Run test
-		err := deployFunction(context.Background(), project, slug, "body", true)
+		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
 		// Check error
 		assert.ErrorContains(t, err, "network error")
 	})
@@ -255,7 +338,7 @@ func TestDeployFunction(t *testing.T) {
 			Patch("/v1/projects/" + project + "/functions/" + slug).
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := deployFunction(context.Background(), project, slug, "body", true)
+		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
 		// Check error
 		assert.ErrorContains(t, err, "Failed to update an existing Function's body on the Supabase project:")
 	})