github.com/supabase/cli@v1.168.1/internal/storage/ls/ls_test.go (about)

     1  package ls
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"testing"
     8  
     9  	"github.com/spf13/afero"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/supabase/cli/internal/storage/client"
    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 TestStorageLS(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("lists buckets", func(t *testing.T) {
    49  		// Setup in-memory fs
    50  		fsys := afero.NewMemMapFs()
    51  		// Setup mock api
    52  		defer gock.OffAll()
    53  		gock.New(utils.DefaultApiHost).
    54  			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
    55  			Reply(http.StatusOK).
    56  			JSON([]api.ApiKeyResponse{{
    57  				Name:   "service_role",
    58  				ApiKey: "service-key",
    59  			}})
    60  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
    61  			Get("/storage/v1/bucket").
    62  			Reply(http.StatusOK).
    63  			JSON([]storage.BucketResponse{})
    64  		// Run test
    65  		err := Run(context.Background(), "ss:///", false, fsys)
    66  		// Check error
    67  		assert.NoError(t, err)
    68  	})
    69  
    70  	t.Run("throws error on invalid URL", func(t *testing.T) {
    71  		// Setup in-memory fs
    72  		fsys := afero.NewMemMapFs()
    73  		// Run test
    74  		err := Run(context.Background(), "", false, fsys)
    75  		// Check error
    76  		assert.ErrorIs(t, err, client.ErrInvalidURL)
    77  	})
    78  
    79  	t.Run("lists objects recursive", func(t *testing.T) {
    80  		// Setup in-memory fs
    81  		fsys := afero.NewMemMapFs()
    82  		// Setup mock api
    83  		defer gock.OffAll()
    84  		gock.New(utils.DefaultApiHost).
    85  			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
    86  			Reply(http.StatusOK).
    87  			JSON([]api.ApiKeyResponse{{
    88  				Name:   "service_role",
    89  				ApiKey: "service-key",
    90  			}})
    91  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
    92  			Get("/storage/v1/bucket").
    93  			Reply(http.StatusOK).
    94  			JSON([]storage.BucketResponse{{
    95  				Id:        "private",
    96  				Name:      "private",
    97  				CreatedAt: "2023-10-13T17:48:58.491Z",
    98  				UpdatedAt: "2023-10-13T17:48:58.491Z",
    99  			}})
   100  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
   101  			Post("/storage/v1/object/list/private").
   102  			Reply(http.StatusOK).
   103  			JSON([]storage.ObjectResponse{})
   104  		// Run test
   105  		err := Run(context.Background(), "ss:///", true, fsys)
   106  		// Check error
   107  		assert.NoError(t, err)
   108  	})
   109  }
   110  
   111  func TestListStoragePaths(t *testing.T) {
   112  	t.Run("lists bucket paths by prefix", func(t *testing.T) {
   113  		// Setup mock api
   114  		defer gock.OffAll()
   115  		gock.New("http://127.0.0.1").
   116  			Get("/storage/v1/bucket").
   117  			Reply(http.StatusOK).
   118  			JSON([]storage.BucketResponse{{
   119  				Id:        "test",
   120  				Name:      "test",
   121  				Public:    true,
   122  				CreatedAt: "2023-10-13T17:48:58.491Z",
   123  				UpdatedAt: "2023-10-13T17:48:58.491Z",
   124  			}, {
   125  				Id:        "private",
   126  				Name:      "private",
   127  				CreatedAt: "2023-10-13T17:48:58.491Z",
   128  				UpdatedAt: "2023-10-13T17:48:58.491Z",
   129  			}})
   130  		// Run test
   131  		paths, err := ListStoragePaths(context.Background(), mockApi, "te")
   132  		// Check error
   133  		assert.NoError(t, err)
   134  		assert.ElementsMatch(t, []string{"test/"}, paths)
   135  		assert.Empty(t, apitest.ListUnmatchedRequests())
   136  	})
   137  
   138  	t.Run("throws error on bucket service unavailable", func(t *testing.T) {
   139  		// Setup mock api
   140  		defer gock.OffAll()
   141  		gock.New("http://127.0.0.1").
   142  			Get("/storage/v1/bucket").
   143  			Reply(http.StatusServiceUnavailable)
   144  		// Run test
   145  		paths, err := ListStoragePaths(context.Background(), mockApi, "/")
   146  		// Check error
   147  		assert.ErrorContains(t, err, "Error status 503:")
   148  		assert.Empty(t, paths)
   149  		assert.Empty(t, apitest.ListUnmatchedRequests())
   150  	})
   151  
   152  	t.Run("lists object paths by prefix", func(t *testing.T) {
   153  		// Setup mock api
   154  		defer gock.OffAll()
   155  		gock.New("http://127.0.0.1").
   156  			Post("/storage/v1/object/list/bucket").
   157  			Reply(http.StatusOK).
   158  			JSON([]storage.ObjectResponse{{
   159  				Name: "folder",
   160  			}, mockFile})
   161  		// Run test
   162  		paths, err := ListStoragePaths(context.Background(), mockApi, "bucket/")
   163  		// Check error
   164  		assert.NoError(t, err)
   165  		assert.ElementsMatch(t, []string{"folder/", "abstract.pdf"}, paths)
   166  		assert.Empty(t, apitest.ListUnmatchedRequests())
   167  	})
   168  
   169  	t.Run("throws error on object service unavailable", func(t *testing.T) {
   170  		// Setup mock api
   171  		defer gock.OffAll()
   172  		gock.New("http://127.0.0.1").
   173  			Post("/storage/v1/object/list/bucket").
   174  			Reply(http.StatusServiceUnavailable)
   175  		// Run test
   176  		paths, err := ListStoragePaths(context.Background(), mockApi, "bucket/")
   177  		// Check error
   178  		assert.ErrorContains(t, err, "Error status 503:")
   179  		assert.Empty(t, paths)
   180  		assert.Empty(t, apitest.ListUnmatchedRequests())
   181  	})
   182  
   183  	t.Run("lists object paths with pagination", func(t *testing.T) {
   184  		// Setup mock api
   185  		defer gock.OffAll()
   186  		expected := make([]string, storage.PAGE_LIMIT)
   187  		resp := make([]storage.ObjectResponse, storage.PAGE_LIMIT)
   188  		for i := 0; i < len(resp); i++ {
   189  			resp[i] = storage.ObjectResponse{Name: fmt.Sprintf("dir_%d", i)}
   190  			expected[i] = resp[i].Name + "/"
   191  		}
   192  		gock.New("http://127.0.0.1").
   193  			Post("/storage/v1/object/list/bucket").
   194  			JSON(storage.ListObjectsQuery{
   195  				Prefix: "",
   196  				Search: "dir",
   197  				Limit:  storage.PAGE_LIMIT,
   198  				Offset: 0,
   199  			}).
   200  			Reply(http.StatusOK).
   201  			JSON(resp)
   202  		gock.New("http://127.0.0.1").
   203  			Post("/storage/v1/object/list/bucket").
   204  			JSON(storage.ListObjectsQuery{
   205  				Prefix: "",
   206  				Search: "dir",
   207  				Limit:  storage.PAGE_LIMIT,
   208  				Offset: storage.PAGE_LIMIT,
   209  			}).
   210  			Reply(http.StatusOK).
   211  			JSON([]storage.ObjectResponse{})
   212  		// Run test
   213  		paths, err := ListStoragePaths(context.Background(), mockApi, "/bucket/dir")
   214  		// Check error
   215  		assert.NoError(t, err)
   216  		assert.ElementsMatch(t, expected, paths)
   217  		assert.Empty(t, apitest.ListUnmatchedRequests())
   218  	})
   219  }
   220  
   221  func TestListStoragePathsAll(t *testing.T) {
   222  	t.Run("lists nested object paths", func(t *testing.T) {
   223  		// Setup mock api
   224  		defer gock.OffAll()
   225  		// List buckets
   226  		gock.New("http://127.0.0.1").
   227  			Get("/storage/v1/bucket").
   228  			Reply(http.StatusOK).
   229  			JSON([]storage.BucketResponse{{
   230  				Id:        "test",
   231  				Name:      "test",
   232  				Public:    true,
   233  				CreatedAt: "2023-10-13T17:48:58.491Z",
   234  				UpdatedAt: "2023-10-13T17:48:58.491Z",
   235  			}, {
   236  				Id:        "private",
   237  				Name:      "private",
   238  				CreatedAt: "2023-10-13T17:48:58.491Z",
   239  				UpdatedAt: "2023-10-13T17:48:58.491Z",
   240  			}})
   241  		// List folders
   242  		gock.New("http://127.0.0.1").
   243  			Post("/storage/v1/object/list/test").
   244  			JSON(storage.ListObjectsQuery{
   245  				Prefix: "",
   246  				Search: "",
   247  				Limit:  storage.PAGE_LIMIT,
   248  				Offset: 0,
   249  			}).
   250  			Reply(http.StatusOK).
   251  			JSON([]storage.ObjectResponse{})
   252  		gock.New("http://127.0.0.1").
   253  			Post("/storage/v1/object/list/private").
   254  			JSON(storage.ListObjectsQuery{
   255  				Prefix: "",
   256  				Search: "",
   257  				Limit:  storage.PAGE_LIMIT,
   258  				Offset: 0,
   259  			}).
   260  			Reply(http.StatusOK).
   261  			JSON([]storage.ObjectResponse{{
   262  				Name: "folder",
   263  			}})
   264  		// List files
   265  		gock.New("http://127.0.0.1").
   266  			Post("/storage/v1/object/list/private").
   267  			JSON(storage.ListObjectsQuery{
   268  				Prefix: "folder/",
   269  				Search: "",
   270  				Limit:  storage.PAGE_LIMIT,
   271  				Offset: 0,
   272  			}).
   273  			Reply(http.StatusOK).
   274  			JSON([]storage.ObjectResponse{mockFile})
   275  		// Run test
   276  		paths, err := ListStoragePathsAll(context.Background(), mockApi, "")
   277  		// Check error
   278  		assert.NoError(t, err)
   279  		assert.ElementsMatch(t, []string{"private/folder/abstract.pdf", "test/"}, paths)
   280  		assert.Empty(t, apitest.ListUnmatchedRequests())
   281  	})
   282  
   283  	t.Run("returns partial result on error", func(t *testing.T) {
   284  		// Setup mock api
   285  		defer gock.OffAll()
   286  		// List folders
   287  		gock.New("http://127.0.0.1").
   288  			Post("/storage/v1/object/list/private").
   289  			JSON(storage.ListObjectsQuery{
   290  				Prefix: "",
   291  				Search: "",
   292  				Limit:  storage.PAGE_LIMIT,
   293  				Offset: 0,
   294  			}).
   295  			Reply(http.StatusOK).
   296  			JSON([]storage.ObjectResponse{{
   297  				Name: "error",
   298  			}, mockFile})
   299  		gock.New("http://127.0.0.1").
   300  			Post("/storage/v1/object/list/private").
   301  			JSON(storage.ListObjectsQuery{
   302  				Prefix: "empty/",
   303  				Search: "",
   304  				Limit:  storage.PAGE_LIMIT,
   305  				Offset: 0,
   306  			}).
   307  			Reply(http.StatusOK).
   308  			JSON([]storage.ObjectResponse{})
   309  		gock.New("http://127.0.0.1").
   310  			Post("/storage/v1/object/list/private").
   311  			JSON(storage.ListObjectsQuery{
   312  				Prefix: "error/",
   313  				Search: "",
   314  				Limit:  storage.PAGE_LIMIT,
   315  				Offset: 0,
   316  			}).
   317  			Reply(http.StatusServiceUnavailable)
   318  		// Run test
   319  		paths, err := ListStoragePathsAll(context.Background(), mockApi, "private/")
   320  		// Check error
   321  		assert.ErrorContains(t, err, "Error status 503:")
   322  		assert.ElementsMatch(t, []string{"private/abstract.pdf"}, paths)
   323  		assert.Empty(t, apitest.ListUnmatchedRequests())
   324  	})
   325  }