github.com/supabase/cli@v1.168.1/internal/storage/rm/rm_test.go (about)

     1  package rm
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"testing"
     7  
     8  	"github.com/spf13/afero"
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/supabase/cli/internal/testing/apitest"
    11  	"github.com/supabase/cli/internal/testing/fstest"
    12  	"github.com/supabase/cli/internal/utils"
    13  	"github.com/supabase/cli/internal/utils/flags"
    14  	"github.com/supabase/cli/pkg/api"
    15  	"github.com/supabase/cli/pkg/fetcher"
    16  	"github.com/supabase/cli/pkg/storage"
    17  	"gopkg.in/h2non/gock.v1"
    18  )
    19  
    20  var mockFile = storage.ObjectResponse{
    21  	Name:           "abstract.pdf",
    22  	Id:             utils.Ptr("9b7f9f48-17a6-4ca8-b14a-39b0205a63e9"),
    23  	UpdatedAt:      utils.Ptr("2023-10-13T18:08:22.068Z"),
    24  	CreatedAt:      utils.Ptr("2023-10-13T18:08:22.068Z"),
    25  	LastAccessedAt: utils.Ptr("2023-10-13T18:08:22.068Z"),
    26  	Metadata: &storage.ObjectMetadata{
    27  		ETag:           `"887ea9be3c68e6f2fca7fd2d7c77d8fe"`,
    28  		Size:           82702,
    29  		Mimetype:       "application/pdf",
    30  		CacheControl:   "max-age=3600",
    31  		LastModified:   "2023-10-13T18:08:22.000Z",
    32  		ContentLength:  82702,
    33  		HttpStatusCode: 200,
    34  	},
    35  }
    36  
    37  var mockApi = storage.StorageAPI{Fetcher: fetcher.NewFetcher(
    38  	"http://127.0.0.1",
    39  )}
    40  
    41  func TestStorageRM(t *testing.T) {
    42  	flags.ProjectRef = apitest.RandomProjectRef()
    43  	// Setup valid access token
    44  	token := apitest.RandomAccessToken(t)
    45  	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
    46  
    47  	t.Run("throws error on invalid url", func(t *testing.T) {
    48  		// Setup in-memory fs
    49  		fsys := afero.NewMemMapFs()
    50  		// Run test
    51  		err := Run(context.Background(), []string{":"}, false, fsys)
    52  		// Check error
    53  		assert.ErrorContains(t, err, "missing protocol scheme")
    54  	})
    55  
    56  	t.Run("throws error on missing bucket", func(t *testing.T) {
    57  		// Setup in-memory fs
    58  		fsys := afero.NewMemMapFs()
    59  		// Run test
    60  		err := Run(context.Background(), []string{"ss:///"}, false, fsys)
    61  		// Check error
    62  		assert.ErrorIs(t, err, errMissingBucket)
    63  	})
    64  
    65  	t.Run("throws error on missing flag", func(t *testing.T) {
    66  		// Setup in-memory fs
    67  		fsys := afero.NewMemMapFs()
    68  		// Run test
    69  		err := Run(context.Background(), []string{"ss:///private/"}, false, fsys)
    70  		// Check error
    71  		assert.ErrorIs(t, err, errMissingFlag)
    72  	})
    73  
    74  	t.Run("removes multiple objects", func(t *testing.T) {
    75  		defer fstest.MockStdin(t, "y")()
    76  		// Setup in-memory fs
    77  		fsys := afero.NewMemMapFs()
    78  		// Setup mock api
    79  		defer gock.OffAll()
    80  		gock.New(utils.DefaultApiHost).
    81  			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
    82  			Reply(http.StatusOK).
    83  			JSON([]api.ApiKeyResponse{{
    84  				Name:   "service_role",
    85  				ApiKey: "service-key",
    86  			}})
    87  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
    88  			Delete("/storage/v1/object/private").
    89  			JSON(storage.DeleteObjectsRequest{Prefixes: []string{
    90  				"abstract.pdf",
    91  				"docs/readme.md",
    92  			}}).
    93  			Reply(http.StatusOK).
    94  			JSON([]storage.DeleteObjectsResponse{{
    95  				BucketId:       "private",
    96  				Version:        "cf5c5c53-ee73-4806-84e3-7d92c954b436",
    97  				Name:           "abstract.pdf",
    98  				Id:             "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9",
    99  				UpdatedAt:      "2023-10-13T18:08:22.068Z",
   100  				CreatedAt:      "2023-10-13T18:08:22.068Z",
   101  				LastAccessedAt: "2023-10-13T18:08:22.068Z",
   102  			}})
   103  		// Run test
   104  		err := Run(context.Background(), []string{
   105  			"ss:///private/abstract.pdf",
   106  			"ss:///private/docs/readme.md",
   107  		}, false, fsys)
   108  		// Check error
   109  		assert.NoError(t, err)
   110  		assert.Empty(t, apitest.ListUnmatchedRequests())
   111  	})
   112  
   113  	t.Run("removes buckets and directories", func(t *testing.T) {
   114  		defer fstest.MockStdin(t, "y")()
   115  		// Setup in-memory fs
   116  		fsys := afero.NewMemMapFs()
   117  		// Setup mock api
   118  		defer gock.OffAll()
   119  		gock.New(utils.DefaultApiHost).
   120  			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
   121  			Reply(http.StatusOK).
   122  			JSON([]api.ApiKeyResponse{{
   123  				Name:   "service_role",
   124  				ApiKey: "service-key",
   125  			}})
   126  		// Delete /test/ bucket
   127  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
   128  			Post("/storage/v1/object/list/test").
   129  			Reply(http.StatusOK).
   130  			JSON([]storage.ObjectResponse{})
   131  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
   132  			Delete("/storage/v1/object/test").
   133  			JSON(storage.DeleteObjectsRequest{Prefixes: []string{
   134  				"",
   135  			}}).
   136  			Reply(http.StatusOK).
   137  			JSON([]storage.DeleteObjectsResponse{})
   138  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
   139  			Post("/storage/v1/object/list/test").
   140  			Reply(http.StatusOK).
   141  			JSON([]storage.ObjectResponse{})
   142  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
   143  			Delete("/storage/v1/bucket/test").
   144  			Reply(http.StatusNotFound).
   145  			JSON(map[string]string{"error": "Bucket not found"})
   146  		// Delete /private/docs/ directory
   147  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
   148  			Delete("/storage/v1/object/private").
   149  			JSON(storage.DeleteObjectsRequest{Prefixes: []string{
   150  				"docs",
   151  			}}).
   152  			Reply(http.StatusOK).
   153  			JSON([]storage.DeleteObjectsResponse{})
   154  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
   155  			Post("/storage/v1/object/list/private").
   156  			Reply(http.StatusOK).
   157  			JSON([]storage.ObjectResponse{mockFile})
   158  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
   159  			Delete("/storage/v1/object/private").
   160  			JSON(storage.DeleteObjectsRequest{Prefixes: []string{
   161  				"docs/abstract.pdf",
   162  			}}).
   163  			Reply(http.StatusOK).
   164  			JSON([]storage.DeleteObjectsResponse{{
   165  				BucketId:       "private",
   166  				Version:        "cf5c5c53-ee73-4806-84e3-7d92c954b436",
   167  				Name:           "abstract.pdf",
   168  				Id:             "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9",
   169  				UpdatedAt:      "2023-10-13T18:08:22.068Z",
   170  				CreatedAt:      "2023-10-13T18:08:22.068Z",
   171  				LastAccessedAt: "2023-10-13T18:08:22.068Z",
   172  			}})
   173  		// Run test
   174  		err := Run(context.Background(), []string{
   175  			"ss:///test",
   176  			"ss:///private/docs",
   177  		}, true, fsys)
   178  		// Check error
   179  		assert.NoError(t, err)
   180  		assert.Empty(t, apitest.ListUnmatchedRequests())
   181  	})
   182  
   183  	t.Run("throws error on delete failure", func(t *testing.T) {
   184  		defer fstest.MockStdin(t, "y")()
   185  		// Setup in-memory fs
   186  		fsys := afero.NewMemMapFs()
   187  		// Setup mock api
   188  		defer gock.OffAll()
   189  		gock.New(utils.DefaultApiHost).
   190  			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
   191  			Reply(http.StatusOK).
   192  			JSON([]api.ApiKeyResponse{{
   193  				Name:   "service_role",
   194  				ApiKey: "service-key",
   195  			}})
   196  		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
   197  			Delete("/storage/v1/object/private").
   198  			Reply(http.StatusServiceUnavailable)
   199  		// Run test
   200  		err := Run(context.Background(), []string{"ss:///private"}, true, fsys)
   201  		// Check error
   202  		assert.ErrorContains(t, err, "Error status 503:")
   203  		assert.Empty(t, apitest.ListUnmatchedRequests())
   204  	})
   205  }
   206  
   207  func TestRemoveAll(t *testing.T) {
   208  	t.Run("removes objects by prefix", func(t *testing.T) {
   209  		// Setup mock api
   210  		defer gock.OffAll()
   211  		// List /private/tmp/
   212  		gock.New("http://127.0.0.1").
   213  			Post("/storage/v1/object/list/private").
   214  			JSON(storage.ListObjectsQuery{
   215  				Prefix: "tmp/",
   216  				Search: "",
   217  				Limit:  storage.PAGE_LIMIT,
   218  				Offset: 0,
   219  			}).
   220  			Reply(http.StatusOK).
   221  			JSON([]storage.ObjectResponse{{
   222  				Name: "docs",
   223  			}})
   224  		// List /private/docs/
   225  		readme := mockFile
   226  		readme.Name = "readme.md"
   227  		gock.New("http://127.0.0.1").
   228  			Post("/storage/v1/object/list/private").
   229  			JSON(storage.ListObjectsQuery{
   230  				Prefix: "tmp/docs/",
   231  				Search: "",
   232  				Limit:  storage.PAGE_LIMIT,
   233  				Offset: 0,
   234  			}).
   235  			Reply(http.StatusOK).
   236  			JSON([]storage.ObjectResponse{mockFile, readme})
   237  		gock.New("http://127.0.0.1").
   238  			Delete("/storage/v1/object/private").
   239  			JSON(storage.DeleteObjectsRequest{Prefixes: []string{
   240  				"tmp/docs/abstract.pdf",
   241  				"tmp/docs/readme.md",
   242  			}}).
   243  			Reply(http.StatusOK).
   244  			JSON([]storage.DeleteObjectsResponse{{
   245  				BucketId:       "private",
   246  				Version:        "cf5c5c53-ee73-4806-84e3-7d92c954b436",
   247  				Name:           "abstract.pdf",
   248  				Id:             "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9",
   249  				UpdatedAt:      "2023-10-13T18:08:22.068Z",
   250  				CreatedAt:      "2023-10-13T18:08:22.068Z",
   251  				LastAccessedAt: "2023-10-13T18:08:22.068Z",
   252  			}, {
   253  				BucketId:       "private",
   254  				Version:        "cf5c5c53-ee73-4806-84e3-7d92c954b436",
   255  				Name:           "readme.md",
   256  				Id:             "9b7f9f48-17a6-4ca8-b14a-39b0205a63e9",
   257  				UpdatedAt:      "2023-10-13T18:08:22.068Z",
   258  				CreatedAt:      "2023-10-13T18:08:22.068Z",
   259  				LastAccessedAt: "2023-10-13T18:08:22.068Z",
   260  			}})
   261  		// Run test
   262  		err := RemoveStoragePathAll(context.Background(), mockApi, "private", "tmp/")
   263  		// Check error
   264  		assert.NoError(t, err)
   265  		assert.Empty(t, apitest.ListUnmatchedRequests())
   266  	})
   267  
   268  	t.Run("removes empty bucket", func(t *testing.T) {
   269  		// Setup mock api
   270  		defer gock.OffAll()
   271  		gock.New("http://127.0.0.1").
   272  			Post("/storage/v1/object/list/private").
   273  			Reply(http.StatusOK).
   274  			JSON([]storage.ObjectResponse{})
   275  		gock.New("http://127.0.0.1").
   276  			Delete("/storage/v1/bucket/private").
   277  			Reply(http.StatusOK).
   278  			JSON(storage.DeleteBucketResponse{Message: "Successfully deleted"})
   279  		// Run test
   280  		err := RemoveStoragePathAll(context.Background(), mockApi, "private", "")
   281  		// Check error
   282  		assert.NoError(t, err)
   283  		assert.Empty(t, apitest.ListUnmatchedRequests())
   284  	})
   285  
   286  	t.Run("throws error on empty directory", func(t *testing.T) {
   287  		// Setup mock api
   288  		defer gock.OffAll()
   289  		gock.New("http://127.0.0.1").
   290  			Post("/storage/v1/object/list/private").
   291  			Reply(http.StatusOK).
   292  			JSON([]storage.ObjectResponse{})
   293  		// Run test
   294  		err := RemoveStoragePathAll(context.Background(), mockApi, "private", "dir")
   295  		// Check error
   296  		assert.ErrorContains(t, err, "Object not found: private/dir")
   297  		assert.Empty(t, apitest.ListUnmatchedRequests())
   298  	})
   299  
   300  	t.Run("throws error on service unavailable", func(t *testing.T) {
   301  		// Setup mock api
   302  		defer gock.OffAll()
   303  		gock.New("http://127.0.0.1").
   304  			Post("/storage/v1/object/list/private").
   305  			Reply(http.StatusServiceUnavailable)
   306  		// Run test
   307  		err := RemoveStoragePathAll(context.Background(), mockApi, "private", "")
   308  		// Check error
   309  		assert.ErrorContains(t, err, "Error status 503:")
   310  		assert.Empty(t, apitest.ListUnmatchedRequests())
   311  	})
   312  
   313  	t.Run("throws error on delete failure", func(t *testing.T) {
   314  		// Setup mock api
   315  		defer gock.OffAll()
   316  		gock.New("http://127.0.0.1").
   317  			Post("/storage/v1/object/list/private").
   318  			Reply(http.StatusOK).
   319  			JSON([]storage.ObjectResponse{mockFile})
   320  		gock.New("http://127.0.0.1").
   321  			Delete("/storage/v1/object/private").
   322  			Reply(http.StatusServiceUnavailable)
   323  		// Run test
   324  		err := RemoveStoragePathAll(context.Background(), mockApi, "private", "")
   325  		// Check error
   326  		assert.ErrorContains(t, err, "Error status 503:")
   327  		assert.Empty(t, apitest.ListUnmatchedRequests())
   328  	})
   329  }