github.com/supabase/cli@v1.168.1/internal/storage/cp/cp_test.go (about)

     1  package cp
     2  
     3  import (
     4  	"context"
     5  	"io/fs"
     6  	"net/http"
     7  	"testing"
     8  
     9  	"github.com/spf13/afero"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  	"github.com/supabase/cli/internal/testing/apitest"
    13  	"github.com/supabase/cli/internal/utils"
    14  	"github.com/supabase/cli/internal/utils/flags"
    15  	"github.com/supabase/cli/pkg/api"
    16  	"github.com/supabase/cli/pkg/fetcher"
    17  	"github.com/supabase/cli/pkg/storage"
    18  	"gopkg.in/h2non/gock.v1"
    19  )
    20  
    21  var mockFile = storage.ObjectResponse{
    22  	Name:           "abstract.pdf",
    23  	Id:             utils.Ptr("9b7f9f48-17a6-4ca8-b14a-39b0205a63e9"),
    24  	UpdatedAt:      utils.Ptr("2023-10-13T18:08:22.068Z"),
    25  	CreatedAt:      utils.Ptr("2023-10-13T18:08:22.068Z"),
    26  	LastAccessedAt: utils.Ptr("2023-10-13T18:08:22.068Z"),
    27  	Metadata: &storage.ObjectMetadata{
    28  		ETag:           `"887ea9be3c68e6f2fca7fd2d7c77d8fe"`,
    29  		Size:           82702,
    30  		Mimetype:       "application/pdf",
    31  		CacheControl:   "max-age=3600",
    32  		LastModified:   "2023-10-13T18:08:22.000Z",
    33  		ContentLength:  82702,
    34  		HttpStatusCode: 200,
    35  	},
    36  }
    37  
    38  var mockApi = storage.StorageAPI{Fetcher: fetcher.NewFetcher(
    39  	"http://127.0.0.1",
    40  )}
    41  
    42  func TestStorageCP(t *testing.T) {
    43  	flags.ProjectRef = apitest.RandomProjectRef()
    44  	// Setup valid access token
    45  	token := apitest.RandomAccessToken(t)
    46  	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
    47  
    48  	t.Run("copy local to remote", func(t *testing.T) {
    49  		// Setup in-memory fs
    50  		fsys := afero.NewMemMapFs()
    51  		require.NoError(t, afero.WriteFile(fsys, "/tmp/file", []byte{}, 0644))
    52  		// Setup mock api
    53  		defer gock.OffAll()
    54  		gock.New(utils.DefaultApiHost).
    55  			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
    56  			Reply(http.StatusOK).
    57  			JSON([]api.ApiKeyResponse{{
    58  				Name:   "service_role",
    59  				ApiKey: "service-key",
    60  			}})
    61  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
    62  			Post("/storage/v1/object/private/file").
    63  			Reply(http.StatusOK)
    64  		// Run test
    65  		err := Run(context.Background(), "/tmp/file", "ss:///private/file", false, 1, fsys)
    66  		// Check error
    67  		assert.NoError(t, err)
    68  		assert.Empty(t, apitest.ListUnmatchedRequests())
    69  	})
    70  
    71  	t.Run("throws error on missing file", func(t *testing.T) {
    72  		// Setup in-memory fs
    73  		fsys := afero.NewMemMapFs()
    74  		// Setup mock api
    75  		defer gock.OffAll()
    76  		gock.New(utils.DefaultApiHost).
    77  			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
    78  			Reply(http.StatusOK).
    79  			JSON([]api.ApiKeyResponse{{
    80  				Name:   "service_role",
    81  				ApiKey: "service-key",
    82  			}})
    83  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
    84  			Get("/storage/v1/bucket").
    85  			Reply(http.StatusOK).
    86  			JSON([]storage.BucketResponse{})
    87  		// Run test
    88  		err := Run(context.Background(), "abstract.pdf", "ss:///private", true, 1, fsys)
    89  		// Check error
    90  		assert.ErrorIs(t, err, fs.ErrNotExist)
    91  		assert.Empty(t, apitest.ListUnmatchedRequests())
    92  	})
    93  
    94  	t.Run("copy remote to local", func(t *testing.T) {
    95  		// Setup in-memory fs
    96  		fsys := afero.NewMemMapFs()
    97  		// Setup mock api
    98  		defer gock.OffAll()
    99  		gock.New(utils.DefaultApiHost).
   100  			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
   101  			Reply(http.StatusOK).
   102  			JSON([]api.ApiKeyResponse{{
   103  				Name:   "service_role",
   104  				ApiKey: "service-key",
   105  			}})
   106  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
   107  			Get("/storage/v1/object/private/file").
   108  			Reply(http.StatusOK)
   109  		// Run test
   110  		err := Run(context.Background(), "ss:///private/file", "abstract.pdf", false, 1, fsys)
   111  		// Check error
   112  		assert.NoError(t, err)
   113  		assert.Empty(t, apitest.ListUnmatchedRequests())
   114  		exists, err := afero.Exists(fsys, "abstract.pdf")
   115  		assert.NoError(t, err)
   116  		assert.True(t, exists)
   117  	})
   118  
   119  	t.Run("throws error on missing bucket", func(t *testing.T) {
   120  		// Setup in-memory fs
   121  		fsys := afero.NewMemMapFs()
   122  		// Setup mock api
   123  		defer gock.OffAll()
   124  		gock.New(utils.DefaultApiHost).
   125  			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
   126  			Reply(http.StatusOK).
   127  			JSON([]api.ApiKeyResponse{{
   128  				Name:   "service_role",
   129  				ApiKey: "service-key",
   130  			}})
   131  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
   132  			Get("/storage/v1/bucket").
   133  			Reply(http.StatusOK).
   134  			JSON([]storage.BucketResponse{})
   135  		// Run test
   136  		err := Run(context.Background(), "ss:///private", ".", true, 1, fsys)
   137  		// Check error
   138  		assert.ErrorContains(t, err, "Object not found: /private")
   139  		assert.Empty(t, apitest.ListUnmatchedRequests())
   140  	})
   141  
   142  	t.Run("throws error on invalid src", func(t *testing.T) {
   143  		// Setup in-memory fs
   144  		fsys := afero.NewMemMapFs()
   145  		// Run test
   146  		err := Run(context.Background(), ":", ".", false, 1, fsys)
   147  		// Check error
   148  		assert.ErrorContains(t, err, "missing protocol scheme")
   149  	})
   150  
   151  	t.Run("throws error on invalid dst", func(t *testing.T) {
   152  		// Setup in-memory fs
   153  		fsys := afero.NewMemMapFs()
   154  		// Run test
   155  		err := Run(context.Background(), ".", ":", false, 1, fsys)
   156  		// Check error
   157  		assert.ErrorContains(t, err, "missing protocol scheme")
   158  	})
   159  
   160  	t.Run("throws error on unsupported operation", func(t *testing.T) {
   161  		// Setup in-memory fs
   162  		fsys := afero.NewMemMapFs()
   163  		// Setup mock api
   164  		defer gock.OffAll()
   165  		gock.New(utils.DefaultApiHost).
   166  			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
   167  			Reply(http.StatusOK).
   168  			JSON([]api.ApiKeyResponse{{
   169  				Name:   "service_role",
   170  				ApiKey: "service-key",
   171  			}})
   172  		// Run test
   173  		err := Run(context.Background(), ".", ".", false, 1, fsys)
   174  		// Check error
   175  		assert.ErrorIs(t, err, errUnsupportedOperation)
   176  	})
   177  }
   178  
   179  func TestUploadAll(t *testing.T) {
   180  	t.Run("uploads directory to new bucket", func(t *testing.T) {
   181  		// Setup in-memory fs
   182  		fsys := afero.NewMemMapFs()
   183  		require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
   184  		// Setup mock api
   185  		defer gock.OffAll()
   186  		gock.New("http://127.0.0.1").
   187  			Get("/storage/v1/bucket").
   188  			Reply(http.StatusOK).
   189  			JSON([]storage.BucketResponse{})
   190  		gock.New("http://127.0.0.1").
   191  			Post("/storage/v1/object/tmp/readme.md").
   192  			Reply(http.StatusNotFound).
   193  			JSON(map[string]string{"error": "Bucket not found"})
   194  		gock.New("http://127.0.0.1").
   195  			Post("/storage/v1/bucket").
   196  			Reply(http.StatusOK).
   197  			JSON(storage.CreateBucketResponse{Name: "tmp"})
   198  		gock.New("http://127.0.0.1").
   199  			Post("/storage/v1/object/tmp/readme.md").
   200  			Reply(http.StatusOK)
   201  		// Run test
   202  		err := UploadStorageObjectAll(context.Background(), mockApi, "", "/tmp", 1, fsys)
   203  		// Check error
   204  		assert.NoError(t, err)
   205  		assert.Empty(t, apitest.ListUnmatchedRequests())
   206  	})
   207  
   208  	t.Run("throws error on failure to create bucket", func(t *testing.T) {
   209  		// Setup in-memory fs
   210  		fsys := afero.NewMemMapFs()
   211  		require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
   212  		// Setup mock api
   213  		defer gock.OffAll()
   214  		gock.New("http://127.0.0.1").
   215  			Get("/storage/v1/bucket").
   216  			Reply(http.StatusOK).
   217  			JSON([]storage.BucketResponse{})
   218  		gock.New("http://127.0.0.1").
   219  			Post("/storage/v1/object/tmp/readme.md").
   220  			Reply(http.StatusNotFound).
   221  			JSON(map[string]string{"error": "Bucket not found"})
   222  		gock.New("http://127.0.0.1").
   223  			Post("/storage/v1/bucket").
   224  			Reply(http.StatusServiceUnavailable)
   225  		// Run test
   226  		err := UploadStorageObjectAll(context.Background(), mockApi, "", "/tmp", 1, fsys)
   227  		// Check error
   228  		assert.ErrorContains(t, err, "Error status 503:")
   229  		assert.Empty(t, apitest.ListUnmatchedRequests())
   230  	})
   231  
   232  	t.Run("uploads directory to existing prefix", func(t *testing.T) {
   233  		// Setup in-memory fs
   234  		fsys := afero.NewMemMapFs()
   235  		require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
   236  		require.NoError(t, afero.WriteFile(fsys, "/tmp/docs/api.md", []byte{}, 0644))
   237  		// Setup mock api
   238  		defer gock.OffAll()
   239  		gock.New("http://127.0.0.1").
   240  			Post("/storage/v1/object/list/private").
   241  			Reply(http.StatusOK).
   242  			JSON([]storage.ObjectResponse{{
   243  				Name: "dir",
   244  			}})
   245  		gock.New("http://127.0.0.1").
   246  			Post("/storage/v1/object/private/dir/tmp/readme.md").
   247  			Reply(http.StatusOK)
   248  		gock.New("http://127.0.0.1").
   249  			Post("/storage/v1/object/private/dir/tmp/docs/api.md").
   250  			Reply(http.StatusOK)
   251  		// Run test
   252  		err := UploadStorageObjectAll(context.Background(), mockApi, "/private/dir/", "/tmp", 1, fsys)
   253  		// Check error
   254  		assert.NoError(t, err)
   255  		assert.Empty(t, apitest.ListUnmatchedRequests())
   256  	})
   257  
   258  	t.Run("uploads file to existing bucket", func(t *testing.T) {
   259  		// Setup in-memory fs
   260  		fsys := afero.NewMemMapFs()
   261  		require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
   262  		// Setup mock api
   263  		defer gock.OffAll()
   264  		gock.New("http://127.0.0.1").
   265  			Get("/storage/v1/bucket").
   266  			Reply(http.StatusOK).
   267  			JSON([]storage.BucketResponse{{
   268  				Id:        "private",
   269  				Name:      "private",
   270  				CreatedAt: "2023-10-13T17:48:58.491Z",
   271  				UpdatedAt: "2023-10-13T17:48:58.491Z",
   272  			}})
   273  		gock.New("http://127.0.0.1").
   274  			Post("/storage/v1/object/private/readme.md").
   275  			Reply(http.StatusOK)
   276  		// Run test
   277  		err := UploadStorageObjectAll(context.Background(), mockApi, "private", "/tmp/readme.md", 1, fsys)
   278  		// Check error
   279  		assert.NoError(t, err)
   280  		assert.Empty(t, apitest.ListUnmatchedRequests())
   281  	})
   282  
   283  	t.Run("uploads file to existing object", func(t *testing.T) {
   284  		// Setup in-memory fs
   285  		fsys := afero.NewMemMapFs()
   286  		require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
   287  		// Setup mock api
   288  		defer gock.OffAll()
   289  		fileObject := mockFile
   290  		fileObject.Name = "file"
   291  		gock.New("http://127.0.0.1").
   292  			Post("/storage/v1/object/list/private").
   293  			Reply(http.StatusOK).
   294  			JSON([]storage.ObjectResponse{fileObject})
   295  		gock.New("http://127.0.0.1").
   296  			Post("/storage/v1/object/private/file").
   297  			Reply(http.StatusOK)
   298  		// Run test
   299  		err := UploadStorageObjectAll(context.Background(), mockApi, "private/file", "/tmp/readme.md", 1, fsys)
   300  		// Check error
   301  		assert.NoError(t, err)
   302  		assert.Empty(t, apitest.ListUnmatchedRequests())
   303  	})
   304  
   305  	t.Run("throws error on service unavailable", func(t *testing.T) {
   306  		// Setup in-memory fs
   307  		fsys := afero.NewMemMapFs()
   308  		// Setup mock api
   309  		defer gock.OffAll()
   310  		gock.New("http://127.0.0.1").
   311  			Get("/storage/v1/bucket").
   312  			Reply(http.StatusServiceUnavailable)
   313  		// Run test
   314  		err := UploadStorageObjectAll(context.Background(), mockApi, "", ".", 1, fsys)
   315  		// Check error
   316  		assert.ErrorContains(t, err, "Error status 503:")
   317  		assert.Empty(t, apitest.ListUnmatchedRequests())
   318  	})
   319  }
   320  
   321  func TestDownloadAll(t *testing.T) {
   322  	t.Run("downloads buckets to existing directory", func(t *testing.T) {
   323  		// Setup in-memory fs
   324  		fsys := afero.NewMemMapFs()
   325  		// Setup mock api
   326  		defer gock.OffAll()
   327  		gock.New("http://127.0.0.1").
   328  			Get("/storage/v1/bucket").
   329  			Reply(http.StatusOK).
   330  			JSON([]storage.BucketResponse{{
   331  				Id:        "test",
   332  				Name:      "test",
   333  				Public:    true,
   334  				CreatedAt: "2023-10-13T17:48:58.491Z",
   335  				UpdatedAt: "2023-10-13T17:48:58.491Z",
   336  			}, {
   337  				Id:        "private",
   338  				Name:      "private",
   339  				CreatedAt: "2023-10-13T17:48:58.491Z",
   340  				UpdatedAt: "2023-10-13T17:48:58.491Z",
   341  			}})
   342  		gock.New("http://127.0.0.1").
   343  			Post("/storage/v1/object/list/private").
   344  			Reply(http.StatusOK).
   345  			JSON([]storage.ObjectResponse{})
   346  		gock.New("http://127.0.0.1").
   347  			Post("/storage/v1/object/list/test").
   348  			Reply(http.StatusOK).
   349  			JSON([]storage.ObjectResponse{})
   350  		// Run test
   351  		err := DownloadStorageObjectAll(context.Background(), mockApi, "", "/", 1, fsys)
   352  		// Check error
   353  		assert.NoError(t, err)
   354  		assert.Empty(t, apitest.ListUnmatchedRequests())
   355  		exists, err := afero.DirExists(fsys, "/private")
   356  		assert.NoError(t, err)
   357  		assert.True(t, exists)
   358  		exists, err = afero.DirExists(fsys, "/test")
   359  		assert.NoError(t, err)
   360  		assert.True(t, exists)
   361  	})
   362  
   363  	t.Run("downloads empty bucket to new directory", func(t *testing.T) {
   364  		// Setup in-memory fs
   365  		fsys := afero.NewMemMapFs()
   366  		// Setup mock api
   367  		defer gock.OffAll()
   368  		gock.New("http://127.0.0.1").
   369  			Get("/storage/v1/object/private").
   370  			Reply(http.StatusNotFound).
   371  			JSON(map[string]string{"error": "Not Found"})
   372  		gock.New("http://127.0.0.1").
   373  			Get("/storage/v1/bucket").
   374  			Reply(http.StatusOK).
   375  			JSON([]storage.BucketResponse{{
   376  				Id:        "private",
   377  				Name:      "private",
   378  				CreatedAt: "2023-10-13T17:48:58.491Z",
   379  				UpdatedAt: "2023-10-13T17:48:58.491Z",
   380  			}})
   381  		gock.New("http://127.0.0.1").
   382  			Post("/storage/v1/object/list/private").
   383  			Reply(http.StatusOK).
   384  			JSON([]storage.ObjectResponse{})
   385  		// Run test
   386  		err := DownloadStorageObjectAll(context.Background(), mockApi, "/private", "/tmp", 1, fsys)
   387  		// Check error
   388  		assert.NoError(t, err)
   389  		assert.Empty(t, apitest.ListUnmatchedRequests())
   390  		exists, err := afero.DirExists(fsys, "/private")
   391  		assert.NoError(t, err)
   392  		assert.False(t, exists)
   393  		exists, err = afero.DirExists(fsys, "/tmp")
   394  		assert.NoError(t, err)
   395  		assert.True(t, exists)
   396  	})
   397  
   398  	t.Run("throws error on empty directory", func(t *testing.T) {
   399  		// Setup in-memory fs
   400  		fsys := afero.NewMemMapFs()
   401  		// Setup mock api
   402  		defer gock.OffAll()
   403  		gock.New("http://127.0.0.1").
   404  			Post("/storage/v1/object/list/private").
   405  			Reply(http.StatusOK).
   406  			JSON([]storage.ObjectResponse{})
   407  		// Run test
   408  		err := DownloadStorageObjectAll(context.Background(), mockApi, "private/dir/", "/", 1, fsys)
   409  		// Check error
   410  		assert.ErrorContains(t, err, "Object not found: private/dir/")
   411  		assert.Empty(t, apitest.ListUnmatchedRequests())
   412  		exists, err := afero.DirExists(fsys, "/private")
   413  		assert.NoError(t, err)
   414  		assert.False(t, exists)
   415  	})
   416  
   417  	t.Run("downloads objects to existing directory", func(t *testing.T) {
   418  		// Setup in-memory fs
   419  		fsys := afero.NewMemMapFs()
   420  		// Setup mock api
   421  		defer gock.OffAll()
   422  		// Lists /private/tmp directory
   423  		gock.New("http://127.0.0.1").
   424  			Post("/storage/v1/object/list/private").
   425  			JSON(storage.ListObjectsQuery{
   426  				Prefix: "tmp/",
   427  				Search: "",
   428  				Limit:  storage.PAGE_LIMIT,
   429  				Offset: 0,
   430  			}).
   431  			Reply(http.StatusOK).
   432  			JSON([]storage.ObjectResponse{{
   433  				Name: "docs",
   434  			}, mockFile})
   435  		gock.New("http://127.0.0.1").
   436  			Get("/storage/v1/object/private/tmp/abstract.pdf").
   437  			Reply(http.StatusOK)
   438  		// Lists /private/tmp/docs directory
   439  		readme := mockFile
   440  		readme.Name = "readme.md"
   441  		gock.New("http://127.0.0.1").
   442  			Post("/storage/v1/object/list/private").
   443  			JSON(storage.ListObjectsQuery{
   444  				Prefix: "tmp/docs/",
   445  				Search: "",
   446  				Limit:  storage.PAGE_LIMIT,
   447  				Offset: 0,
   448  			}).
   449  			Reply(http.StatusOK).
   450  			JSON([]storage.ObjectResponse{readme})
   451  		gock.New("http://127.0.0.1").
   452  			Get("/storage/v1/object/private/tmp/docs/readme.md").
   453  			Reply(http.StatusOK)
   454  		// Run test
   455  		err := DownloadStorageObjectAll(context.Background(), mockApi, "private/tmp/", "/", 1, fsys)
   456  		// Check error
   457  		assert.NoError(t, err)
   458  		assert.Empty(t, apitest.ListUnmatchedRequests())
   459  		exists, err := afero.Exists(fsys, "/tmp/abstract.pdf")
   460  		assert.NoError(t, err)
   461  		assert.True(t, exists)
   462  		exists, err = afero.Exists(fsys, "/tmp/docs/readme.md")
   463  		assert.NoError(t, err)
   464  		assert.True(t, exists)
   465  	})
   466  
   467  	t.Run("downloads object to existing file", func(t *testing.T) {
   468  		// Setup in-memory fs
   469  		fsys := afero.NewMemMapFs()
   470  		// Setup mock api
   471  		defer gock.OffAll()
   472  		gock.New("http://127.0.0.1").
   473  			Post("/storage/v1/object/list/private").
   474  			Reply(http.StatusOK).
   475  			JSON([]storage.ObjectResponse{mockFile})
   476  		gock.New("http://127.0.0.1").
   477  			Get("/storage/v1/object/private/abstract.pdf").
   478  			Reply(http.StatusOK)
   479  		// Run test
   480  		err := DownloadStorageObjectAll(context.Background(), mockApi, "/private/abstract.pdf", "/tmp/file", 1, fsys)
   481  		// Check error
   482  		assert.NoError(t, err)
   483  		assert.Empty(t, apitest.ListUnmatchedRequests())
   484  		exists, err := afero.DirExists(fsys, "/private")
   485  		assert.NoError(t, err)
   486  		assert.False(t, exists)
   487  		exists, err = afero.Exists(fsys, "/tmp/file")
   488  		assert.NoError(t, err)
   489  		assert.True(t, exists)
   490  	})
   491  }