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