supabase-cli/internal/storage/rm/rm_test.go

331 lines
10 KiB
Go

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())
})
}