github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/functions/deploy/deploy_test.go (about) 1 package deploy 2 3 import ( 4 "archive/zip" 5 "bytes" 6 "context" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io" 11 "log" 12 "net/http" 13 "os" 14 "strings" 15 "testing" 16 17 "github.com/spf13/afero" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/require" 20 "github.com/Redstoneguy129/cli/internal/testing/apitest" 21 "github.com/Redstoneguy129/cli/internal/utils" 22 "github.com/Redstoneguy129/cli/pkg/api" 23 "gopkg.in/h2non/gock.v1" 24 ) 25 26 func TestMain(m *testing.M) { 27 // Setup fake deno binary 28 if len(os.Args) > 1 && (os.Args[1] == "bundle" || os.Args[1] == "upgrade" || os.Args[1] == "run") { 29 msg := os.Getenv("TEST_DENO_ERROR") 30 if msg != "" { 31 fmt.Fprintln(os.Stderr, msg) 32 os.Exit(1) 33 } 34 os.Exit(0) 35 } 36 denoPath, err := os.Executable() 37 if err != nil { 38 log.Fatalln(err) 39 } 40 utils.DenoPathOverride = denoPath 41 // Run test suite 42 os.Exit(m.Run()) 43 } 44 45 func TestDeployCommand(t *testing.T) { 46 t.Run("deploys new function (legacy bundle)", func(t *testing.T) { 47 const slug = "test-func" 48 // Setup in-memory fs 49 fsys := afero.NewMemMapFs() 50 // Setup valid project ref 51 project := apitest.RandomProjectRef() 52 // Setup valid access token 53 token := apitest.RandomAccessToken(t) 54 t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) 55 // Setup valid deno path 56 _, err := fsys.Create(utils.DenoPathOverride) 57 require.NoError(t, err) 58 // Setup mock api 59 defer gock.OffAll() 60 gock.New("https://api.supabase.io"). 61 Get("/v1/projects/" + project + "/functions/" + slug). 62 Reply(http.StatusNotFound) 63 gock.New("https://api.supabase.io"). 64 Post("/v1/projects/" + project + "/functions"). 65 Reply(http.StatusCreated). 66 JSON(api.FunctionResponse{Id: "1"}) 67 // Run test 68 noVerifyJWT := true 69 assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJWT, true, "", fsys)) 70 // Validate api 71 assert.Empty(t, apitest.ListUnmatchedRequests()) 72 }) 73 74 t.Run("deploys new function (ESZIP)", func(t *testing.T) { 75 const slug = "test-func" 76 // Setup in-memory fs 77 fsys := afero.NewMemMapFs() 78 // Setup valid project ref 79 project := apitest.RandomProjectRef() 80 // Setup valid access token 81 token := apitest.RandomAccessToken(t) 82 t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) 83 // Setup valid deno path 84 _, err := fsys.Create(utils.DenoPathOverride) 85 require.NoError(t, err) 86 // Setup mock api 87 defer gock.OffAll() 88 gock.New("https://api.supabase.io"). 89 Get("/v1/projects/" + project + "/functions/" + slug). 90 Reply(http.StatusNotFound) 91 gock.New("https://api.supabase.io"). 92 Post("/v1/projects/" + project + "/functions"). 93 Reply(http.StatusCreated). 94 JSON(api.FunctionResponse{Id: "1"}) 95 // Run test 96 noVerifyJWT := true 97 assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJWT, false, "", fsys)) 98 // Validate api 99 assert.Empty(t, apitest.ListUnmatchedRequests()) 100 }) 101 102 t.Run("updates deployed function (legacy bundle)", func(t *testing.T) { 103 const slug = "test-func" 104 // Setup in-memory fs 105 fsys := afero.NewMemMapFs() 106 // Setup valid project ref 107 project := apitest.RandomProjectRef() 108 require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644)) 109 // Setup valid access token 110 token := apitest.RandomAccessToken(t) 111 t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) 112 // Setup valid deno path 113 _, err := fsys.Create(utils.DenoPathOverride) 114 require.NoError(t, err) 115 // Setup mock api 116 defer gock.OffAll() 117 gock.New("https://api.supabase.io"). 118 Get("/v1/projects/" + project + "/functions/" + slug). 119 Reply(http.StatusOK). 120 JSON(api.FunctionResponse{Id: "1"}) 121 gock.New("https://api.supabase.io"). 122 Patch("/v1/projects/" + project + "/functions/" + slug). 123 Reply(http.StatusOK). 124 JSON(api.FunctionResponse{Id: "1"}) 125 // Run test 126 assert.NoError(t, Run(context.Background(), slug, "", nil, true, "", fsys)) 127 // Validate api 128 assert.Empty(t, apitest.ListUnmatchedRequests()) 129 }) 130 131 t.Run("updates deployed function (ESZIP)", func(t *testing.T) { 132 const slug = "test-func" 133 // Setup in-memory fs 134 fsys := afero.NewMemMapFs() 135 // Setup valid project ref 136 project := apitest.RandomProjectRef() 137 require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644)) 138 // Setup valid access token 139 token := apitest.RandomAccessToken(t) 140 t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) 141 // Setup valid deno path 142 _, err := fsys.Create(utils.DenoPathOverride) 143 require.NoError(t, err) 144 // Setup mock api 145 defer gock.OffAll() 146 gock.New("https://api.supabase.io"). 147 Get("/v1/projects/" + project + "/functions/" + slug). 148 Reply(http.StatusOK). 149 JSON(api.FunctionResponse{Id: "1"}) 150 gock.New("https://api.supabase.io"). 151 Patch("/v1/projects/" + project + "/functions/" + slug). 152 Reply(http.StatusOK). 153 JSON(api.FunctionResponse{Id: "1"}) 154 // Run test 155 assert.NoError(t, Run(context.Background(), slug, "", nil, false, "", fsys)) 156 // Validate api 157 assert.Empty(t, apitest.ListUnmatchedRequests()) 158 }) 159 160 t.Run("throws error on malformed ref", func(t *testing.T) { 161 // Setup in-memory fs 162 fsys := afero.NewMemMapFs() 163 // Setup invalid project ref 164 require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte("test-project"), 0644)) 165 // Run test 166 noVerifyJWT := true 167 err := Run(context.Background(), "test-func", "", &noVerifyJWT, true, "", fsys) 168 // Check error 169 assert.ErrorContains(t, err, "Invalid project ref format.") 170 }) 171 172 t.Run("throws error on malformed ref arg", func(t *testing.T) { 173 // Setup in-memory fs 174 fsys := afero.NewMemMapFs() 175 // Run test 176 noVerifyJWT := true 177 err := Run(context.Background(), "test-func", "test-project", &noVerifyJWT, true, "", fsys) 178 // Check error 179 assert.ErrorContains(t, err, "Invalid project ref format.") 180 }) 181 182 t.Run("throws error on malformed slug", func(t *testing.T) { 183 // Setup in-memory fs 184 fsys := afero.NewMemMapFs() 185 // Setup valid project ref 186 project := apitest.RandomProjectRef() 187 // Run test 188 noVerifyJWT := true 189 err := Run(context.Background(), "@", project, &noVerifyJWT, true, "", fsys) 190 // Check error 191 assert.ErrorContains(t, err, "Invalid Function name.") 192 }) 193 194 t.Run("throws error on failure to install deno", func(t *testing.T) { 195 // Setup in-memory fs 196 fsys := afero.NewReadOnlyFs(afero.NewMemMapFs()) 197 // Setup valid project ref 198 project := apitest.RandomProjectRef() 199 // Run test 200 noVerifyJWT := true 201 err := Run(context.Background(), "test-func", project, &noVerifyJWT, true, "", fsys) 202 // Check error 203 assert.ErrorContains(t, err, "operation not permitted") 204 }) 205 206 t.Run("throws error on bundle failure", func(t *testing.T) { 207 // Setup in-memory fs 208 fsys := afero.NewMemMapFs() 209 // Setup valid project ref 210 project := apitest.RandomProjectRef() 211 // Setup deno error 212 t.Setenv("TEST_DENO_ERROR", "bundle failed") 213 var body bytes.Buffer 214 archive := zip.NewWriter(&body) 215 w, err := archive.Create("deno") 216 require.NoError(t, err) 217 _, err = w.Write([]byte("binary")) 218 require.NoError(t, err) 219 require.NoError(t, archive.Close()) 220 // Setup mock api 221 defer gock.OffAll() 222 gock.New("https://github.com"). 223 Get("/denoland/deno/releases/download/v" + utils.DenoVersion). 224 Reply(http.StatusOK). 225 Body(&body) 226 // Run test 227 noVerifyJWT := true 228 err = Run(context.Background(), "test-func", project, &noVerifyJWT, true, "", fsys) 229 // Check error 230 assert.ErrorContains(t, err, "Error bundling function: exit status 1\nbundle failed\n") 231 assert.Empty(t, apitest.ListUnmatchedRequests()) 232 }) 233 234 t.Run("throws error on ESZIP failure", func(t *testing.T) { 235 // Setup in-memory fs 236 fsys := afero.NewMemMapFs() 237 // Setup valid project ref 238 project := apitest.RandomProjectRef() 239 // Setup deno error 240 t.Setenv("TEST_DENO_ERROR", "eszip failed") 241 var body bytes.Buffer 242 archive := zip.NewWriter(&body) 243 w, err := archive.Create("deno") 244 require.NoError(t, err) 245 _, err = w.Write([]byte("binary")) 246 require.NoError(t, err) 247 require.NoError(t, archive.Close()) 248 // Setup mock api 249 defer gock.OffAll() 250 gock.New("https://github.com"). 251 Get("/denoland/deno/releases/download/v" + utils.DenoVersion). 252 Reply(http.StatusOK). 253 Body(&body) 254 255 noVerifyJWT := true 256 err = Run(context.Background(), "test-func", project, &noVerifyJWT, false, "", fsys) 257 // Check error 258 assert.ErrorContains(t, err, "Error bundling function: exit status 1\neszip failed\n") 259 }) 260 261 t.Run("verify_jwt param falls back to config", func(t *testing.T) { 262 const slug = "test-func" 263 // Setup in-memory fs 264 fsys := afero.NewMemMapFs() 265 require.NoError(t, utils.WriteConfig(fsys, false)) 266 f, err := fsys.OpenFile("supabase/config.toml", os.O_APPEND|os.O_WRONLY, 0600) 267 require.NoError(t, err) 268 _, err = f.WriteString(` 269 [functions.` + slug + `] 270 verify_jwt = false 271 `) 272 require.NoError(t, err) 273 require.NoError(t, f.Close()) 274 // Setup valid project ref 275 project := apitest.RandomProjectRef() 276 // Setup valid access token 277 token := apitest.RandomAccessToken(t) 278 t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) 279 // Setup valid deno path 280 _, err = fsys.Create(utils.DenoPathOverride) 281 require.NoError(t, err) 282 // Setup mock api 283 defer gock.OffAll() 284 gock.New("https://api.supabase.io"). 285 Get("/v1/projects/" + project + "/functions/" + slug). 286 Reply(http.StatusNotFound) 287 gock.New("https://api.supabase.io"). 288 Post("/v1/projects/" + project + "/functions"). 289 AddMatcher(func(req *http.Request, ereq *gock.Request) (bool, error) { 290 body, err := io.ReadAll(req.Body) 291 if err != nil { 292 return false, err 293 } 294 295 var bodyJson map[string]interface{} 296 err = json.Unmarshal(body, &bodyJson) 297 if err != nil { 298 return false, err 299 } 300 301 return bodyJson["verify_jwt"] == false, nil 302 }). 303 Reply(http.StatusCreated). 304 JSON(api.FunctionResponse{Id: "1"}) 305 // Run test 306 assert.NoError(t, Run(context.Background(), slug, project, nil, true, "", fsys)) 307 // Validate api 308 assert.Empty(t, apitest.ListUnmatchedRequests()) 309 }) 310 311 t.Run("verify_jwt flag overrides config", func(t *testing.T) { 312 const slug = "test-func" 313 // Setup in-memory fs 314 fsys := afero.NewMemMapFs() 315 require.NoError(t, utils.WriteConfig(fsys, false)) 316 f, err := fsys.OpenFile("supabase/config.toml", os.O_APPEND|os.O_WRONLY, 0600) 317 require.NoError(t, err) 318 _, err = f.WriteString(` 319 [functions.` + slug + `] 320 verify_jwt = false 321 `) 322 require.NoError(t, err) 323 require.NoError(t, f.Close()) 324 // Setup valid project ref 325 project := apitest.RandomProjectRef() 326 // Setup valid access token 327 token := apitest.RandomAccessToken(t) 328 t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) 329 // Setup valid deno path 330 _, err = fsys.Create(utils.DenoPathOverride) 331 require.NoError(t, err) 332 // Setup mock api 333 defer gock.OffAll() 334 gock.New("https://api.supabase.io"). 335 Get("/v1/projects/" + project + "/functions/" + slug). 336 Reply(http.StatusNotFound) 337 gock.New("https://api.supabase.io"). 338 Post("/v1/projects/" + project + "/functions"). 339 AddMatcher(func(req *http.Request, ereq *gock.Request) (bool, error) { 340 body, err := io.ReadAll(req.Body) 341 if err != nil { 342 return false, err 343 } 344 345 var bodyJson map[string]interface{} 346 err = json.Unmarshal(body, &bodyJson) 347 if err != nil { 348 return false, err 349 } 350 351 return bodyJson["verify_jwt"] == true, nil 352 }). 353 Reply(http.StatusCreated). 354 JSON(api.FunctionResponse{Id: "1"}) 355 // Run test 356 noVerifyJwt := false 357 assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJwt, true, "", fsys)) 358 // Validate api 359 assert.Empty(t, apitest.ListUnmatchedRequests()) 360 }) 361 362 t.Run("uses fallback import map", func(t *testing.T) { 363 const slug = "test-func" 364 // Setup in-memory fs 365 fsys := afero.NewMemMapFs() 366 require.NoError(t, utils.WriteConfig(fsys, false)) 367 require.NoError(t, afero.WriteFile(fsys, "supabase/functions/import_map.json", []byte(""), 0644)) 368 // Setup valid project ref 369 project := apitest.RandomProjectRef() 370 // Setup valid access token 371 token := apitest.RandomAccessToken(t) 372 t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) 373 // Setup valid deno path 374 _, err := fsys.Create(utils.DenoPathOverride) 375 require.NoError(t, err) 376 // Setup mock api 377 defer gock.OffAll() 378 gock.New("https://api.supabase.io"). 379 Get("/v1/projects/" + project + "/functions/" + slug). 380 Reply(http.StatusNotFound) 381 gock.New("https://api.supabase.io"). 382 Post("/v1/projects/"+project+"/functions"). 383 MatchParam("import_map", "true"). 384 Reply(http.StatusCreated). 385 JSON(api.FunctionResponse{Id: "1"}) 386 // Run test 387 noVerifyJwt := false 388 assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJwt, false, "", fsys)) 389 // Validate api 390 assert.Empty(t, apitest.ListUnmatchedRequests()) 391 }) 392 } 393 394 func TestDeployFunction(t *testing.T) { 395 const slug = "test-func" 396 // Setup valid project ref 397 project := apitest.RandomProjectRef() 398 // Setup valid access token 399 token := apitest.RandomAccessToken(t) 400 t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) 401 402 t.Run("throws error on network failure", func(t *testing.T) { 403 // Setup mock api 404 defer gock.OffAll() 405 gock.New("https://api.supabase.io"). 406 Get("/v1/projects/" + project + "/functions/" + slug). 407 ReplyError(errors.New("network error")) 408 // Run test 409 err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true) 410 // Check error 411 assert.ErrorContains(t, err, "network error") 412 }) 413 414 t.Run("throws error on service unavailable", func(t *testing.T) { 415 // Setup mock api 416 defer gock.OffAll() 417 gock.New("https://api.supabase.io"). 418 Get("/v1/projects/" + project + "/functions/" + slug). 419 Reply(http.StatusServiceUnavailable) 420 // Run test 421 err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true) 422 // Check error 423 assert.ErrorContains(t, err, "Unexpected error deploying Function:") 424 }) 425 426 t.Run("throws error on create failure", func(t *testing.T) { 427 // Setup mock api 428 defer gock.OffAll() 429 gock.New("https://api.supabase.io"). 430 Get("/v1/projects/" + project + "/functions/" + slug). 431 Reply(http.StatusNotFound) 432 gock.New("https://api.supabase.io"). 433 Post("/v1/projects/" + project + "/functions"). 434 ReplyError(errors.New("network error")) 435 // Run test 436 err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true) 437 // Check error 438 assert.ErrorContains(t, err, "network error") 439 }) 440 441 t.Run("throws error on create unavailable", func(t *testing.T) { 442 // Setup mock api 443 defer gock.OffAll() 444 gock.New("https://api.supabase.io"). 445 Get("/v1/projects/" + project + "/functions/" + slug). 446 Reply(http.StatusNotFound) 447 gock.New("https://api.supabase.io"). 448 Post("/v1/projects/" + project + "/functions"). 449 Reply(http.StatusServiceUnavailable) 450 // Run test 451 err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true) 452 // Check error 453 assert.ErrorContains(t, err, "Failed to create a new Function on the Supabase project:") 454 }) 455 456 t.Run("throws error on update failure", func(t *testing.T) { 457 // Setup mock api 458 defer gock.OffAll() 459 gock.New("https://api.supabase.io"). 460 Get("/v1/projects/" + project + "/functions/" + slug). 461 Reply(http.StatusOK). 462 JSON(api.FunctionResponse{Id: "1"}) 463 gock.New("https://api.supabase.io"). 464 Patch("/v1/projects/" + project + "/functions/" + slug). 465 ReplyError(errors.New("network error")) 466 // Run test 467 err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true) 468 // Check error 469 assert.ErrorContains(t, err, "network error") 470 }) 471 472 t.Run("throws error on update unavailable", func(t *testing.T) { 473 // Setup mock api 474 defer gock.OffAll() 475 gock.New("https://api.supabase.io"). 476 Get("/v1/projects/" + project + "/functions/" + slug). 477 Reply(http.StatusOK). 478 JSON(api.FunctionResponse{Id: "1"}) 479 gock.New("https://api.supabase.io"). 480 Patch("/v1/projects/" + project + "/functions/" + slug). 481 Reply(http.StatusServiceUnavailable) 482 // Run test 483 err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true) 484 // Check error 485 assert.ErrorContains(t, err, "Failed to update an existing Function's body on the Supabase project:") 486 }) 487 }