package ls import ( "context" "fmt" "net/http" "testing" "github.com/h2non/gock" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/supabase/cli/internal/storage/client" "github.com/supabase/cli/internal/testing/apitest" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/pkg/api" "github.com/supabase/cli/pkg/cast" "github.com/supabase/cli/pkg/fetcher" "github.com/supabase/cli/pkg/storage" ) var mockFile = storage.ObjectResponse{ Name: "abstract.pdf", Id: cast.Ptr("9b7f9f48-17a6-4ca8-b14a-39b0205a63e9"), UpdatedAt: cast.Ptr("2023-10-13T18:08:22.068Z"), CreatedAt: cast.Ptr("2023-10-13T18:08:22.068Z"), LastAccessedAt: cast.Ptr("2023-10-13T18:08:22.068Z"), Metadata: &storage.ObjectMetadata{ ETag: `"887ea9be3c68e6f2fca7fd2d7c77d8fe"`, Size: 82702, Mimetype: "application/pdf", CacheControl: "max-age=3600", LastModified: "2023-10-13T18:08:22.000Z", ContentLength: 82702, HttpStatusCode: 200, }, } var mockApi = storage.StorageAPI{Fetcher: fetcher.NewFetcher( "http://127.0.0.1", )} func TestStorageLS(t *testing.T) { flags.ProjectRef = apitest.RandomProjectRef() // Setup valid access token token := apitest.RandomAccessToken(t) t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) t.Run("lists buckets", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup mock api defer gock.OffAll() gock.New(utils.DefaultApiHost). Get("/v1/projects/" + flags.ProjectRef + "/api-keys"). Reply(http.StatusOK). JSON([]api.ApiKeyResponse{{ Name: "service_role", ApiKey: "service-key", }}) gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)). Get("/storage/v1/bucket"). Reply(http.StatusOK). JSON([]storage.BucketResponse{}) // Run test err := Run(context.Background(), "ss:///", false, fsys) // Check error assert.NoError(t, err) }) t.Run("throws error on invalid URL", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Run test err := Run(context.Background(), "", false, fsys) // Check error assert.ErrorIs(t, err, client.ErrInvalidURL) }) t.Run("lists objects recursive", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup mock api defer gock.OffAll() gock.New(utils.DefaultApiHost). Get("/v1/projects/" + flags.ProjectRef + "/api-keys"). Reply(http.StatusOK). JSON([]api.ApiKeyResponse{{ Name: "service_role", ApiKey: "service-key", }}) gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)). Get("/storage/v1/bucket"). Reply(http.StatusOK). JSON([]storage.BucketResponse{{ Id: "private", Name: "private", CreatedAt: "2023-10-13T17:48:58.491Z", UpdatedAt: "2023-10-13T17:48:58.491Z", }}) gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)). Post("/storage/v1/object/list/private"). Reply(http.StatusOK). JSON([]storage.ObjectResponse{}) // Run test err := Run(context.Background(), "ss:///", true, fsys) // Check error assert.NoError(t, err) }) } func TestListStoragePaths(t *testing.T) { t.Run("lists bucket paths by prefix", func(t *testing.T) { // Setup mock api defer gock.OffAll() gock.New("http://127.0.0.1"). Get("/storage/v1/bucket"). Reply(http.StatusOK). JSON([]storage.BucketResponse{{ Id: "test", Name: "test", Public: true, CreatedAt: "2023-10-13T17:48:58.491Z", UpdatedAt: "2023-10-13T17:48:58.491Z", }, { Id: "private", Name: "private", CreatedAt: "2023-10-13T17:48:58.491Z", UpdatedAt: "2023-10-13T17:48:58.491Z", }}) // Run test paths, err := ListStoragePaths(context.Background(), mockApi, "te") // Check error assert.NoError(t, err) assert.ElementsMatch(t, []string{"test/"}, paths) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on bucket service unavailable", func(t *testing.T) { // Setup mock api defer gock.OffAll() gock.New("http://127.0.0.1"). Get("/storage/v1/bucket"). Reply(http.StatusServiceUnavailable) // Run test paths, err := ListStoragePaths(context.Background(), mockApi, "/") // Check error assert.ErrorContains(t, err, "Error status 503:") assert.Empty(t, paths) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("lists object paths by prefix", func(t *testing.T) { // Setup mock api defer gock.OffAll() gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/bucket"). Reply(http.StatusOK). JSON([]storage.ObjectResponse{{ Name: "folder", }, mockFile}) // Run test paths, err := ListStoragePaths(context.Background(), mockApi, "bucket/") // Check error assert.NoError(t, err) assert.ElementsMatch(t, []string{"folder/", "abstract.pdf"}, paths) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on object service unavailable", func(t *testing.T) { // Setup mock api defer gock.OffAll() gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/bucket"). Reply(http.StatusServiceUnavailable) // Run test paths, err := ListStoragePaths(context.Background(), mockApi, "bucket/") // Check error assert.ErrorContains(t, err, "Error status 503:") assert.Empty(t, paths) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("lists object paths with pagination", func(t *testing.T) { // Setup mock api defer gock.OffAll() expected := make([]string, storage.PAGE_LIMIT) resp := make([]storage.ObjectResponse, storage.PAGE_LIMIT) for i := 0; i < len(resp); i++ { resp[i] = storage.ObjectResponse{Name: fmt.Sprintf("dir_%d", i)} expected[i] = resp[i].Name + "/" } gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/bucket"). JSON(storage.ListObjectsQuery{ Prefix: "", Search: "dir", Limit: storage.PAGE_LIMIT, Offset: 0, }). Reply(http.StatusOK). JSON(resp) gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/bucket"). JSON(storage.ListObjectsQuery{ Prefix: "", Search: "dir", Limit: storage.PAGE_LIMIT, Offset: storage.PAGE_LIMIT, }). Reply(http.StatusOK). JSON([]storage.ObjectResponse{}) // Run test paths, err := ListStoragePaths(context.Background(), mockApi, "/bucket/dir") // Check error assert.NoError(t, err) assert.ElementsMatch(t, expected, paths) assert.Empty(t, apitest.ListUnmatchedRequests()) }) } func TestListStoragePathsAll(t *testing.T) { t.Run("lists nested object paths", func(t *testing.T) { // Setup mock api defer gock.OffAll() // List buckets gock.New("http://127.0.0.1"). Get("/storage/v1/bucket"). Reply(http.StatusOK). JSON([]storage.BucketResponse{{ Id: "test", Name: "test", Public: true, CreatedAt: "2023-10-13T17:48:58.491Z", UpdatedAt: "2023-10-13T17:48:58.491Z", }, { Id: "private", Name: "private", CreatedAt: "2023-10-13T17:48:58.491Z", UpdatedAt: "2023-10-13T17:48:58.491Z", }}) // List folders gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/test"). JSON(storage.ListObjectsQuery{ Prefix: "", Search: "", Limit: storage.PAGE_LIMIT, Offset: 0, }). Reply(http.StatusOK). JSON([]storage.ObjectResponse{}) gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/private"). JSON(storage.ListObjectsQuery{ Prefix: "", Search: "", Limit: storage.PAGE_LIMIT, Offset: 0, }). Reply(http.StatusOK). JSON([]storage.ObjectResponse{{ Name: "folder", }}) // List files gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/private"). JSON(storage.ListObjectsQuery{ Prefix: "folder/", Search: "", Limit: storage.PAGE_LIMIT, Offset: 0, }). Reply(http.StatusOK). JSON([]storage.ObjectResponse{mockFile}) // Run test paths, err := ListStoragePathsAll(context.Background(), mockApi, "") // Check error assert.NoError(t, err) assert.ElementsMatch(t, []string{"private/folder/abstract.pdf", "test/"}, paths) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("returns partial result on error", func(t *testing.T) { // Setup mock api defer gock.OffAll() // List folders gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/private"). JSON(storage.ListObjectsQuery{ Prefix: "", Search: "", Limit: storage.PAGE_LIMIT, Offset: 0, }). Reply(http.StatusOK). JSON([]storage.ObjectResponse{{ Name: "error", }, mockFile}) gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/private"). JSON(storage.ListObjectsQuery{ Prefix: "empty/", Search: "", Limit: storage.PAGE_LIMIT, Offset: 0, }). Reply(http.StatusOK). JSON([]storage.ObjectResponse{}) gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/private"). JSON(storage.ListObjectsQuery{ Prefix: "error/", Search: "", Limit: storage.PAGE_LIMIT, Offset: 0, }). Reply(http.StatusServiceUnavailable) // Run test paths, err := ListStoragePathsAll(context.Background(), mockApi, "private/") // Check error assert.ErrorContains(t, err, "Error status 503:") assert.ElementsMatch(t, []string{"private/abstract.pdf"}, paths) assert.Empty(t, apitest.ListUnmatchedRequests()) }) }