github.com/supabase/cli@v1.168.1/internal/storage/mv/mv_test.go (about)

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