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 }