package rm import ( "context" "net/http" "testing" "github.com/h2non/gock" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/supabase/cli/internal/testing/apitest" "github.com/supabase/cli/internal/testing/fstest" "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 TestStorageRM(t *testing.T) { flags.ProjectRef = apitest.RandomProjectRef() // Setup valid access token token := apitest.RandomAccessToken(t) t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) t.Run("throws error on invalid url", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Run test err := Run(context.Background(), []string{":"}, false, fsys) // Check error assert.ErrorContains(t, err, "missing protocol scheme") }) t.Run("throws error on missing bucket", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Run test err := Run(context.Background(), []string{"ss:///"}, false, fsys) // Check error assert.ErrorIs(t, err, errMissingBucket) }) t.Run("throws error on missing flag", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Run test err := Run(context.Background(), []string{"ss:///private/"}, false, fsys) // Check error assert.ErrorIs(t, err, errMissingFlag) }) t.Run("removes multiple objects", func(t *testing.T) { t.Cleanup(fstest.MockStdin(t, "y")) // 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)). Delete("/storage/v1/object/private"). JSON(storage.DeleteObjectsRequest{Prefixes: []string{ "abstract.pdf", "docs/readme.md", }}). Reply(http.StatusOK). JSON([]storage.DeleteObjectsResponse{{ BucketId: "private", Version: "cf5c5c53-ee73-4806-84e3-7d92c954b436", Name: "abstract.pdf", Id: "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9", UpdatedAt: "2023-10-13T18:08:22.068Z", CreatedAt: "2023-10-13T18:08:22.068Z", LastAccessedAt: "2023-10-13T18:08:22.068Z", }}) // Run test err := Run(context.Background(), []string{ "ss:///private/abstract.pdf", "ss:///private/docs/readme.md", }, false, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("removes buckets and directories", func(t *testing.T) { t.Cleanup(fstest.MockStdin(t, "y")) // 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", }}) // Delete /test/ bucket gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)). Post("/storage/v1/object/list/test"). Reply(http.StatusOK). JSON([]storage.ObjectResponse{}) gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)). Delete("/storage/v1/object/test"). JSON(storage.DeleteObjectsRequest{Prefixes: []string{ "", }}). Reply(http.StatusOK). JSON([]storage.DeleteObjectsResponse{}) gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)). Post("/storage/v1/object/list/test"). Reply(http.StatusOK). JSON([]storage.ObjectResponse{}) gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)). Delete("/storage/v1/bucket/test"). Reply(http.StatusNotFound). JSON(map[string]string{"error": "Bucket not found"}) // Delete /private/docs/ directory gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)). Delete("/storage/v1/object/private"). JSON(storage.DeleteObjectsRequest{Prefixes: []string{ "docs", }}). Reply(http.StatusOK). JSON([]storage.DeleteObjectsResponse{}) gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)). Post("/storage/v1/object/list/private"). Reply(http.StatusOK). JSON([]storage.ObjectResponse{mockFile}) gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)). Delete("/storage/v1/object/private"). JSON(storage.DeleteObjectsRequest{Prefixes: []string{ "docs/abstract.pdf", }}). Reply(http.StatusOK). JSON([]storage.DeleteObjectsResponse{{ BucketId: "private", Version: "cf5c5c53-ee73-4806-84e3-7d92c954b436", Name: "abstract.pdf", Id: "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9", UpdatedAt: "2023-10-13T18:08:22.068Z", CreatedAt: "2023-10-13T18:08:22.068Z", LastAccessedAt: "2023-10-13T18:08:22.068Z", }}) // Run test err := Run(context.Background(), []string{ "ss:///test", "ss:///private/docs", }, true, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on delete failure", func(t *testing.T) { t.Cleanup(fstest.MockStdin(t, "y")) // 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)). Delete("/storage/v1/object/private"). Reply(http.StatusServiceUnavailable) // Run test err := Run(context.Background(), []string{"ss:///private"}, true, fsys) // Check error assert.ErrorContains(t, err, "Error status 503:") assert.Empty(t, apitest.ListUnmatchedRequests()) }) } func TestRemoveAll(t *testing.T) { t.Run("removes objects by prefix", func(t *testing.T) { // Setup mock api defer gock.OffAll() // List /private/tmp/ gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/private"). JSON(storage.ListObjectsQuery{ Prefix: "tmp/", Search: "", Limit: storage.PAGE_LIMIT, Offset: 0, }). Reply(http.StatusOK). JSON([]storage.ObjectResponse{{ Name: "docs", }}) // List /private/docs/ readme := mockFile readme.Name = "readme.md" gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/private"). JSON(storage.ListObjectsQuery{ Prefix: "tmp/docs/", Search: "", Limit: storage.PAGE_LIMIT, Offset: 0, }). Reply(http.StatusOK). JSON([]storage.ObjectResponse{mockFile, readme}) gock.New("http://127.0.0.1"). Delete("/storage/v1/object/private"). JSON(storage.DeleteObjectsRequest{Prefixes: []string{ "tmp/docs/abstract.pdf", "tmp/docs/readme.md", }}). Reply(http.StatusOK). JSON([]storage.DeleteObjectsResponse{{ BucketId: "private", Version: "cf5c5c53-ee73-4806-84e3-7d92c954b436", Name: "abstract.pdf", Id: "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9", UpdatedAt: "2023-10-13T18:08:22.068Z", CreatedAt: "2023-10-13T18:08:22.068Z", LastAccessedAt: "2023-10-13T18:08:22.068Z", }, { BucketId: "private", Version: "cf5c5c53-ee73-4806-84e3-7d92c954b436", Name: "readme.md", Id: "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9", UpdatedAt: "2023-10-13T18:08:22.068Z", CreatedAt: "2023-10-13T18:08:22.068Z", LastAccessedAt: "2023-10-13T18:08:22.068Z", }}) // Run test err := RemoveStoragePathAll(context.Background(), mockApi, "private", "tmp/") // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("removes empty bucket", func(t *testing.T) { // Setup mock api defer gock.OffAll() gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/private"). Reply(http.StatusOK). JSON([]storage.ObjectResponse{}) gock.New("http://127.0.0.1"). Delete("/storage/v1/bucket/private"). Reply(http.StatusOK). JSON(storage.DeleteBucketResponse{Message: "Successfully deleted"}) // Run test err := RemoveStoragePathAll(context.Background(), mockApi, "private", "") // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on empty directory", func(t *testing.T) { // Setup mock api defer gock.OffAll() gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/private"). Reply(http.StatusOK). JSON([]storage.ObjectResponse{}) // Run test err := RemoveStoragePathAll(context.Background(), mockApi, "private", "dir") // Check error assert.ErrorContains(t, err, "Object not found: private/dir") assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on service unavailable", func(t *testing.T) { // Setup mock api defer gock.OffAll() gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/private"). Reply(http.StatusServiceUnavailable) // Run test err := RemoveStoragePathAll(context.Background(), mockApi, "private", "") // Check error assert.ErrorContains(t, err, "Error status 503:") assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on delete failure", func(t *testing.T) { // Setup mock api defer gock.OffAll() gock.New("http://127.0.0.1"). Post("/storage/v1/object/list/private"). Reply(http.StatusOK). JSON([]storage.ObjectResponse{mockFile}) gock.New("http://127.0.0.1"). Delete("/storage/v1/object/private"). Reply(http.StatusServiceUnavailable) // Run test err := RemoveStoragePathAll(context.Background(), mockApi, "private", "") // Check error assert.ErrorContains(t, err, "Error status 503:") assert.Empty(t, apitest.ListUnmatchedRequests()) }) }