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  }