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