Repository: supabase/cli
Pull request #464 (2022-09-16 11:02:36)

chore: add tests for function serve
## What kind of change does this PR introduce?

tests

## What is the new behavior?

- use the same service role key and anon key from template
- moves login and project ref prompt to cmd
- tests for specific error string
- increase coverage of function deploy

## Additional context

Add any other context or screenshots.


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

* **0** of **39**   **(0.0%)**  changed or added relevant lines in **3** files are covered.
* **1** unchanged line in **1** file lost coverage.
* Overall coverage increased (+**0.007%**) to **36.404%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [cmd/functions.go](https://coveralls.io/builds/52531534/source?filename=cmd%2Ffunctions.go#L81) | 0 | 2 | 0.0%
| [internal/start/start.go](https://coveralls.io/builds/52531534/source?filename=internal%2Fstart%2Fstart.go#L509) | 0 | 9 | 0.0%
| [internal/functions/serve/serve.go](https://coveralls.io/builds/52531534/source?filename=internal%2Ffunctions%2Fserve%2Fserve.go#L24) | 0 | 28 | 0.0%
<!-- | **Total:** | **0** | **39** | **0.0%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/functions/serve/serve.go](https://coveralls.io/builds/52531534/source?filename=internal%2Ffunctions%2Fserve%2Fserve.go#L99) | 1 | 0% |
<!-- | **Total:** | **1** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/52531534/badge)](https://coveralls.io/builds/52531534) |
| :-- | --: |
| Change from base [Build 3067340254](https://coveralls.io/builds/52531469): |  0.007% |
| Covered Lines: | 1798 |
| Relevant Lines: | 4939 |

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


Patch:
diff --git a/cmd/functions.go b/cmd/functions.go
index 69e338ea..0b6b003f 100644
--- a/cmd/functions.go
+++ b/cmd/functions.go
@@ -1,8 +1,12 @@
 package cmd
 
 import (
+	"bufio"
+	"errors"
+	"fmt"
 	"os"
 	"os/signal"
+	"strings"
 
 	"github.com/spf13/afero"
 	"github.com/spf13/cobra"
@@ -10,6 +14,8 @@ import (
 	"github.com/supabase/cli/internal/functions/deploy"
 	"github.com/supabase/cli/internal/functions/new"
 	"github.com/supabase/cli/internal/functions/serve"
+	"github.com/supabase/cli/internal/login"
+	"github.com/supabase/cli/internal/utils"
 )
 
 var (
@@ -18,39 +24,40 @@ var (
 		Short: "Manage Supabase Edge functions",
 	}
 
+	projectRef string
+
 	functionsDeleteCmd = &cobra.Command{
 		Use:   "delete <Function name>",
 		Short: "Delete a Function from Supabase",
 		Long:  "Delete a Function from the linked Supabase project. This does NOT remove the Function locally.",
 		Args:  cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
-			projectRef, err := cmd.Flags().GetString("project-ref")
-			if err != nil {
+			fsys := afero.NewOsFs()
+			if err := PromptLogin(fsys); err != nil {
 				return err
 			}
-
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
-			return delete.Run(ctx, args[0], projectRef, afero.NewOsFs())
+			return delete.Run(ctx, args[0], projectRef, fsys)
 		},
 	}
 
+	noVerifyJWT bool
+
 	functionsDeployCmd = &cobra.Command{
 		Use:   "deploy <Function name>",
 		Short: "Deploy a Function to Supabase",
 		Long:  "Deploy a Function to the linked Supabase project.",
 		Args:  cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
-			noVerifyJWT, err := cmd.Flags().GetBool("no-verify-jwt")
-			if err != nil {
+			fsys := afero.NewOsFs()
+			if err := PromptLogin(fsys); err != nil {
 				return err
 			}
-			projectRef, err := cmd.Flags().GetString("project-ref")
-			if err != nil {
+			if err := PromptProjectRef(fsys); err != nil {
 				return err
 			}
-
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
-			return deploy.Run(ctx, args[0], projectRef, !noVerifyJWT, afero.NewOsFs())
+			return deploy.Run(ctx, args[0], projectRef, !noVerifyJWT, fsys)
 		},
 	}
 
@@ -64,34 +71,59 @@ var (
 		},
 	}
 
+	envFilePath string
+
 	functionsServeCmd = &cobra.Command{
 		Use:   "serve <Function name>",
 		Short: "Serve a Function locally",
 		Args:  cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
-			envFilePath, err := cmd.Flags().GetString("env-file")
-			if err != nil {
-				return err
-			}
-			noVerifyJWT, err := cmd.Flags().GetBool("no-verify-jwt")
-			if err != nil {
-				return err
-			}
-
-			return serve.Run(args[0], envFilePath, !noVerifyJWT)
+			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
+			return serve.Run(ctx, args[0], envFilePath, !noVerifyJWT, afero.NewOsFs())
 		},
 	}
 )
 
 func init() {
-	functionsDeleteCmd.Flags().String("project-ref", "", "Project ref of the Supabase project.")
-	functionsDeployCmd.Flags().Bool("no-verify-jwt", false, "Disable JWT verification for the Function.")
-	functionsDeployCmd.Flags().String("project-ref", "", "Project ref of the Supabase project.")
-	functionsServeCmd.Flags().Bool("no-verify-jwt", false, "Disable JWT verification for the Function.")
-	functionsServeCmd.Flags().String("env-file", "", "Path to an env file to be populated to the Function environment.")
+	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.")
+	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)
 	functionsCmd.AddCommand(functionsDeployCmd)
 	functionsCmd.AddCommand(functionsNewCmd)
 	functionsCmd.AddCommand(functionsServeCmd)
 	rootCmd.AddCommand(functionsCmd)
 }
+
+func PromptLogin(fsys afero.Fs) error {
+	if _, err := utils.LoadAccessTokenFS(fsys); err == nil {
+		return nil
+	} else if strings.HasPrefix(err.Error(), "Access token not provided. Supply an access token by running") {
+		return login.Run(os.Stdin, fsys)
+	} else {
+		return err
+	}
+}
+
+func PromptProjectRef(fsys afero.Fs) error {
+	if len(projectRef) > 0 {
+		return nil
+	} else if err := utils.AssertIsLinkedFS(fsys); err == nil {
+		return nil
+	} else if strings.HasPrefix(err.Error(), "Cannot find project ref. Have you run") {
+		fmt.Printf(`You can find your project ref from the project's dashboard home page, e.g. %s/project/<project-ref>.
+Enter your project ref: `, utils.GetSupabaseDashboardURL())
+
+		scanner := bufio.NewScanner(os.Stdin)
+		if !scanner.Scan() {
+			return errors.New("Cancelled " + utils.Aqua("supabase functions deploy") + ".")
+		}
+
+		projectRef = strings.TrimSpace(scanner.Text())
+		return nil
+	} else {
+		return err
+	}
+}
diff --git a/internal/functions/delete/delete.go b/internal/functions/delete/delete.go
index 9777b2ab..da934558 100644
--- a/internal/functions/delete/delete.go
+++ b/internal/functions/delete/delete.go
@@ -5,11 +5,8 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"os"
-	"strings"
 
 	"github.com/spf13/afero"
-	"github.com/supabase/cli/internal/login"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/pkg/api"
 )
@@ -18,16 +15,6 @@ func Run(ctx context.Context, slug string, projectRefArg string, fsys afero.Fs)
 	// 1. Sanity checks.
 	projectRef := projectRefArg
 	{
-		if err := utils.AssertSupabaseCliIsSetUpFS(fsys); err != nil {
-			return err
-		}
-		if _, err := utils.LoadAccessTokenFS(fsys); err != nil && strings.HasPrefix(err.Error(), "Access token not provided. Supply an access token by running") {
-			if err := login.Run(os.Stdin, fsys); err != nil {
-				return err
-			}
-		} else if err != nil {
-			return err
-		}
 		if len(projectRefArg) == 0 {
 			ref, err := utils.LoadProjectRef(fsys)
 			if err != nil {
diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go
index 26b9844c..fbae95e0 100644
--- a/internal/functions/deploy/deploy.go
+++ b/internal/functions/deploy/deploy.go
@@ -1,7 +1,6 @@
 package deploy
 
 import (
-	"bufio"
 	"bytes"
 	"context"
 	"errors"
@@ -10,10 +9,8 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
-	"strings"
 
 	"github.com/spf13/afero"
-	"github.com/supabase/cli/internal/login"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/pkg/api"
 )
@@ -22,44 +19,19 @@ func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool,
 	// 1. Sanity checks.
 	projectRef := projectRefArg
 	{
-		if _, err := utils.LoadAccessTokenFS(fsys); err != nil && strings.HasPrefix(err.Error(), "Access token not provided. Supply an access token by running") {
-			if err := login.Run(os.Stdin, fsys); err != nil {
-				return err
-			}
-		} else if err != nil {
-			return err
-		}
 		if len(projectRefArg) == 0 {
-			if err := utils.AssertIsLinkedFS(fsys); err != nil && strings.HasPrefix(err.Error(), "Cannot find project ref. Have you run") {
-				fmt.Printf(`You can find your project ref from the project's dashboard home page, e.g. %s/project/<project-ref>.
-Enter your project ref: `, utils.GetSupabaseDashboardURL())
-
-				scanner := bufio.NewScanner(os.Stdin)
-				if !scanner.Scan() {
-					fmt.Println("Cancelled " + utils.Aqua("supabase functions deploy") + ".")
-					return nil
-				}
-
-				projectRef = strings.TrimSpace(scanner.Text())
-			} else if err != nil {
+			ref, err := utils.LoadProjectRef(fsys)
+			if err != nil {
 				return err
 			}
-		}
-		if !utils.ProjectRefPattern.MatchString(projectRef) {
+			projectRef = ref
+		} else if !utils.ProjectRefPattern.MatchString(projectRef) {
 			return errors.New("Invalid project ref format. Must be like `abcdefghijklmnopqrst`.")
 		}
-		if len(projectRefArg) == 0 {
-			if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(utils.ProjectRefPath)); err != nil {
-				return err
-			}
-			if err := afero.WriteFile(fsys, utils.ProjectRefPath, []byte(projectRef), 0644); err != nil {
-				return err
-			}
-		}
-		if err := utils.InstallOrUpgradeDeno(ctx, fsys); err != nil {
+		if err := utils.ValidateFunctionSlug(slug); err != nil {
 			return err
 		}
-		if err := utils.ValidateFunctionSlug(slug); err != nil {
+		if err := utils.InstallOrUpgradeDeno(ctx, fsys); err != nil {
 			return err
 		}
 	}
@@ -96,6 +68,10 @@ Enter your project ref: `, utils.GetSupabaseDashboardURL())
 	}
 
 	// 3. Deploy new Function.
+	return deployFunction(ctx, projectRef, slug, newFunctionBody, verifyJWT)
+}
+
+func deployFunction(ctx context.Context, projectRef, slug, newFunctionBody string, verifyJWT bool) error {
 	var deployedFuncId string
 	{
 		resp, err := utils.GetSupabase().GetFunctionWithResponse(ctx, projectRef, slug, &api.GetFunctionParams{})
diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go
index b52ad65e..2389e669 100644
--- a/internal/functions/serve/serve.go
+++ b/internal/functions/serve/serve.go
@@ -4,60 +4,45 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"io"
 	"os"
-	"os/signal"
+	"path/filepath"
 	"strconv"
 	"strings"
-	"syscall"
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/pkg/stdcopy"
 	"github.com/joho/godotenv"
+	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/utils"
 )
 
-var ctx = context.Background()
+const (
+	relayFuncDir = "/home/deno/functions"
+)
 
-func Run(slug string, envFilePath string, verifyJWT bool) error {
+func Run(ctx context.Context, slug string, envFilePath string, verifyJWT bool, fsys afero.Fs) error {
 	// 1. Sanity checks.
 	{
-		if err := utils.AssertSupabaseCliIsSetUp(); err != nil {
-			return err
-		}
-		if err := utils.AssertDockerIsRunning(); err != nil {
+		if err := utils.AssertSupabaseCliIsSetUpFS(fsys); err != nil {
 			return err
 		}
-		if err := utils.LoadConfig(); err != nil {
+		if err := utils.LoadConfigFS(fsys); err != nil {
 			return err
 		}
-		if err := utils.AssertSupabaseStartIsRunning(); err != nil {
+		if err := utils.AssertSupabaseDbIsRunning(); err != nil {
 			return err
 		}
 		if err := utils.ValidateFunctionSlug(slug); err != nil {
 			return err
 		}
 		if envFilePath != "" {
-			if _, err := os.ReadFile(envFilePath); err != nil {
+			if _, err := fsys.Stat(envFilePath); err != nil {
 				return fmt.Errorf("Failed to read env file: %w", err)
 			}
 		}
 	}
 
-	// 2. Stop on SIGINT/SIGTERM.
-	{
-		termCh := make(chan os.Signal, 1)
-		signal.Notify(termCh, syscall.SIGINT, syscall.SIGTERM)
-		go func() {
-			<-termCh
-			_ = utils.Docker.ContainerRemove(ctx, utils.DenoRelayId, types.ContainerRemoveOptions{
-				RemoveVolumes: true,
-				Force:         true,
-			})
-		}()
-	}
-
 	// 3. Start relay.
 	{
 		_ = utils.Docker.ContainerRemove(ctx, utils.DenoRelayId, types.ContainerRemoveOptions{
@@ -66,7 +51,7 @@ func Run(slug string, envFilePath string, verifyJWT bool) error {
 		})
 
 		env := []string{
-			"JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long",
+			"JWT_SECRET=" + utils.JWTSecret,
 			"DENO_ORIGIN=http://localhost:8000",
 		}
 		if verifyJWT {
@@ -92,41 +77,47 @@ func Run(slug string, envFilePath string, verifyJWT bool) error {
 				},
 			},
 			&container.HostConfig{
-				Binds:       []string{cwd + "/supabase/functions:/home/deno/functions:ro,z"},
+				Binds:       []string{filepath.Join(cwd, utils.FunctionsDir) + ":" + relayFuncDir + ":ro,z"},
 				NetworkMode: container.NetworkMode(utils.NetId),
 			},
 		); err != nil {
 			return err
 		}
+
+		go func() {
+			<-ctx.Done()
+			if ctx.Err() != nil {
+				if err := utils.Docker.ContainerRemove(context.Background(), utils.DenoRelayId, types.ContainerRemoveOptions{
+					RemoveVolumes: true,
+					Force:         true,
+				}); err != nil {
+					fmt.Fprintln(os.Stderr, "Failed to remove container:", utils.DenoRelayId, err)
+				}
+			}
+		}()
 	}
 
 	// 4. Start Function.
-	{
-		fmt.Println("Starting " + utils.Bold("supabase/functions/"+slug))
-		out, err := utils.DockerExec(ctx, utils.DenoRelayId, []string{
-			"deno", "cache", "/home/deno/functions/" + slug + "/index.ts",
-		})
-		if err != nil {
-			return err
-		}
-		if _, err := stdcopy.StdCopy(io.Discard, io.Discard, out); err != nil {
-			return err
-		}
+	localFuncDir := filepath.Join(utils.FunctionsDir, slug)
+	dockerFuncPath := relayFuncDir + "/" + slug + "/index.ts"
+	fmt.Println("Starting " + utils.Bold(localFuncDir))
+	if _, err := utils.DockerExecOnce(ctx, utils.DenoRelayId, nil, []string{
+		"deno", "cache", dockerFuncPath,
+	}); err != nil {
+		return err
 	}
 
 	{
-		fmt.Println("Serving " + utils.Bold("supabase/functions/"+slug))
+		fmt.Println("Serving " + utils.Bold(localFuncDir))
 
 		env := []string{
 			"SUPABASE_URL=http://" + utils.KongId + ":8000",
-			"SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs",
-			"SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU",
+			"SUPABASE_ANON_KEY=" + utils.AnonKey,
+			"SUPABASE_SERVICE_ROLE_KEY=" + utils.ServiceRoleKey,
 			"SUPABASE_DB_URL=postgresql://postgres:postgres@localhost:" + strconv.FormatUint(uint64(utils.Config.Db.Port), 10) + "/postgres",
 		}
 
-		if envFilePath == "" {
-			// skip
-		} else {
+		if envFilePath != "" {
 			envMap, err := godotenv.Read(envFilePath)
 			if err != nil {
 				return err
@@ -145,7 +136,7 @@ func Run(slug string, envFilePath string, verifyJWT bool) error {
 			types.ExecConfig{
 				Env: env,
 				Cmd: []string{
-					"deno", "run", "--no-check=remote", "--allow-all", "--watch", "--no-clear-screen", "/home/deno/functions/" + slug + "/index.ts",
+					"deno", "run", "--no-check=remote", "--allow-all", "--watch", "--no-clear-screen", dockerFuncPath,
 				},
 				AttachStderr: true,
 				AttachStdout: true,
@@ -165,6 +156,6 @@ func Run(slug string, envFilePath string, verifyJWT bool) error {
 		}
 	}
 
-	fmt.Println("Stopped serving " + utils.Bold("supabase/functions/"+slug))
+	fmt.Println("Stopped serving " + utils.Bold(localFuncDir))
 	return nil
 }
diff --git a/internal/start/start.go b/internal/start/start.go
index 01cb6ffe..cbf56968 100644
--- a/internal/start/start.go
+++ b/internal/start/start.go
@@ -506,7 +506,11 @@ EOSQL
 	// Start Kong.
 	{
 		var kongConfigBuf bytes.Buffer
-		if err := kongConfigTemplate.Execute(&kongConfigBuf, struct{ ProjectId string }{ProjectId: utils.Config.ProjectId}); err != nil {
+		if err := kongConfigTemplate.Execute(&kongConfigBuf, struct{ ProjectId, AnonKey, ServiceRoleKey string }{
+			ProjectId:      utils.Config.ProjectId,
+			AnonKey:        utils.AnonKey,
+			ServiceRoleKey: utils.ServiceRoleKey,
+		}); err != nil {
 			return err
 		}
 
@@ -559,7 +563,7 @@ EOF
 			"GOTRUE_JWT_AUD=authenticated",
 			"GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated",
 			fmt.Sprintf("GOTRUE_JWT_EXP=%v", utils.Config.Auth.JwtExpiry),
-			"GOTRUE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long",
+			"GOTRUE_JWT_SECRET=" + utils.JWTSecret,
 
 			fmt.Sprintf("GOTRUE_EXTERNAL_EMAIL_ENABLED=%v", *utils.Config.Auth.Email.EnableSignup),
 			fmt.Sprintf("GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=%v", *utils.Config.Auth.Email.DoubleConfirmChanges),
@@ -652,7 +656,7 @@ EOF
 				"DB_SSL=false",
 				"SLOT_NAME=supabase_realtime",
 				"TEMPORARY_SLOT=true",
-				"JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long",
+				"JWT_SECRET=" + utils.JWTSecret,
 				"SECURE_CHANNELS=true",
 				"REPLICATION_MODE=RLS",
 				"REPLICATION_POLL_INTERVAL=100",
@@ -680,7 +684,7 @@ EOF
 				"PGRST_DB_SCHEMAS=" + strings.Join(append([]string{"public", "storage", "graphql_public"}, utils.Config.Api.Schemas...), ","),
 				"PGRST_DB_EXTRA_SEARCH_PATH=" + strings.Join(append([]string{"public"}, utils.Config.Api.ExtraSearchPath...), ","),
 				"PGRST_DB_ANON_ROLE=anon",
-				"PGRST_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long",
+				"PGRST_JWT_SECRET=" + utils.JWTSecret,
 			},
 			Labels: map[string]string{
 				"com.supabase.cli.project":   utils.Config.ProjectId,
@@ -702,10 +706,10 @@ EOF
 		&container.Config{
 			Image: utils.GetRegistryImageUrl(utils.StorageImage),
 			Env: []string{
-				"ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs",
-				"SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU",
+				"ANON_KEY=" + utils.AnonKey,
+				"SERVICE_KEY=" + utils.ServiceRoleKey,
 				"POSTGREST_URL=http://" + utils.RestId + ":3000",
-				"PGRST_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long",
+				"PGRST_JWT_SECRET=" + utils.JWTSecret,
 				"DATABASE_URL=postgresql://supabase_storage_admin:postgres@" + utils.DbId + ":5432/postgres",
 				"FILE_SIZE_LIMIT=52428800",
 				"STORAGE_BACKEND=file",
@@ -783,8 +787,8 @@ EOF
 
 				"SUPABASE_URL=http://" + utils.KongId + ":8000",
 				fmt.Sprintf("SUPABASE_REST_URL=http://localhost:%v/rest/v1/", utils.Config.Api.Port),
-				"SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs",
-				"SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU",
+				"SUPABASE_ANON_KEY=" + utils.AnonKey,
+				"SUPABASE_SERVICE_KEY=" + utils.ServiceRoleKey,
 			},
 			Labels: map[string]string{
 				"com.supabase.cli.project":   utils.Config.ProjectId,
diff --git a/internal/start/templates/kong_config b/internal/start/templates/kong_config
index 1f536401..12c2b723 100644
--- a/internal/start/templates/kong_config
+++ b/internal/start/templates/kong_config
@@ -116,7 +116,7 @@ services:
 consumers:
   - username: anon
     keyauth_credentials:
-      - key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs
+      - key: {{ .AnonKey }}
   - username: service_role
     keyauth_credentials:
-      - key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU
+      - key: {{ .ServiceRoleKey }}
diff --git a/internal/utils/misc.go b/internal/utils/misc.go
index e7da746f..36022c3b 100644
--- a/internal/utils/misc.go
+++ b/internal/utils/misc.go
@@ -53,6 +53,7 @@ DO 'BEGIN WHILE (
 ) > 0 LOOP END LOOP; END';`
 	AnonKey        = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs"
 	ServiceRoleKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU"
+	JWTSecret      = "super-secret-jwt-token-with-at-least-32-characters-long"
 
 	ConfigPath     = "supabase/config.toml"
 	ProjectRefPath = "supabase/.temp/project-ref"


Test patch:
diff --git a/internal/db/lint/lint_test.go b/internal/db/lint/lint_test.go
index 4f175d79..1dfd0693 100644
--- a/internal/db/lint/lint_test.go
+++ b/internal/db/lint/lint_test.go
@@ -14,6 +14,7 @@ import (
 	"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/testing/pgtest"
 	"github.com/supabase/cli/internal/utils"
 	"gopkg.in/h2non/gock.v1"
@@ -64,6 +65,8 @@ func TestLintCommand(t *testing.T) {
 		Query("rollback").Reply("ROLLBACK")
 	// Run test
 	assert.NoError(t, Run(context.Background(), []string{"public"}, "warning", fsys, conn.Intercept))
+	// Validate api
+	assert.Empty(t, apitest.ListUnmatchedRequests())
 }
 
 func TestLintDatabase(t *testing.T) {
diff --git a/internal/functions/delete/delete_test.go b/internal/functions/delete/delete_test.go
index 5650a7d7..1667e7ef 100644
--- a/internal/functions/delete/delete_test.go
+++ b/internal/functions/delete/delete_test.go
@@ -19,7 +19,6 @@ func TestDeleteCommand(t *testing.T) {
 	t.Run("deletes function from project", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		require.NoError(t, utils.WriteConfig(fsys, false))
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644))
@@ -37,38 +36,44 @@ func TestDeleteCommand(t *testing.T) {
 			Reply(http.StatusOK)
 		// Run test
 		assert.NoError(t, Run(context.Background(), "test-func", "", fsys))
+		// Validate api
+		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
-	t.Run("throws error if uninitialised", func(t *testing.T) {
+	t.Run("throws error on malformed ref", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
+		// Setup invalid project ref
+		require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte("test-project"), 0644))
 		// Run test
-		assert.Error(t, Run(context.Background(), "test-func", "test-project", fsys))
+		err := Run(context.Background(), "test-func", "", fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Invalid project ref format.")
 	})
 
-	t.Run("throws error on malformed ref", func(t *testing.T) {
+	t.Run("throws error on malformed ref arg", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		require.NoError(t, utils.WriteConfig(fsys, false))
 		// Run test
-		assert.Error(t, Run(context.Background(), "test-func", "test-project", fsys))
-		assert.Error(t, Run(context.Background(), "test-func", "", fsys))
+		err := Run(context.Background(), "test-func", "test-project", fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Invalid project ref format.")
 	})
 
 	t.Run("throws error on malformed slug", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		require.NoError(t, utils.WriteConfig(fsys, false))
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Run test
-		assert.Error(t, Run(context.Background(), "@", project, fsys))
+		err := Run(context.Background(), "@", project, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Invalid Function name.")
 	})
 
 	t.Run("throws error on network failure", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		require.NoError(t, utils.WriteConfig(fsys, false))
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Setup valid access token
@@ -80,13 +85,15 @@ func TestDeleteCommand(t *testing.T) {
 			Get("/v1/projects/" + project + "/functions/test-func").
 			ReplyError(errors.New("network error"))
 		// Run test
-		assert.Error(t, Run(context.Background(), "test-func", project, fsys))
+		err := Run(context.Background(), "test-func", project, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "network error")
+		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
 	t.Run("throws error on service unavailable", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		require.NoError(t, utils.WriteConfig(fsys, false))
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Setup valid access token
@@ -98,13 +105,15 @@ func TestDeleteCommand(t *testing.T) {
 			Get("/v1/projects/" + project + "/functions/test-func").
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		assert.Error(t, Run(context.Background(), "test-func", project, fsys))
+		err := Run(context.Background(), "test-func", project, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Unexpected error deleting Function:")
+		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
 	t.Run("throws error on missing function", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		require.NoError(t, utils.WriteConfig(fsys, false))
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Setup valid access token
@@ -116,13 +125,15 @@ func TestDeleteCommand(t *testing.T) {
 			Get("/v1/projects/" + project + "/functions/test-func").
 			Reply(http.StatusNotFound)
 		// Run test
-		assert.Error(t, Run(context.Background(), "test-func", project, fsys))
+		err := Run(context.Background(), "test-func", project, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Function test-func does not exist on the Supabase project.")
+		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
 	t.Run("throws error on delete failure", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		require.NoError(t, utils.WriteConfig(fsys, false))
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Setup valid access token
@@ -138,13 +149,15 @@ func TestDeleteCommand(t *testing.T) {
 			Delete("/v1/projects/" + project + "/functions/test-func").
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		assert.Error(t, Run(context.Background(), "test-func", project, fsys))
+		err := Run(context.Background(), "test-func", project, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Failed to delete Function test-func on the Supabase project:")
+		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
 	t.Run("throws error on delete network failure", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		require.NoError(t, utils.WriteConfig(fsys, false))
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Setup valid access token
@@ -160,6 +173,9 @@ func TestDeleteCommand(t *testing.T) {
 			Delete("/v1/projects/" + project + "/functions/test-func").
 			ReplyError(errors.New("network error"))
 		// Run test
-		assert.Error(t, Run(context.Background(), "test-func", project, fsys))
+		err := Run(context.Background(), "test-func", project, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "network error")
+		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 }
diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go
index 08d208c5..251942d3 100644
--- a/internal/functions/deploy/deploy_test.go
+++ b/internal/functions/deploy/deploy_test.go
@@ -1,7 +1,11 @@
 package deploy
 
 import (
+	"archive/zip"
+	"bytes"
 	"context"
+	"errors"
+	"fmt"
 	"log"
 	"net/http"
 	"os"
@@ -19,6 +23,11 @@ import (
 func TestMain(m *testing.M) {
 	// Setup fake deno binary
 	if len(os.Args) > 1 && (os.Args[1] == "bundle" || os.Args[1] == "upgrade") {
+		msg := os.Getenv("TEST_DENO_ERROR")
+		if msg != "" {
+			fmt.Fprintln(os.Stderr, msg)
+			os.Exit(1)
+		}
 		os.Exit(0)
 	}
 	denoPath, err := os.Executable()
@@ -32,6 +41,7 @@ func TestMain(m *testing.M) {
 
 func TestDeployCommand(t *testing.T) {
 	t.Run("deploys new function", func(t *testing.T) {
+		const slug = "test-func"
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		// Setup valid project ref
@@ -45,23 +55,25 @@ func TestDeployCommand(t *testing.T) {
 		// Setup mock api
 		defer gock.OffAll()
 		gock.New("https://api.supabase.io").
-			Get("/v1/projects/" + project + "/functions/test-func").
+			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(), "test-func", project, false, fsys))
+		assert.NoError(t, Run(context.Background(), slug, project, false, fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
 	t.Run("updates deployed function", 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))
@@ -71,16 +83,180 @@ func TestDeployCommand(t *testing.T) {
 		// Setup mock api
 		defer gock.OffAll()
 		gock.New("https://api.supabase.io").
-			Get("/v1/projects/" + project + "/functions/test-func").
+			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").
+			Patch("/v1/projects/" + project + "/functions/" + slug).
 			Reply(http.StatusOK).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), "test-func", project, false, fsys))
+		assert.NoError(t, Run(context.Background(), slug, "", true, fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
+
+	t.Run("throws error on malformed ref", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// 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)
+		// Check error
+		assert.ErrorContains(t, err, "Invalid project ref format.")
+	})
+
+	t.Run("throws error on malformed ref arg", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Run test
+		err := Run(context.Background(), "test-func", "test-project", false, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Invalid project ref format.")
+	})
+
+	t.Run("throws error on malformed slug", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// Run test
+		err := Run(context.Background(), "@", project, false, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Invalid Function name.")
+	})
+
+	t.Run("throws error on failure to install deno", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// Run test
+		err := Run(context.Background(), "test-func", project, false, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "operation not permitted")
+	})
+
+	t.Run("throws error on bundle 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", "bundle 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)
+		// Run test
+		err = Run(context.Background(), "test-func", project, false, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Error bundling function: exit status 1\nbundle failed\n")
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+}
+
+func TestDeployFunction(t *testing.T) {
+	const slug = "test-func"
+	// Setup valid project ref
+	project := apitest.RandomProjectRef()
+	// Setup valid access token
+	token := apitest.RandomAccessToken(t)
+	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
+
+	t.Run("throws error on network failure", func(t *testing.T) {
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New("https://api.supabase.io").
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			ReplyError(errors.New("network error"))
+		// Run test
+		err := deployFunction(context.Background(), project, slug, "body", true)
+		// 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("https://api.supabase.io").
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusServiceUnavailable)
+		// Run test
+		err := deployFunction(context.Background(), project, slug, "body", true)
+		// Check error
+		assert.ErrorContains(t, err, "Unexpected error deploying Function:")
+	})
+
+	t.Run("throws error on create failure", func(t *testing.T) {
+		// 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").
+			ReplyError(errors.New("network error"))
+		// Run test
+		err := deployFunction(context.Background(), project, slug, "body", true)
+		// Check error
+		assert.ErrorContains(t, err, "network error")
+	})
+
+	t.Run("throws error on create unavailable", func(t *testing.T) {
+		// 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.StatusServiceUnavailable)
+		// Run test
+		err := deployFunction(context.Background(), project, slug, "body", true)
+		// Check error
+		assert.ErrorContains(t, err, "Failed to create a new Function on the Supabase project:")
+	})
+
+	t.Run("throws error on update failure", func(t *testing.T) {
+		// 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).
+			ReplyError(errors.New("network error"))
+		// Run test
+		err := deployFunction(context.Background(), project, slug, "body", true)
+		// Check error
+		assert.ErrorContains(t, err, "network error")
+	})
+
+	t.Run("throws error on update unavailable", func(t *testing.T) {
+		// 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.StatusServiceUnavailable)
+		// Run test
+		err := deployFunction(context.Background(), project, slug, "body", true)
+		// Check error
+		assert.ErrorContains(t, err, "Failed to update an existing Function's body on the Supabase project:")
+	})
 }