github.com/thiagoyeds/go-cloud@v0.26.0/blob/blob_test.go (about) 1 // Copyright 2018 The Go Cloud Development Kit Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package blob 16 17 import ( 18 "bytes" 19 "context" 20 "errors" 21 "fmt" 22 "io" 23 "net/url" 24 "strings" 25 "sync" 26 "testing" 27 28 "github.com/google/go-cmp/cmp" 29 "gocloud.dev/blob/driver" 30 "gocloud.dev/gcerrors" 31 "gocloud.dev/internal/gcerr" 32 ) 33 34 var ( 35 errFake = errors.New("fake") 36 errNotFound = errors.New("fake not found") 37 ) 38 39 func TestExists(t *testing.T) { 40 tests := []struct { 41 Description string 42 Err error 43 Want bool 44 WantErr bool 45 }{ 46 { 47 Description: "no error -> exists", 48 Err: nil, 49 Want: true, 50 WantErr: false, 51 }, 52 { 53 Description: "notfound error -> !exists", 54 Err: errNotFound, 55 Want: false, 56 WantErr: false, 57 }, 58 { 59 Description: "other error -> error", 60 Err: errFake, 61 Want: false, 62 WantErr: true, 63 }, 64 } 65 66 for _, test := range tests { 67 t.Run(test.Description, func(t *testing.T) { 68 drv := &fakeAttributes{attributesErr: test.Err} 69 b := NewBucket(drv) 70 defer b.Close() 71 got, gotErr := b.Exists(context.Background(), "key") 72 if got != test.Want { 73 t.Errorf("got %v want %v", got, test.Want) 74 } 75 if (gotErr != nil) != test.WantErr { 76 t.Errorf("got err %v want %v", gotErr, test.WantErr) 77 } 78 }) 79 } 80 } 81 82 // fakeAttributes implements driver.Bucket. Only Attributes is implemented, 83 // returning a zero Attributes struct and attributesErr. 84 type fakeAttributes struct { 85 driver.Bucket 86 attributesErr error 87 } 88 89 func (b *fakeAttributes) Attributes(ctx context.Context, key string) (*driver.Attributes, error) { 90 if b.attributesErr != nil { 91 return nil, b.attributesErr 92 } 93 return &driver.Attributes{}, nil 94 } 95 96 func (b *fakeAttributes) ErrorCode(err error) gcerrors.ErrorCode { 97 if err == errNotFound { 98 return gcerrors.NotFound 99 } 100 return gcerrors.Unknown 101 } 102 103 func (b *fakeAttributes) Close() error { return nil } 104 105 // Verify that ListIterator works even if driver.ListPaged returns empty pages. 106 func TestListIterator(t *testing.T) { 107 ctx := context.Background() 108 want := []string{"a", "b", "c"} 109 db := &fakeLister{ 110 pages: [][]string{{"a"}, {}, {}, {"b", "c"}, {}, {}}, 111 wantPageSizes: []int{0, 0, 0, 0, 0, 0}, 112 } 113 b := NewBucket(db) 114 defer b.Close() 115 iter := b.List(nil) 116 var got []string 117 for { 118 obj, err := iter.Next(ctx) 119 if err == io.EOF { 120 break 121 } 122 if err != nil { 123 t.Fatal(err) 124 } 125 got = append(got, obj.Key) 126 } 127 if !cmp.Equal(got, want) { 128 t.Errorf("got %v, want %v", got, want) 129 } 130 } 131 132 // Verify that ListPage works even if driver.ListPaged returns empty pages. 133 func TestListPage(t *testing.T) { 134 ctx := context.Background() 135 want := [][]string{{"a", "b"}, {"c", "d"}, {"e"}} 136 db := &fakeLister{ 137 pages: [][]string{{}, {"a", "b"}, {}, {}, {"c"}, {}, {"d"}, {}, {}, {"e"}}, 138 wantPageSizes: []int{2, 2, 2, 2, 2, 1, 1, 2, 2, 2}, 139 } 140 b := NewBucket(db) 141 defer b.Close() 142 143 nextToken := FirstPageToken 144 got := [][]string{} 145 for { 146 page, token, err := b.ListPage(ctx, nextToken, 2, nil) 147 if err == io.EOF { 148 break 149 } 150 if err != nil { 151 t.Fatal(err) 152 } 153 gotPage := make([]string, len(page)) 154 for i, o := range page { 155 gotPage[i] = o.Key 156 } 157 got = append(got, gotPage) 158 nextToken = token 159 } 160 if !cmp.Equal(got, want) { 161 t.Errorf("got %v, want %v", got, want) 162 } 163 } 164 165 // fakeLister implements driver.Bucket. Only ListPaged is implemented, 166 // returning static data from pages. 167 type fakeLister struct { 168 driver.Bucket 169 pages [][]string 170 wantPageSizes []int 171 } 172 173 func (b *fakeLister) ListPaged(ctx context.Context, opts *driver.ListOptions) (*driver.ListPage, error) { 174 if len(b.pages) != len(b.wantPageSizes) { 175 return nil, fmt.Errorf("invalid fakeLister setup") 176 } 177 if len(b.pages) == 0 { 178 return &driver.ListPage{}, nil 179 } 180 page := b.pages[0] 181 wantPageSize := b.wantPageSizes[0] 182 b.pages = b.pages[1:] 183 b.wantPageSizes = b.wantPageSizes[1:] 184 if opts.PageSize != wantPageSize { 185 return nil, fmt.Errorf("got page size %d, want %d", opts.PageSize, wantPageSize) 186 } 187 var objs []*driver.ListObject 188 for _, key := range page { 189 objs = append(objs, &driver.ListObject{Key: key}) 190 } 191 return &driver.ListPage{Objects: objs, NextPageToken: []byte{1}}, nil 192 } 193 194 func (*fakeLister) Close() error { return nil } 195 func (*fakeLister) ErrorCode(err error) gcerrors.ErrorCode { return gcerrors.Unknown } 196 197 // erroringBucket implements driver.Bucket. All interface methods that return 198 // errors are implemented, and return errFake. 199 // In addition, when passed the key "work", NewRangeReader and NewTypedWriter 200 // will return a Reader/Writer respectively, that always return errFake 201 // from Read/Write and Close. 202 type erroringBucket struct { 203 driver.Bucket 204 } 205 206 type erroringReader struct { 207 driver.Reader 208 } 209 210 func (r *erroringReader) Read(p []byte) (int, error) { 211 return 0, errFake 212 } 213 214 func (r *erroringReader) Close() error { 215 return errFake 216 } 217 218 type erroringWriter struct { 219 driver.Writer 220 } 221 222 func (r *erroringWriter) Write(p []byte) (int, error) { 223 return 0, errFake 224 } 225 226 func (r *erroringWriter) Close() error { 227 return errFake 228 } 229 230 func (b *erroringBucket) Attributes(ctx context.Context, key string) (*driver.Attributes, error) { 231 return nil, errFake 232 } 233 234 func (b *erroringBucket) ListPaged(ctx context.Context, opts *driver.ListOptions) (*driver.ListPage, error) { 235 return nil, errFake 236 } 237 238 func (b *erroringBucket) NewRangeReader(ctx context.Context, key string, offset, length int64, opts *driver.ReaderOptions) (driver.Reader, error) { 239 if key == "work" { 240 return &erroringReader{}, nil 241 } 242 return nil, errFake 243 } 244 245 func (b *erroringBucket) NewTypedWriter(ctx context.Context, key string, contentType string, opts *driver.WriterOptions) (driver.Writer, error) { 246 if key == "work" { 247 return &erroringWriter{}, nil 248 } 249 return nil, errFake 250 } 251 252 func (b *erroringBucket) Copy(ctx context.Context, dstKey, srcKey string, opts *driver.CopyOptions) error { 253 return errFake 254 } 255 256 func (b *erroringBucket) Delete(ctx context.Context, key string) error { 257 return errFake 258 } 259 260 func (b *erroringBucket) SignedURL(ctx context.Context, key string, opts *driver.SignedURLOptions) (string, error) { 261 return "", errFake 262 } 263 264 func (b *erroringBucket) Close() error { 265 return errFake 266 } 267 268 func (b *erroringBucket) ErrorCode(err error) gcerrors.ErrorCode { 269 return gcerrors.Unknown 270 } 271 272 // TestErrorsAreWrapped tests that all errors returned from the driver are 273 // wrapped exactly once by the portable type. 274 func TestErrorsAreWrapped(t *testing.T) { 275 ctx := context.Background() 276 buf := bytes.Repeat([]byte{'A'}, sniffLen) 277 b := NewBucket(&erroringBucket{}) 278 279 // verifyWrap ensures that err is wrapped exactly once. 280 verifyWrap := func(description string, err error) { 281 if err == nil { 282 t.Errorf("%s: got nil error, wanted non-nil", description) 283 return 284 } 285 if _, ok := err.(*gcerr.Error); !ok { 286 t.Errorf("%s: not wrapped: %v", description, err) 287 } 288 if s := err.Error(); !strings.HasPrefix(s, "blob ") { 289 t.Logf("short form of error: %v", err) 290 t.Logf("with details: %+v", err) 291 t.Errorf("%s: Error() for wrapped error doesn't start with blob: prefix: %s", description, s) 292 } 293 } 294 295 _, err := b.Attributes(ctx, "") 296 verifyWrap("Attributes", err) 297 298 iter := b.List(nil) 299 _, err = iter.Next(ctx) 300 verifyWrap("ListIterator.Next", err) 301 302 _, err = b.NewRangeReader(ctx, "", 0, 1, nil) 303 verifyWrap("NewRangeReader", err) 304 _, err = b.ReadAll(ctx, "") 305 verifyWrap("ReadAll", err) 306 307 // Providing ContentType means driver.NewTypedWriter is called right away. 308 _, err = b.NewWriter(ctx, "", &WriterOptions{ContentType: "foo"}) 309 verifyWrap("NewWriter", err) 310 err = b.WriteAll(ctx, "", buf, &WriterOptions{ContentType: "foo"}) 311 verifyWrap("WriteAll", err) 312 313 // Not providing ContentType means driver.NewTypedWriter is only called 314 // after writing sniffLen bytes. 315 w, _ := b.NewWriter(ctx, "", nil) 316 _, err = w.Write(buf) 317 verifyWrap("NewWriter (no ContentType)", err) 318 w.Close() 319 err = b.WriteAll(ctx, "", buf, nil) 320 verifyWrap("WriteAll (no ContentType)", err) 321 322 r, _ := b.NewRangeReader(ctx, "work", 0, 1, nil) 323 _, err = r.Read(buf) 324 verifyWrap("Reader.Read", err) 325 326 err = r.Close() 327 verifyWrap("Reader.Close", err) 328 329 w, _ = b.NewWriter(ctx, "work", &WriterOptions{ContentType: "foo"}) 330 _, err = w.Write(buf) 331 verifyWrap("Writer.Write", err) 332 333 err = w.Close() 334 verifyWrap("Writer.Close", err) 335 336 err = b.Copy(ctx, "", "", nil) 337 verifyWrap("Copy", err) 338 339 err = b.Delete(ctx, "") 340 verifyWrap("Delete", err) 341 342 _, err = b.SignedURL(ctx, "", nil) 343 verifyWrap("SignedURL", err) 344 345 err = b.Close() 346 verifyWrap("Close", err) 347 } 348 349 var ( 350 testOpenOnce sync.Once 351 testOpenGot *url.URL 352 ) 353 354 // TestBucketIsClosed verifies that all Bucket functions return an error 355 // if the Bucket is closed. 356 func TestBucketIsClosed(t *testing.T) { 357 ctx := context.Background() 358 buf := bytes.Repeat([]byte{'A'}, sniffLen) 359 360 bucket := NewBucket(&erroringBucket{}) 361 bucket.Close() 362 363 if _, err := bucket.Attributes(ctx, ""); err != errClosed { 364 t.Error(err) 365 } 366 iter := bucket.List(nil) 367 if _, err := iter.Next(ctx); err != errClosed { 368 t.Error(err) 369 } 370 371 if _, err := bucket.NewRangeReader(ctx, "", 0, 1, nil); err != errClosed { 372 t.Error(err) 373 } 374 if _, err := bucket.ReadAll(ctx, ""); err != errClosed { 375 t.Error(err) 376 } 377 if _, err := bucket.NewWriter(ctx, "", nil); err != errClosed { 378 t.Error(err) 379 } 380 if err := bucket.WriteAll(ctx, "", buf, nil); err != errClosed { 381 t.Error(err) 382 } 383 if _, err := bucket.NewRangeReader(ctx, "work", 0, 1, nil); err != errClosed { 384 t.Error(err) 385 } 386 if err := bucket.Copy(ctx, "", "", nil); err != errClosed { 387 t.Error(err) 388 } 389 if err := bucket.Delete(ctx, ""); err != errClosed { 390 t.Error(err) 391 } 392 if _, err := bucket.SignedURL(ctx, "", nil); err != errClosed { 393 t.Error(err) 394 } 395 if err := bucket.Close(); err != errClosed { 396 t.Error(err) 397 } 398 } 399 400 func TestURLMux(t *testing.T) { 401 ctx := context.Background() 402 403 mux := new(URLMux) 404 fake := &fakeOpener{} 405 mux.RegisterBucket("foo", fake) 406 mux.RegisterBucket("err", fake) 407 408 if diff := cmp.Diff(mux.BucketSchemes(), []string{"err", "foo"}); diff != "" { 409 t.Errorf("Schemes: %s", diff) 410 } 411 if !mux.ValidBucketScheme("foo") || !mux.ValidBucketScheme("err") { 412 t.Errorf("ValidBucketScheme didn't return true for valid scheme") 413 } 414 if mux.ValidBucketScheme("foo2") || mux.ValidBucketScheme("http") { 415 t.Errorf("ValidBucketScheme didn't return false for invalid scheme") 416 } 417 418 for _, tc := range []struct { 419 name string 420 url string 421 wantErr bool 422 }{ 423 { 424 name: "empty URL", 425 wantErr: true, 426 }, 427 { 428 name: "invalid URL", 429 url: ":foo", 430 wantErr: true, 431 }, 432 { 433 name: "invalid URL no scheme", 434 url: "foo", 435 wantErr: true, 436 }, 437 { 438 name: "unregistered scheme", 439 url: "bar://mybucket", 440 wantErr: true, 441 }, 442 { 443 name: "func returns error", 444 url: "err://mybucket", 445 wantErr: true, 446 }, 447 { 448 name: "no query options", 449 url: "foo://mybucket", 450 }, 451 { 452 name: "empty query options", 453 url: "foo://mybucket?", 454 }, 455 { 456 name: "query options", 457 url: "foo://mybucket?aAa=bBb&cCc=dDd", 458 }, 459 { 460 name: "multiple query options", 461 url: "foo://mybucket?x=a&x=b&x=c", 462 }, 463 { 464 name: "fancy bucket name", 465 url: "foo:///foo/bar/baz", 466 }, 467 { 468 name: "using api scheme prefix", 469 url: "blob+foo:///foo/bar/baz", 470 }, 471 { 472 name: "using api+type scheme prefix", 473 url: "blob+bucket+foo:///foo/bar/baz", 474 }, 475 } { 476 t.Run(tc.name, func(t *testing.T) { 477 _, gotErr := mux.OpenBucket(ctx, tc.url) 478 if (gotErr != nil) != tc.wantErr { 479 t.Fatalf("got err %v, want error %v", gotErr, tc.wantErr) 480 } 481 if gotErr != nil { 482 return 483 } 484 if got := fake.u.String(); got != tc.url { 485 t.Errorf("got %q want %q", got, tc.url) 486 } 487 // Repeat with OpenBucketURL. 488 parsed, err := url.Parse(tc.url) 489 if err != nil { 490 t.Fatal(err) 491 } 492 _, gotErr = mux.OpenBucketURL(ctx, parsed) 493 if gotErr != nil { 494 t.Fatalf("got err %v want nil", gotErr) 495 } 496 if got := fake.u.String(); got != tc.url { 497 t.Errorf("got %q want %q", got, tc.url) 498 } 499 }) 500 } 501 } 502 503 type fakeOpener struct { 504 u *url.URL // last url passed to OpenBucketURL 505 } 506 507 func (o *fakeOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*Bucket, error) { 508 if u.Scheme == "err" { 509 return nil, errors.New("fail") 510 } 511 o.u = u 512 return nil, nil 513 }