github.com/supabase/cli@v1.168.1/internal/functions/deploy/deploy_test.go (about)

     1  package deploy
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/spf13/afero"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  	"github.com/supabase/cli/internal/testing/apitest"
    19  	"github.com/supabase/cli/internal/utils"
    20  	"github.com/supabase/cli/pkg/api"
    21  	"gopkg.in/h2non/gock.v1"
    22  )
    23  
    24  func TestDeployOne(t *testing.T) {
    25  	const slug = "test-func"
    26  	imageUrl := utils.GetRegistryImageUrl(utils.EdgeRuntimeImage)
    27  	utils.EdgeRuntimeId = "test-edge-runtime"
    28  	const containerId = "test-container"
    29  
    30  	t.Run("deploys new function (ESZIP)", func(t *testing.T) {
    31  		// Setup in-memory fs
    32  		fsys := afero.NewMemMapFs()
    33  
    34  		// Setup valid project ref
    35  		project := apitest.RandomProjectRef()
    36  		// Setup valid access token
    37  		token := apitest.RandomAccessToken(t)
    38  		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
    39  
    40  		// Setup mock api
    41  		defer gock.OffAll()
    42  		gock.New(utils.DefaultApiHost).
    43  			Get("/v1/projects/" + project + "/functions/" + slug).
    44  			Reply(http.StatusNotFound)
    45  		gock.New(utils.DefaultApiHost).
    46  			Post("/v1/projects/" + project + "/functions").
    47  			ParamPresent("import_map_path").
    48  			ParamPresent("entrypoint_path").
    49  			Reply(http.StatusCreated).
    50  			JSON(api.FunctionResponse{Id: "1"})
    51  
    52  		// Setup mock docker
    53  		require.NoError(t, apitest.MockDocker(utils.Docker))
    54  		apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
    55  		require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
    56  
    57  		// Setup output file
    58  		outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug))
    59  		require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
    60  
    61  		// Run test
    62  		noVerifyJWT := true
    63  		err := deployOne(context.Background(), slug, project, "", &noVerifyJWT, fsys)
    64  		// Check error
    65  		assert.NoError(t, err)
    66  		assert.Empty(t, apitest.ListUnmatchedRequests())
    67  	})
    68  
    69  	t.Run("updates deployed function (ESZIP)", func(t *testing.T) {
    70  		// Setup in-memory fs
    71  		fsys := afero.NewMemMapFs()
    72  		// Setup valid project ref
    73  		project := apitest.RandomProjectRef()
    74  		// Setup valid access token
    75  		token := apitest.RandomAccessToken(t)
    76  		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
    77  		// Setup valid deno path
    78  		_, err := fsys.Create(utils.DenoPathOverride)
    79  		require.NoError(t, err)
    80  		// Setup mock api
    81  		defer gock.OffAll()
    82  		gock.New(utils.DefaultApiHost).
    83  			Get("/v1/projects/" + project + "/functions/" + slug).
    84  			Reply(http.StatusOK).
    85  			JSON(api.FunctionResponse{Id: "1"})
    86  		gock.New(utils.DefaultApiHost).
    87  			Patch("/v1/projects/" + project + "/functions/" + slug).
    88  			Reply(http.StatusOK).
    89  			JSON(api.FunctionResponse{Id: "1"})
    90  
    91  		// Setup mock docker
    92  		require.NoError(t, apitest.MockDocker(utils.Docker))
    93  		apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
    94  		require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
    95  
    96  		// Setup output file
    97  		outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug))
    98  		require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
    99  
   100  		// Run test
   101  		err = deployOne(context.Background(), slug, project, "", nil, fsys)
   102  		// Check error
   103  		assert.NoError(t, err)
   104  		assert.Empty(t, apitest.ListUnmatchedRequests())
   105  	})
   106  
   107  	t.Run("throws error on missing import map", func(t *testing.T) {
   108  		// Setup in-memory fs
   109  		fsys := afero.NewMemMapFs()
   110  		// Setup valid project ref
   111  		project := apitest.RandomProjectRef()
   112  		// Run test
   113  		err := deployOne(context.Background(), slug, project, "import_map.json", nil, fsys)
   114  		// Check error
   115  		assert.ErrorIs(t, err, os.ErrNotExist)
   116  	})
   117  
   118  	t.Run("throws error on bundle failure", func(t *testing.T) {
   119  		// Setup in-memory fs
   120  		fsys := afero.NewMemMapFs()
   121  		// Setup valid project ref
   122  		project := apitest.RandomProjectRef()
   123  		// Setup deno error
   124  		t.Setenv("TEST_DENO_ERROR", "bundle failed")
   125  		var body bytes.Buffer
   126  		archive := zip.NewWriter(&body)
   127  		w, err := archive.Create("deno")
   128  		require.NoError(t, err)
   129  		_, err = w.Write([]byte("binary"))
   130  		require.NoError(t, err)
   131  		require.NoError(t, archive.Close())
   132  		// Setup mock api
   133  		defer gock.OffAll()
   134  		gock.New("https://github.com").
   135  			Get("/denoland/deno/releases/download/v" + utils.DenoVersion).
   136  			Reply(http.StatusOK).
   137  			Body(&body)
   138  
   139  		// Setup mock docker
   140  		require.NoError(t, apitest.MockDocker(utils.Docker))
   141  		apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
   142  		require.NoError(t, apitest.MockDockerLogsExitCode(utils.Docker, containerId, 1))
   143  
   144  		// Run test
   145  		err = deployOne(context.Background(), slug, project, "", nil, fsys)
   146  		// Check error
   147  		assert.ErrorContains(t, err, "error running container: exit 1")
   148  		assert.Empty(t, apitest.ListUnmatchedRequests())
   149  	})
   150  }
   151  
   152  func TestDeployAll(t *testing.T) {
   153  	const slug = "test-func"
   154  	imageUrl := utils.GetRegistryImageUrl(utils.EdgeRuntimeImage)
   155  	utils.EdgeRuntimeId = "test-edge-runtime"
   156  	const containerId = "test-container"
   157  
   158  	t.Run("deploys multiple functions", func(t *testing.T) {
   159  		functions := []string{slug, slug + "-2"}
   160  		// Setup in-memory fs
   161  		fsys := afero.NewMemMapFs()
   162  		// Setup valid project ref
   163  		project := apitest.RandomProjectRef()
   164  		// Setup valid access token
   165  		token := apitest.RandomAccessToken(t)
   166  		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   167  
   168  		// Setup mock api
   169  		defer gock.OffAll()
   170  		for i := range functions {
   171  			// Do not match slug to avoid flakey tests
   172  			gock.New(utils.DefaultApiHost).
   173  				Get("/v1/projects/" + project + "/functions/").
   174  				Reply(http.StatusNotFound)
   175  			gock.New(utils.DefaultApiHost).
   176  				Post("/v1/projects/" + project + "/functions").
   177  				Reply(http.StatusCreated).
   178  				JSON(api.FunctionResponse{Id: fmt.Sprintf("%d", i)})
   179  
   180  			// Setup mock docker
   181  			require.NoError(t, apitest.MockDocker(utils.Docker))
   182  			apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
   183  			require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
   184  		}
   185  
   186  		// Setup output file
   187  		for _, v := range functions {
   188  			outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", v))
   189  			require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
   190  		}
   191  
   192  		// Run test
   193  		noVerifyJWT := true
   194  		err := deployAll(context.Background(), functions, project, "", &noVerifyJWT, fsys)
   195  		// Check error
   196  		assert.NoError(t, err)
   197  		assert.Empty(t, apitest.ListUnmatchedRequests())
   198  	})
   199  
   200  	t.Run("throws error on failure to install deno", func(t *testing.T) {
   201  		// Setup in-memory fs
   202  		fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
   203  		// Setup valid project ref
   204  		project := apitest.RandomProjectRef()
   205  		// Run test
   206  		err := deployAll(context.Background(), []string{slug}, project, "", nil, fsys)
   207  		// Check error
   208  		assert.ErrorContains(t, err, "operation not permitted")
   209  	})
   210  
   211  	t.Run("throws error on copy failure", func(t *testing.T) {
   212  		// Setup in-memory fs
   213  		fsys := afero.NewMemMapFs()
   214  		// Setup valid project ref
   215  		project := apitest.RandomProjectRef()
   216  		// Setup valid deno path
   217  		_, err := fsys.Create(utils.DenoPathOverride)
   218  		require.NoError(t, err)
   219  		// Run test
   220  		err = deployAll(context.Background(), []string{slug}, project, "", nil, afero.NewReadOnlyFs(fsys))
   221  		// Check error
   222  		assert.ErrorContains(t, err, "operation not permitted")
   223  	})
   224  }
   225  
   226  func TestDeployCommand(t *testing.T) {
   227  	const slug = "test-func"
   228  	imageUrl := utils.GetRegistryImageUrl(utils.EdgeRuntimeImage)
   229  	utils.EdgeRuntimeId = "test-edge-runtime"
   230  	const containerId = "test-container"
   231  
   232  	t.Run("deploys multiple functions", func(t *testing.T) {
   233  		functions := []string{slug, slug + "-2"}
   234  		// Setup in-memory fs
   235  		fsys := afero.NewMemMapFs()
   236  		require.NoError(t, utils.WriteConfig(fsys, false))
   237  		// Setup valid project ref
   238  		project := apitest.RandomProjectRef()
   239  		// Setup valid access token
   240  		token := apitest.RandomAccessToken(t)
   241  		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   242  		// Setup valid deno path
   243  		_, err := fsys.Create(utils.DenoPathOverride)
   244  		require.NoError(t, err)
   245  
   246  		// Setup mock api
   247  		defer gock.OffAll()
   248  		for i := range functions {
   249  			// Do not match slug to avoid flakey tests
   250  			gock.New(utils.DefaultApiHost).
   251  				Get("/v1/projects/" + project + "/functions/").
   252  				Reply(http.StatusNotFound)
   253  			gock.New(utils.DefaultApiHost).
   254  				Post("/v1/projects/" + project + "/functions").
   255  				Reply(http.StatusCreated).
   256  				JSON(api.FunctionResponse{Id: fmt.Sprintf("%d", i)})
   257  
   258  				// Setup mock docker
   259  			require.NoError(t, apitest.MockDocker(utils.Docker))
   260  			apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
   261  			require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
   262  		}
   263  
   264  		// Setup output file
   265  		for _, v := range functions {
   266  			outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", v))
   267  			require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
   268  		}
   269  
   270  		// Run test
   271  		noVerifyJWT := true
   272  		err = Run(context.Background(), functions, project, &noVerifyJWT, "", fsys)
   273  		// Check error
   274  		assert.NoError(t, err)
   275  		assert.Empty(t, apitest.ListUnmatchedRequests())
   276  	})
   277  
   278  	t.Run("deploys functions from directory", func(t *testing.T) {
   279  		// Setup in-memory fs
   280  		fsys := afero.NewMemMapFs()
   281  		require.NoError(t, utils.WriteConfig(fsys, false))
   282  		importMapPath, err := filepath.Abs(utils.FallbackImportMapPath)
   283  		require.NoError(t, err)
   284  		require.NoError(t, afero.WriteFile(fsys, importMapPath, []byte("{}"), 0644))
   285  		// Setup function entrypoint
   286  		entrypointPath := filepath.Join(utils.FunctionsDir, slug, "index.ts")
   287  		require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644))
   288  		ignorePath := filepath.Join(utils.FunctionsDir, "_ignore", "index.ts")
   289  		require.NoError(t, afero.WriteFile(fsys, ignorePath, []byte{}, 0644))
   290  		// Setup valid project ref
   291  		project := apitest.RandomProjectRef()
   292  		// Setup valid access token
   293  		token := apitest.RandomAccessToken(t)
   294  		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   295  		// Setup valid deno path
   296  		_, err = fsys.Create(utils.DenoPathOverride)
   297  		require.NoError(t, err)
   298  		// Setup mock api
   299  		defer gock.OffAll()
   300  		gock.New(utils.DefaultApiHost).
   301  			Get("/v1/projects/" + project + "/functions/" + slug).
   302  			Reply(http.StatusNotFound)
   303  		gock.New(utils.DefaultApiHost).
   304  			Post("/v1/projects/"+project+"/functions").
   305  			MatchParam("slug", slug).
   306  			ParamPresent("import_map_path").
   307  			Reply(http.StatusCreated).
   308  			JSON(api.FunctionResponse{Id: "1"})
   309  
   310  		// Setup mock docker
   311  		require.NoError(t, apitest.MockDocker(utils.Docker))
   312  		apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
   313  		require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
   314  
   315  		// Setup output file
   316  		outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug))
   317  		require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
   318  
   319  		// Run test
   320  		err = Run(context.Background(), nil, project, nil, "", fsys)
   321  		// Check error
   322  		assert.NoError(t, err)
   323  		assert.Empty(t, apitest.ListUnmatchedRequests())
   324  	})
   325  
   326  	t.Run("throws error on malformed slug", func(t *testing.T) {
   327  		// Setup in-memory fs
   328  		fsys := afero.NewMemMapFs()
   329  		require.NoError(t, utils.WriteConfig(fsys, false))
   330  		// Run test
   331  		err := Run(context.Background(), []string{"_invalid"}, "", nil, "", fsys)
   332  		// Check error
   333  		assert.ErrorContains(t, err, "Invalid Function name.")
   334  	})
   335  
   336  	t.Run("throws error on empty functions", func(t *testing.T) {
   337  		// Setup in-memory fs
   338  		fsys := afero.NewMemMapFs()
   339  		require.NoError(t, utils.WriteConfig(fsys, false))
   340  		require.NoError(t, fsys.MkdirAll(utils.FunctionsDir, 0755))
   341  		// Run test
   342  		err := Run(context.Background(), nil, "", nil, "", fsys)
   343  		// Check error
   344  		assert.ErrorContains(t, err, "No Functions specified or found in supabase/functions")
   345  	})
   346  
   347  	t.Run("verify_jwt param falls back to config", func(t *testing.T) {
   348  		// Setup in-memory fs
   349  		fsys := afero.NewMemMapFs()
   350  		require.NoError(t, utils.WriteConfig(fsys, false))
   351  		f, err := fsys.OpenFile("supabase/config.toml", os.O_APPEND|os.O_WRONLY, 0600)
   352  		require.NoError(t, err)
   353  		_, err = f.WriteString(`
   354  [functions.` + slug + `]
   355  verify_jwt = false
   356  `)
   357  		require.NoError(t, err)
   358  		require.NoError(t, f.Close())
   359  		// Setup valid project ref
   360  		project := apitest.RandomProjectRef()
   361  		// Setup valid access token
   362  		token := apitest.RandomAccessToken(t)
   363  		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   364  		// Setup valid deno path
   365  		_, err = fsys.Create(utils.DenoPathOverride)
   366  		require.NoError(t, err)
   367  		// Setup mock api
   368  		defer gock.OffAll()
   369  		gock.New(utils.DefaultApiHost).
   370  			Get("/v1/projects/" + project + "/functions/" + slug).
   371  			Reply(http.StatusNotFound)
   372  		gock.New(utils.DefaultApiHost).
   373  			Post("/v1/projects/"+project+"/functions").
   374  			MatchParam("verify_jwt", "false").
   375  			Reply(http.StatusCreated).
   376  			JSON(api.FunctionResponse{Id: "1"})
   377  
   378  		// Setup mock docker
   379  		require.NoError(t, apitest.MockDocker(utils.Docker))
   380  		apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
   381  		require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
   382  
   383  		// Setup output file
   384  		outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug))
   385  		require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
   386  
   387  		// Run test
   388  		assert.NoError(t, Run(context.Background(), []string{slug}, project, nil, "", fsys))
   389  		// Validate api
   390  		assert.Empty(t, apitest.ListUnmatchedRequests())
   391  	})
   392  
   393  	t.Run("verify_jwt flag overrides config", func(t *testing.T) {
   394  		// Setup in-memory fs
   395  		fsys := afero.NewMemMapFs()
   396  		require.NoError(t, utils.WriteConfig(fsys, false))
   397  		f, err := fsys.OpenFile("supabase/config.toml", os.O_APPEND|os.O_WRONLY, 0600)
   398  		require.NoError(t, err)
   399  		_, err = f.WriteString(`
   400  [functions.` + slug + `]
   401  verify_jwt = false
   402  `)
   403  		require.NoError(t, err)
   404  		require.NoError(t, f.Close())
   405  		// Setup valid project ref
   406  		project := apitest.RandomProjectRef()
   407  		// Setup valid access token
   408  		token := apitest.RandomAccessToken(t)
   409  		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   410  		// Setup valid deno path
   411  		_, err = fsys.Create(utils.DenoPathOverride)
   412  		require.NoError(t, err)
   413  		// Setup mock api
   414  		defer gock.OffAll()
   415  		gock.New(utils.DefaultApiHost).
   416  			Get("/v1/projects/" + project + "/functions/" + slug).
   417  			Reply(http.StatusNotFound)
   418  		gock.New(utils.DefaultApiHost).
   419  			Post("/v1/projects/"+project+"/functions").
   420  			MatchParam("verify_jwt", "true").
   421  			Reply(http.StatusCreated).
   422  			JSON(api.FunctionResponse{Id: "1"})
   423  
   424  		// Setup mock docker
   425  		require.NoError(t, apitest.MockDocker(utils.Docker))
   426  		apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
   427  		require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
   428  
   429  		// Setup output file
   430  		outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug))
   431  		require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
   432  
   433  		// Run test
   434  		noVerifyJwt := false
   435  		assert.NoError(t, Run(context.Background(), []string{slug}, project, &noVerifyJwt, "", fsys))
   436  		// Validate api
   437  		assert.Empty(t, apitest.ListUnmatchedRequests())
   438  	})
   439  }
   440  
   441  func TestDeployFunction(t *testing.T) {
   442  	const slug = "test-func"
   443  	// Setup valid project ref
   444  	project := apitest.RandomProjectRef()
   445  	// Setup valid access token
   446  	token := apitest.RandomAccessToken(t)
   447  	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   448  	utils.EdgeRuntimeId = "test-edge-runtime"
   449  
   450  	t.Run("throws error on network failure", func(t *testing.T) {
   451  		// Setup mock api
   452  		defer gock.OffAll()
   453  		gock.New(utils.DefaultApiHost).
   454  			Get("/v1/projects/" + project + "/functions/" + slug).
   455  			ReplyError(errors.New("network error"))
   456  		// Run test
   457  		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
   458  		// Check error
   459  		assert.ErrorContains(t, err, "network error")
   460  	})
   461  
   462  	t.Run("throws error on service unavailable", func(t *testing.T) {
   463  		// Setup mock api
   464  		defer gock.OffAll()
   465  		gock.New(utils.DefaultApiHost).
   466  			Get("/v1/projects/" + project + "/functions/" + slug).
   467  			Reply(http.StatusServiceUnavailable)
   468  		// Run test
   469  		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
   470  		// Check error
   471  		assert.ErrorContains(t, err, "Unexpected error deploying Function:")
   472  	})
   473  
   474  	t.Run("throws error on create failure", func(t *testing.T) {
   475  		// Setup mock api
   476  		defer gock.OffAll()
   477  		gock.New(utils.DefaultApiHost).
   478  			Get("/v1/projects/" + project + "/functions/" + slug).
   479  			Reply(http.StatusNotFound)
   480  		gock.New(utils.DefaultApiHost).
   481  			Post("/v1/projects/" + project + "/functions").
   482  			ReplyError(errors.New("network error"))
   483  		// Run test
   484  		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
   485  		// Check error
   486  		assert.ErrorContains(t, err, "network error")
   487  	})
   488  
   489  	t.Run("throws error on create unavailable", func(t *testing.T) {
   490  		// Setup mock api
   491  		defer gock.OffAll()
   492  		gock.New(utils.DefaultApiHost).
   493  			Get("/v1/projects/" + project + "/functions/" + slug).
   494  			Reply(http.StatusNotFound)
   495  		gock.New(utils.DefaultApiHost).
   496  			Post("/v1/projects/" + project + "/functions").
   497  			Reply(http.StatusServiceUnavailable)
   498  		// Run test
   499  		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
   500  		// Check error
   501  		assert.ErrorContains(t, err, "Failed to create a new Function on the Supabase project:")
   502  	})
   503  
   504  	t.Run("throws error on update failure", func(t *testing.T) {
   505  		// Setup mock api
   506  		defer gock.OffAll()
   507  		gock.New(utils.DefaultApiHost).
   508  			Get("/v1/projects/" + project + "/functions/" + slug).
   509  			Reply(http.StatusOK).
   510  			JSON(api.FunctionResponse{Id: "1"})
   511  		gock.New(utils.DefaultApiHost).
   512  			Patch("/v1/projects/" + project + "/functions/" + slug).
   513  			ReplyError(errors.New("network error"))
   514  		// Run test
   515  		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
   516  		// Check error
   517  		assert.ErrorContains(t, err, "network error")
   518  	})
   519  
   520  	t.Run("throws error on update unavailable", func(t *testing.T) {
   521  		// Setup mock api
   522  		defer gock.OffAll()
   523  		gock.New(utils.DefaultApiHost).
   524  			Get("/v1/projects/" + project + "/functions/" + slug).
   525  			Reply(http.StatusOK).
   526  			JSON(api.FunctionResponse{Id: "1"})
   527  		gock.New(utils.DefaultApiHost).
   528  			Patch("/v1/projects/" + project + "/functions/" + slug).
   529  			Reply(http.StatusServiceUnavailable)
   530  		// Run test
   531  		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
   532  		// Check error
   533  		assert.ErrorContains(t, err, "Failed to update an existing Function's body on the Supabase project:")
   534  	})
   535  }