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  }