supabase-cli/internal/storage/cp/cp_test.go

493 lines
15 KiB
Go

package cp
import (
"context"
"io/fs"
"net/http"
"testing"
"github.com/h2non/gock"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"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 TestStorageCP(t *testing.T) {
flags.ProjectRef = apitest.RandomProjectRef()
// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
t.Run("copy local to remote", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, "/tmp/file", []byte{}, 0644))
// 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)).
Post("/storage/v1/object/private/file").
Reply(http.StatusOK)
// Run test
err := Run(context.Background(), "/tmp/file", "ss:///private/file", false, 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on missing file", 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(), "abstract.pdf", "ss:///private", true, 1, fsys)
// Check error
assert.ErrorIs(t, err, fs.ErrNotExist)
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("copy remote to local", 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/object/private/file").
Reply(http.StatusOK)
// Run test
err := Run(context.Background(), "ss:///private/file", "abstract.pdf", false, 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
exists, err := afero.Exists(fsys, "abstract.pdf")
assert.NoError(t, err)
assert.True(t, exists)
})
t.Run("throws error on missing bucket", 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:///private", ".", true, 1, fsys)
// Check error
assert.ErrorContains(t, err, "Object not found: /private")
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on invalid src", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Run test
err := Run(context.Background(), ":", ".", false, 1, fsys)
// Check error
assert.ErrorContains(t, err, "missing protocol scheme")
})
t.Run("throws error on invalid dst", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Run test
err := Run(context.Background(), ".", ":", false, 1, fsys)
// Check error
assert.ErrorContains(t, err, "missing protocol scheme")
})
t.Run("throws error on unsupported operation", 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",
}})
// Run test
err := Run(context.Background(), ".", ".", false, 1, fsys)
// Check error
assert.ErrorIs(t, err, errUnsupportedOperation)
})
}
func TestUploadAll(t *testing.T) {
t.Run("uploads directory to new bucket", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
// Setup mock api
defer gock.OffAll()
gock.New("http://127.0.0.1").
Get("/storage/v1/bucket").
Reply(http.StatusOK).
JSON([]storage.BucketResponse{})
gock.New("http://127.0.0.1").
Post("/storage/v1/object/tmp/readme.md").
Reply(http.StatusNotFound).
JSON(map[string]string{"error": "Bucket not found"})
gock.New("http://127.0.0.1").
Post("/storage/v1/bucket").
Reply(http.StatusOK).
JSON(storage.CreateBucketResponse{Name: "tmp"})
gock.New("http://127.0.0.1").
Post("/storage/v1/object/tmp/readme.md").
Reply(http.StatusOK)
// Run test
err := UploadStorageObjectAll(context.Background(), mockApi, "", "/tmp", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on failure to create bucket", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
// Setup mock api
defer gock.OffAll()
gock.New("http://127.0.0.1").
Get("/storage/v1/bucket").
Reply(http.StatusOK).
JSON([]storage.BucketResponse{})
gock.New("http://127.0.0.1").
Post("/storage/v1/object/tmp/readme.md").
Reply(http.StatusNotFound).
JSON(map[string]string{"error": "Bucket not found"})
gock.New("http://127.0.0.1").
Post("/storage/v1/bucket").
Reply(http.StatusServiceUnavailable)
// Run test
err := UploadStorageObjectAll(context.Background(), mockApi, "", "/tmp", 1, fsys)
// Check error
assert.ErrorContains(t, err, "Error status 503:")
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("uploads directory to existing prefix", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
require.NoError(t, afero.WriteFile(fsys, "/tmp/docs/api.md", []byte{}, 0644))
// 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{{
Name: "dir",
}})
gock.New("http://127.0.0.1").
Post("/storage/v1/object/private/dir/tmp/readme.md").
Reply(http.StatusOK)
gock.New("http://127.0.0.1").
Post("/storage/v1/object/private/dir/tmp/docs/api.md").
Reply(http.StatusOK)
// Run test
err := UploadStorageObjectAll(context.Background(), mockApi, "/private/dir/", "/tmp", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("uploads file to existing bucket", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
// Setup mock api
defer gock.OffAll()
gock.New("http://127.0.0.1").
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("http://127.0.0.1").
Post("/storage/v1/object/private/readme.md").
Reply(http.StatusOK)
// Run test
err := UploadStorageObjectAll(context.Background(), mockApi, "private", "/tmp/readme.md", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("uploads file to existing object", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
// Setup mock api
defer gock.OffAll()
fileObject := mockFile
fileObject.Name = "file"
gock.New("http://127.0.0.1").
Post("/storage/v1/object/list/private").
Reply(http.StatusOK).
JSON([]storage.ObjectResponse{fileObject})
gock.New("http://127.0.0.1").
Post("/storage/v1/object/private/file").
Reply(http.StatusOK)
// Run test
err := UploadStorageObjectAll(context.Background(), mockApi, "private/file", "/tmp/readme.md", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on service unavailable", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock api
defer gock.OffAll()
gock.New("http://127.0.0.1").
Get("/storage/v1/bucket").
Reply(http.StatusServiceUnavailable)
// Run test
err := UploadStorageObjectAll(context.Background(), mockApi, "missing", ".", 1, fsys)
// Check error
assert.ErrorContains(t, err, "Error status 503:")
assert.Empty(t, apitest.ListUnmatchedRequests())
})
}
func TestDownloadAll(t *testing.T) {
t.Run("downloads buckets to existing directory", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// 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",
}})
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").
Post("/storage/v1/object/list/test").
Reply(http.StatusOK).
JSON([]storage.ObjectResponse{})
// Run test
err := DownloadStorageObjectAll(context.Background(), mockApi, "", "/", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
exists, err := afero.DirExists(fsys, "/private")
assert.NoError(t, err)
assert.True(t, exists)
exists, err = afero.DirExists(fsys, "/test")
assert.NoError(t, err)
assert.True(t, exists)
})
t.Run("downloads empty bucket to new directory", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock api
defer gock.OffAll()
gock.New("http://127.0.0.1").
Get("/storage/v1/object/private").
Reply(http.StatusNotFound).
JSON(map[string]string{"error": "Not Found"})
gock.New("http://127.0.0.1").
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("http://127.0.0.1").
Post("/storage/v1/object/list/private").
Reply(http.StatusOK).
JSON([]storage.ObjectResponse{})
// Run test
err := DownloadStorageObjectAll(context.Background(), mockApi, "/private", "/tmp", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
exists, err := afero.DirExists(fsys, "/private")
assert.NoError(t, err)
assert.False(t, exists)
exists, err = afero.DirExists(fsys, "/tmp")
assert.NoError(t, err)
assert.True(t, exists)
})
t.Run("throws error on empty directory", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// 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 := DownloadStorageObjectAll(context.Background(), mockApi, "private/dir/", "/", 1, fsys)
// Check error
assert.ErrorContains(t, err, "Object not found: private/dir/")
assert.Empty(t, apitest.ListUnmatchedRequests())
exists, err := afero.DirExists(fsys, "/private")
assert.NoError(t, err)
assert.False(t, exists)
})
t.Run("downloads objects to existing directory", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock api
defer gock.OffAll()
// Lists /private/tmp directory
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",
}, mockFile})
gock.New("http://127.0.0.1").
Get("/storage/v1/object/private/tmp/abstract.pdf").
Reply(http.StatusOK)
// Lists /private/tmp/docs directory
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{readme})
gock.New("http://127.0.0.1").
Get("/storage/v1/object/private/tmp/docs/readme.md").
Reply(http.StatusOK)
// Run test
err := DownloadStorageObjectAll(context.Background(), mockApi, "private/tmp/", "/", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
exists, err := afero.Exists(fsys, "/tmp/abstract.pdf")
assert.NoError(t, err)
assert.True(t, exists)
exists, err = afero.Exists(fsys, "/tmp/docs/readme.md")
assert.NoError(t, err)
assert.True(t, exists)
})
t.Run("downloads object to existing file", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// 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").
Get("/storage/v1/object/private/abstract.pdf").
Reply(http.StatusOK)
// Run test
err := DownloadStorageObjectAll(context.Background(), mockApi, "/private/abstract.pdf", "/tmp/file", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
exists, err := afero.DirExists(fsys, "/private")
assert.NoError(t, err)
assert.False(t, exists)
exists, err = afero.Exists(fsys, "/tmp/file")
assert.NoError(t, err)
assert.True(t, exists)
})
}