github.com/supabase/cli@v1.168.1/internal/functions/download/download_test.go (about)

     1  package download
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"log"
    10  	"net/http"
    11  	"os"
    12  	"testing"
    13  
    14  	"github.com/spf13/afero"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  	"github.com/supabase/cli/internal/testing/apitest"
    18  	"github.com/supabase/cli/internal/utils"
    19  	"github.com/supabase/cli/pkg/api"
    20  	"gopkg.in/h2non/gock.v1"
    21  )
    22  
    23  func TestMain(m *testing.M) {
    24  	// Setup fake deno binary
    25  	if len(os.Args) > 1 && (os.Args[1] == "bundle" || os.Args[1] == "upgrade" || os.Args[1] == "run") {
    26  		msg := os.Getenv("TEST_DENO_ERROR")
    27  		if msg != "" {
    28  			fmt.Fprintln(os.Stderr, msg)
    29  			os.Exit(1)
    30  		}
    31  		os.Exit(0)
    32  	}
    33  	denoPath, err := os.Executable()
    34  	if err != nil {
    35  		log.Fatalln(err)
    36  	}
    37  	utils.DenoPathOverride = denoPath
    38  	// Run test suite
    39  	os.Exit(m.Run())
    40  }
    41  
    42  func TestDownloadCommand(t *testing.T) {
    43  	const slug = "test-func"
    44  
    45  	t.Run("downloads eszip bundle", func(t *testing.T) {
    46  		// Setup in-memory fs
    47  		fsys := afero.NewMemMapFs()
    48  		// Setup valid project ref
    49  		project := apitest.RandomProjectRef()
    50  		// Setup valid access token
    51  		token := apitest.RandomAccessToken(t)
    52  		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
    53  		// Setup valid deno path
    54  		_, err := fsys.Create(utils.DenoPathOverride)
    55  		require.NoError(t, err)
    56  		// Setup mock api
    57  		defer gock.OffAll()
    58  		gock.New(utils.DefaultApiHost).
    59  			Get("/v1/projects/" + project + "/functions/" + slug).
    60  			Reply(http.StatusOK).
    61  			JSON(api.FunctionResponse{Id: "1"})
    62  		gock.New(utils.DefaultApiHost).
    63  			Get("/v1/projects/" + project + "/functions/" + slug + "/body").
    64  			Reply(http.StatusOK)
    65  		// Run test
    66  		err = Run(context.Background(), slug, project, true, fsys)
    67  		// Check error
    68  		assert.NoError(t, err)
    69  		assert.Empty(t, apitest.ListUnmatchedRequests())
    70  	})
    71  
    72  	t.Run("throws error on malformed slug", func(t *testing.T) {
    73  		// Setup in-memory fs
    74  		fsys := afero.NewMemMapFs()
    75  		// Setup valid project ref
    76  		project := apitest.RandomProjectRef()
    77  		// Run test
    78  		err := Run(context.Background(), "@", project, true, fsys)
    79  		// Check error
    80  		assert.ErrorContains(t, err, "Invalid Function name.")
    81  	})
    82  
    83  	t.Run("throws error on failure to install deno", func(t *testing.T) {
    84  		// Setup in-memory fs
    85  		fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
    86  		// Setup valid project ref
    87  		project := apitest.RandomProjectRef()
    88  		// Run test
    89  		err := Run(context.Background(), slug, project, true, fsys)
    90  		// Check error
    91  		assert.ErrorContains(t, err, "operation not permitted")
    92  	})
    93  
    94  	t.Run("throws error on copy failure", func(t *testing.T) {
    95  		// Setup in-memory fs
    96  		fsys := afero.NewMemMapFs()
    97  		// Setup valid project ref
    98  		project := apitest.RandomProjectRef()
    99  		// Setup valid deno path
   100  		_, err := fsys.Create(utils.DenoPathOverride)
   101  		require.NoError(t, err)
   102  		// Run test
   103  		err = Run(context.Background(), slug, project, true, afero.NewReadOnlyFs(fsys))
   104  		// Check error
   105  		assert.ErrorContains(t, err, "operation not permitted")
   106  	})
   107  
   108  	t.Run("throws error on missing function", func(t *testing.T) {
   109  		// Setup in-memory fs
   110  		fsys := afero.NewMemMapFs()
   111  		// Setup valid project ref
   112  		project := apitest.RandomProjectRef()
   113  		// Setup valid access token
   114  		token := apitest.RandomAccessToken(t)
   115  		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   116  		// Setup valid deno path
   117  		_, err := fsys.Create(utils.DenoPathOverride)
   118  		require.NoError(t, err)
   119  		// Setup mock api
   120  		defer gock.OffAll()
   121  		gock.New(utils.DefaultApiHost).
   122  			Get("/v1/projects/" + project + "/functions/" + slug).
   123  			Reply(http.StatusNotFound).
   124  			JSON(map[string]string{"message": "Function not found"})
   125  		// Run test
   126  		err = Run(context.Background(), slug, project, true, fsys)
   127  		// Check error
   128  		assert.ErrorContains(t, err, "Function test-func does not exist on the Supabase project.")
   129  	})
   130  }
   131  
   132  func TestDownloadFunction(t *testing.T) {
   133  	const slug = "test-func"
   134  	// Setup valid project ref
   135  	project := apitest.RandomProjectRef()
   136  	// Setup valid access token
   137  	token := apitest.RandomAccessToken(t)
   138  	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   139  
   140  	t.Run("throws error on network error", func(t *testing.T) {
   141  		// Setup mock api
   142  		defer gock.OffAll()
   143  		gock.New(utils.DefaultApiHost).
   144  			Get("/v1/projects/" + project + "/functions/" + slug).
   145  			Reply(http.StatusOK).
   146  			JSON(api.FunctionResponse{Id: "1"})
   147  		gock.New(utils.DefaultApiHost).
   148  			Get("/v1/projects/" + project + "/functions/" + slug + "/body").
   149  			ReplyError(errors.New("network error"))
   150  		// Run test
   151  		err := downloadFunction(context.Background(), project, slug, "")
   152  		// Check error
   153  		assert.ErrorContains(t, err, "network error")
   154  	})
   155  
   156  	t.Run("throws error on service unavailable", func(t *testing.T) {
   157  		// Setup mock api
   158  		defer gock.OffAll()
   159  		gock.New(utils.DefaultApiHost).
   160  			Get("/v1/projects/" + project + "/functions/" + slug).
   161  			Reply(http.StatusOK).
   162  			JSON(api.FunctionResponse{Id: "1"})
   163  		gock.New(utils.DefaultApiHost).
   164  			Get("/v1/projects/" + project + "/functions/" + slug + "/body").
   165  			Reply(http.StatusServiceUnavailable)
   166  		// Run test
   167  		err := downloadFunction(context.Background(), project, slug, "")
   168  		// Check error
   169  		assert.ErrorContains(t, err, "Unexpected error downloading Function:")
   170  	})
   171  
   172  	t.Run("throws error on extract failure", func(t *testing.T) {
   173  		// Setup deno error
   174  		t.Setenv("TEST_DENO_ERROR", "extract failed")
   175  		var body bytes.Buffer
   176  		archive := zip.NewWriter(&body)
   177  		w, err := archive.Create("deno")
   178  		require.NoError(t, err)
   179  		_, err = w.Write([]byte("binary"))
   180  		require.NoError(t, err)
   181  		require.NoError(t, archive.Close())
   182  		// Setup mock api
   183  		defer gock.OffAll()
   184  		gock.New(utils.DefaultApiHost).
   185  			Get("/v1/projects/" + project + "/functions/" + slug).
   186  			Reply(http.StatusOK).
   187  			JSON(api.FunctionResponse{Id: "1"})
   188  		gock.New(utils.DefaultApiHost).
   189  			Get("/v1/projects/" + project + "/functions/" + slug + "/body").
   190  			Reply(http.StatusOK)
   191  		// Run test
   192  		err = downloadFunction(context.Background(), project, slug, "")
   193  		// Check error
   194  		assert.ErrorContains(t, err, "Error downloading function: exit status 1\nextract failed\n")
   195  		assert.Empty(t, apitest.ListUnmatchedRequests())
   196  	})
   197  }
   198  
   199  func TestGetMetadata(t *testing.T) {
   200  	const slug = "test-func"
   201  	project := apitest.RandomProjectRef()
   202  	// Setup valid access token
   203  	token := apitest.RandomAccessToken(t)
   204  	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   205  
   206  	t.Run("fallback to default paths", func(t *testing.T) {
   207  		// Setup mock api
   208  		defer gock.OffAll()
   209  		gock.New(utils.DefaultApiHost).
   210  			Get("/v1/projects/" + project + "/functions/" + slug).
   211  			Reply(http.StatusOK).
   212  			JSON(api.FunctionResponse{Id: "1"})
   213  		// Run test
   214  		meta, err := getFunctionMetadata(context.Background(), project, slug)
   215  		// Check error
   216  		assert.NoError(t, err)
   217  		assert.Equal(t, legacyEntrypointPath, *meta.EntrypointPath)
   218  		assert.Equal(t, legacyImportMapPath, *meta.ImportMapPath)
   219  	})
   220  
   221  	t.Run("throws error on network error", func(t *testing.T) {
   222  		// Setup mock api
   223  		defer gock.OffAll()
   224  		gock.New(utils.DefaultApiHost).
   225  			Get("/v1/projects/" + project + "/functions/" + slug).
   226  			ReplyError(errors.New("network error"))
   227  		// Run test
   228  		meta, err := getFunctionMetadata(context.Background(), project, slug)
   229  		// Check error
   230  		assert.ErrorContains(t, err, "network error")
   231  		assert.Nil(t, meta)
   232  	})
   233  
   234  	t.Run("throws error on service unavailable", func(t *testing.T) {
   235  		// Setup mock api
   236  		defer gock.OffAll()
   237  		gock.New(utils.DefaultApiHost).
   238  			Get("/v1/projects/" + project + "/functions/" + slug).
   239  			Reply(http.StatusServiceUnavailable)
   240  		// Run test
   241  		meta, err := getFunctionMetadata(context.Background(), project, slug)
   242  		// Check error
   243  		assert.ErrorContains(t, err, "Failed to download Function test-func on the Supabase project:")
   244  		assert.Nil(t, meta)
   245  	})
   246  }