github.com/cornelk/go-cloud@v0.17.1/blob/gcsblob/gcsblob_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 gcsblob
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"flag"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"net/http"
    24  	"net/url"
    25  	"os"
    26  	"os/user"
    27  	"path/filepath"
    28  	"testing"
    29  
    30  	"cloud.google.com/go/storage"
    31  	"github.com/cornelk/go-cloud/blob"
    32  	"github.com/cornelk/go-cloud/blob/driver"
    33  	"github.com/cornelk/go-cloud/blob/drivertest"
    34  	"github.com/cornelk/go-cloud/gcerrors"
    35  	"github.com/cornelk/go-cloud/gcp"
    36  	"github.com/cornelk/go-cloud/internal/testing/setup"
    37  	"github.com/google/go-cmp/cmp"
    38  	"google.golang.org/api/googleapi"
    39  )
    40  
    41  const (
    42  	// These constants capture values that were used during the last -record.
    43  	//
    44  	// If you want to use --record mode,
    45  	// 1. Create a bucket in your GCP project:
    46  	//    https://console.cloud.google.com/storage/browser, then "Create Bucket".
    47  	// 2. Update the bucketName constant to your bucket name.
    48  	// 3. Create a service account in your GCP project and update the
    49  	//    serviceAccountID constant to it.
    50  	// 4. Download a private key to a .pem file as described here:
    51  	//    https://godoc.org/cloud.google.com/go/storage#SignedURLOptions
    52  	//    and pass a path to it via the --privatekey flag.
    53  	// TODO(issue #300): Use Terraform to provision a bucket, and get the bucket
    54  	//    name from the Terraform output instead (saving a copy of it for replay).
    55  	bucketName       = "go-cloud-blob-test-bucket"
    56  	serviceAccountID = "storage-updater@go-cloud-test-216917.iam.gserviceaccount.com"
    57  )
    58  
    59  var pathToPrivateKey = flag.String("privatekey", "", "path to .pem file containing private key (required for --record); defaults to ~/Downloads/gcs-private-key.pem")
    60  
    61  type harness struct {
    62  	client *gcp.HTTPClient
    63  	opts   *Options
    64  	rt     http.RoundTripper
    65  	closer func()
    66  }
    67  
    68  func newHarness(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
    69  	opts := &Options{GoogleAccessID: serviceAccountID}
    70  	if *setup.Record {
    71  		if *pathToPrivateKey == "" {
    72  			usr, _ := user.Current()
    73  			*pathToPrivateKey = filepath.Join(usr.HomeDir, "Downloads", "gcs-private-key.pem")
    74  		}
    75  		// Use a real private key for signing URLs during -record.
    76  		pk, err := ioutil.ReadFile(*pathToPrivateKey)
    77  		if err != nil {
    78  			t.Fatalf("Couldn't find private key at %v: %v", *pathToPrivateKey, err)
    79  		}
    80  		opts.PrivateKey = pk
    81  	} else {
    82  		// Use a dummy signer in replay mode.
    83  		opts.SignBytes = func(b []byte) ([]byte, error) { return []byte("signed!"), nil }
    84  	}
    85  	client, rt, done := setup.NewGCPClient(ctx, t)
    86  	return &harness{client: client, opts: opts, rt: rt, closer: done}, nil
    87  }
    88  
    89  func (h *harness) HTTPClient() *http.Client {
    90  	return &http.Client{Transport: h.rt}
    91  }
    92  
    93  func (h *harness) MakeDriver(ctx context.Context) (driver.Bucket, error) {
    94  	return openBucket(ctx, h.client, bucketName, h.opts)
    95  }
    96  
    97  func (h *harness) Close() {
    98  	h.closer()
    99  }
   100  
   101  func TestConformance(t *testing.T) {
   102  	drivertest.RunConformanceTests(t, newHarness, []drivertest.AsTest{verifyContentLanguage{}})
   103  }
   104  
   105  func BenchmarkGcsblob(b *testing.B) {
   106  	ctx := context.Background()
   107  	creds, err := gcp.DefaultCredentials(ctx)
   108  	if err != nil {
   109  		b.Fatal(err)
   110  	}
   111  	client, err := gcp.NewHTTPClient(gcp.DefaultTransport(), gcp.CredentialsTokenSource(creds))
   112  	if err != nil {
   113  		b.Fatal(err)
   114  	}
   115  	bkt, err := OpenBucket(context.Background(), client, bucketName, nil)
   116  	if err != nil {
   117  		b.Fatal(err)
   118  	}
   119  	drivertest.RunBenchmarks(b, bkt)
   120  }
   121  
   122  const language = "nl"
   123  
   124  // verifyContentLanguage uses As to access the underlying GCS types and
   125  // read/write the ContentLanguage field.
   126  type verifyContentLanguage struct{}
   127  
   128  func (verifyContentLanguage) Name() string {
   129  	return "verify ContentLanguage can be written and read through As"
   130  }
   131  
   132  func (verifyContentLanguage) BucketCheck(b *blob.Bucket) error {
   133  	var client *storage.Client
   134  	if !b.As(&client) {
   135  		return errors.New("Bucket.As failed")
   136  	}
   137  	return nil
   138  }
   139  
   140  func (verifyContentLanguage) ErrorCheck(b *blob.Bucket, err error) error {
   141  	// Can't really verify this one because the storage library returns
   142  	// a sentinel error, storage.ErrObjectNotExist, for "not exists"
   143  	// instead of the supported As type googleapi.Error.
   144  	// Call ErrorAs anyway, and expect it to fail.
   145  	var to *googleapi.Error
   146  	if b.ErrorAs(err, &to) {
   147  		return errors.New("expected ErrorAs to fail")
   148  	}
   149  	return nil
   150  }
   151  
   152  func (verifyContentLanguage) BeforeRead(as func(interface{}) bool) error {
   153  	var objp **storage.ObjectHandle
   154  	if !as(&objp) {
   155  		return errors.New("BeforeRead.As failed to get ObjectHandle")
   156  	}
   157  	var sr *storage.Reader
   158  	if !as(&sr) {
   159  		return errors.New("BeforeRead.As failed to get Reader")
   160  	}
   161  	return nil
   162  }
   163  
   164  func (verifyContentLanguage) BeforeWrite(as func(interface{}) bool) error {
   165  	var objp **storage.ObjectHandle
   166  	if !as(&objp) {
   167  		return errors.New("Writer.As failed to get ObjectHandle")
   168  	}
   169  	var sw *storage.Writer
   170  	if !as(&sw) {
   171  		return errors.New("Writer.As failed to get Writer")
   172  	}
   173  	sw.ContentLanguage = language
   174  	return nil
   175  }
   176  
   177  func (verifyContentLanguage) BeforeCopy(as func(interface{}) bool) error {
   178  	var coh *CopyObjectHandles
   179  	if !as(&coh) {
   180  		return errors.New("BeforeCopy.As failed to get CopyObjectHandles")
   181  	}
   182  	var copier *storage.Copier
   183  	if !as(&copier) {
   184  		return errors.New("BeforeCopy.As failed")
   185  	}
   186  	return nil
   187  }
   188  
   189  func (verifyContentLanguage) BeforeList(as func(interface{}) bool) error {
   190  	var q *storage.Query
   191  	if !as(&q) {
   192  		return errors.New("List.As failed")
   193  	}
   194  	// Nothing to do.
   195  	return nil
   196  }
   197  
   198  func (verifyContentLanguage) AttributesCheck(attrs *blob.Attributes) error {
   199  	var oa storage.ObjectAttrs
   200  	if !attrs.As(&oa) {
   201  		return errors.New("Attributes.As returned false")
   202  	}
   203  	if got := oa.ContentLanguage; got != language {
   204  		return fmt.Errorf("got %q want %q", got, language)
   205  	}
   206  	return nil
   207  }
   208  
   209  func (verifyContentLanguage) ReaderCheck(r *blob.Reader) error {
   210  	var rr *storage.Reader
   211  	if !r.As(&rr) {
   212  		return errors.New("Reader.As returned false")
   213  	}
   214  	// GCS doesn't return Content-Language via storage.Reader.
   215  	return nil
   216  }
   217  
   218  func (verifyContentLanguage) ListObjectCheck(o *blob.ListObject) error {
   219  	var oa storage.ObjectAttrs
   220  	if !o.As(&oa) {
   221  		return errors.New("ListObject.As returned false")
   222  	}
   223  	if o.IsDir {
   224  		return nil
   225  	}
   226  	if got := oa.ContentLanguage; got != language {
   227  		return fmt.Errorf("got %q want %q", got, language)
   228  	}
   229  	return nil
   230  }
   231  
   232  // GCS-specific unit tests.
   233  func TestBufferSize(t *testing.T) {
   234  	tests := []struct {
   235  		size int
   236  		want int
   237  	}{
   238  		{
   239  			size: 5 * 1024 * 1024,
   240  			want: 5 * 1024 * 1024,
   241  		},
   242  		{
   243  			size: 0,
   244  			want: googleapi.DefaultUploadChunkSize,
   245  		},
   246  		{
   247  			size: -1024,
   248  			want: 0,
   249  		},
   250  	}
   251  	for i, test := range tests {
   252  		got := bufferSize(test.size)
   253  		if got != test.want {
   254  			t.Errorf("%d) got buffer size %d, want %d", i, got, test.want)
   255  		}
   256  	}
   257  }
   258  
   259  func TestOpenBucket(t *testing.T) {
   260  	tests := []struct {
   261  		description string
   262  		bucketName  string
   263  		nilClient   bool
   264  		want        string
   265  		wantErr     bool
   266  	}{
   267  		{
   268  			description: "empty bucket name results in error",
   269  			wantErr:     true,
   270  		},
   271  		{
   272  			description: "nil client results in error",
   273  			bucketName:  "foo",
   274  			nilClient:   true,
   275  			wantErr:     true,
   276  		},
   277  		{
   278  			description: "success",
   279  			bucketName:  "foo",
   280  			want:        "foo",
   281  		},
   282  	}
   283  
   284  	ctx := context.Background()
   285  	for _, test := range tests {
   286  		t.Run(test.description, func(t *testing.T) {
   287  			var client *gcp.HTTPClient
   288  			if !test.nilClient {
   289  				var done func()
   290  				client, _, done = setup.NewGCPClient(ctx, t)
   291  				defer done()
   292  			}
   293  
   294  			// Create driver impl.
   295  			drv, err := openBucket(ctx, client, test.bucketName, nil)
   296  			if (err != nil) != test.wantErr {
   297  				t.Errorf("got err %v want error %v", err, test.wantErr)
   298  			}
   299  			if err == nil && drv != nil && drv.name != test.want {
   300  				t.Errorf("got %q want %q", drv.name, test.want)
   301  			}
   302  
   303  			// Create portable type.
   304  			b, err := OpenBucket(ctx, client, test.bucketName, nil)
   305  			if b != nil {
   306  				defer b.Close()
   307  			}
   308  			if (err != nil) != test.wantErr {
   309  				t.Errorf("got err %v want error %v", err, test.wantErr)
   310  			}
   311  		})
   312  	}
   313  }
   314  
   315  // TestPreconditions tests setting of ObjectHandle preconditions via As.
   316  func TestPreconditions(t *testing.T) {
   317  	const (
   318  		key     = "precondition-key"
   319  		key2    = "precondition-key2"
   320  		content = "hello world"
   321  	)
   322  
   323  	ctx := context.Background()
   324  	h, err := newHarness(ctx, t)
   325  	if err != nil {
   326  		t.Fatal(err)
   327  	}
   328  	defer h.Close()
   329  
   330  	drv, err := h.MakeDriver(ctx)
   331  	if err != nil {
   332  		t.Fatal(err)
   333  	}
   334  	bucket := blob.NewBucket(drv)
   335  	defer bucket.Close()
   336  
   337  	// Try writing with a failing precondition.
   338  	if err := bucket.WriteAll(ctx, key, []byte(content), &blob.WriterOptions{
   339  		BeforeWrite: func(asFunc func(interface{}) bool) error {
   340  			var objp **storage.ObjectHandle
   341  			if !asFunc(&objp) {
   342  				return errors.New("Writer.As failed to get ObjectHandle")
   343  			}
   344  			// Replace the ObjectHandle with a new one that adds Conditions.
   345  			*objp = (*objp).If(storage.Conditions{GenerationMatch: -999})
   346  			return nil
   347  		},
   348  	}); err == nil || gcerrors.Code(err) != gcerrors.FailedPrecondition {
   349  		t.Errorf("got error %v, wanted FailedPrecondition for Write", err)
   350  	}
   351  
   352  	// Repeat with a precondition that will pass.
   353  	if err := bucket.WriteAll(ctx, key, []byte(content), &blob.WriterOptions{
   354  		BeforeWrite: func(asFunc func(interface{}) bool) error {
   355  			var objp **storage.ObjectHandle
   356  			if !asFunc(&objp) {
   357  				return errors.New("Writer.As failed to get ObjectHandle")
   358  			}
   359  			// Replace the ObjectHandle with a new one that adds Conditions.
   360  			*objp = (*objp).If(storage.Conditions{DoesNotExist: true})
   361  			return nil
   362  		},
   363  	}); err != nil {
   364  		t.Errorf("got error %v, wanted nil", err)
   365  	}
   366  	defer bucket.Delete(ctx, key)
   367  
   368  	// Try reading with a failing precondition.
   369  	_, err = bucket.NewReader(ctx, key, &blob.ReaderOptions{
   370  		BeforeRead: func(asFunc func(interface{}) bool) error {
   371  			var objp **storage.ObjectHandle
   372  			if !asFunc(&objp) {
   373  				return errors.New("Reader.As failed to get ObjectHandle")
   374  			}
   375  			// Replace the ObjectHandle with a new one.
   376  			*objp = (*objp).Generation(999999)
   377  			return nil
   378  		},
   379  	})
   380  	if err == nil || gcerrors.Code(err) != gcerrors.NotFound {
   381  		t.Errorf("got error %v, wanted NotFound for Read", err)
   382  	}
   383  
   384  	attrs, err := bucket.Attributes(ctx, key)
   385  	if err != nil {
   386  		t.Fatal(err)
   387  	}
   388  	var oa storage.ObjectAttrs
   389  	if !attrs.As(&oa) {
   390  		t.Fatal("Attributes.As failed")
   391  	}
   392  	generation := oa.Generation
   393  
   394  	// Repeat with a precondition that will pass.
   395  	reader, err := bucket.NewReader(ctx, key, &blob.ReaderOptions{
   396  		BeforeRead: func(asFunc func(interface{}) bool) error {
   397  			var objp **storage.ObjectHandle
   398  			if !asFunc(&objp) {
   399  				return errors.New("Reader.As failed to get ObjectHandle")
   400  			}
   401  			// Replace the ObjectHandle with a new one.
   402  			*objp = (*objp).Generation(generation)
   403  			return nil
   404  		},
   405  	})
   406  	if err != nil {
   407  		t.Fatal(err)
   408  	}
   409  	defer reader.Close()
   410  	gotBytes, err := ioutil.ReadAll(reader)
   411  	if err != nil {
   412  		t.Fatal(err)
   413  	}
   414  	if got := string(gotBytes); got != content {
   415  		t.Errorf("got %q want %q", got, content)
   416  	}
   417  
   418  	// Try copying with a failing precondition on Dst.
   419  	err = bucket.Copy(ctx, key2, key, &blob.CopyOptions{
   420  		BeforeCopy: func(asFunc func(interface{}) bool) error {
   421  			var coh *CopyObjectHandles
   422  			if !asFunc(&coh) {
   423  				return errors.New("Copy.As failed to get CopyObjectHandles")
   424  			}
   425  			// Replace the dst ObjectHandle with a new one.
   426  			coh.Dst = coh.Dst.If(storage.Conditions{GenerationMatch: -999})
   427  			return nil
   428  		},
   429  	})
   430  	if err == nil || gcerrors.Code(err) != gcerrors.FailedPrecondition {
   431  		t.Errorf("got error %v, wanted FailedPrecondition for Copy", err)
   432  	}
   433  
   434  	// Try copying with a failing precondition on Src.
   435  	err = bucket.Copy(ctx, key2, key, &blob.CopyOptions{
   436  		BeforeCopy: func(asFunc func(interface{}) bool) error {
   437  			var coh *CopyObjectHandles
   438  			if !asFunc(&coh) {
   439  				return errors.New("Copy.As failed to get CopyObjectHandles")
   440  			}
   441  			// Replace the src ObjectHandle with a new one.
   442  			coh.Src = coh.Src.Generation(9999999)
   443  			return nil
   444  		},
   445  	})
   446  	if err == nil || gcerrors.Code(err) != gcerrors.NotFound {
   447  		t.Errorf("got error %v, wanted NotFound for Copy", err)
   448  	}
   449  
   450  	// Repeat with preconditions on Dst and Src that will succeed.
   451  	err = bucket.Copy(ctx, key2, key, &blob.CopyOptions{
   452  		BeforeCopy: func(asFunc func(interface{}) bool) error {
   453  			var coh *CopyObjectHandles
   454  			if !asFunc(&coh) {
   455  				return errors.New("Reader.As failed to get CopyObjectHandles")
   456  			}
   457  			coh.Dst = coh.Dst.If(storage.Conditions{DoesNotExist: true})
   458  			coh.Src = coh.Src.Generation(generation)
   459  			return nil
   460  		},
   461  	})
   462  	if err != nil {
   463  		t.Error(err)
   464  	}
   465  	defer bucket.Delete(ctx, key2)
   466  }
   467  
   468  func TestURLOpenerForParams(t *testing.T) {
   469  	ctx := context.Background()
   470  
   471  	// Create a file for use as a dummy private key file.
   472  	privateKey := []byte("some content")
   473  	pkFile, err := ioutil.TempFile("", "my-private-key")
   474  	if err != nil {
   475  		t.Fatal(err)
   476  	}
   477  	defer os.Remove(pkFile.Name())
   478  	if _, err := pkFile.Write(privateKey); err != nil {
   479  		t.Fatal(err)
   480  	}
   481  	if err := pkFile.Close(); err != nil {
   482  		t.Fatal(err)
   483  	}
   484  
   485  	tests := []struct {
   486  		name     string
   487  		currOpts Options
   488  		query    url.Values
   489  		wantOpts Options
   490  		wantErr  bool
   491  	}{
   492  		{
   493  			name: "InvalidParam",
   494  			query: url.Values{
   495  				"foo": {"bar"},
   496  			},
   497  			wantErr: true,
   498  		},
   499  		{
   500  			name: "AccessID",
   501  			query: url.Values{
   502  				"access_id": {"bar"},
   503  			},
   504  			wantOpts: Options{GoogleAccessID: "bar"},
   505  		},
   506  		{
   507  			name:     "AccessID override",
   508  			currOpts: Options{GoogleAccessID: "foo"},
   509  			query: url.Values{
   510  				"access_id": {"bar"},
   511  			},
   512  			wantOpts: Options{GoogleAccessID: "bar"},
   513  		},
   514  		{
   515  			name:     "AccessID not overridden",
   516  			currOpts: Options{GoogleAccessID: "bar"},
   517  			wantOpts: Options{GoogleAccessID: "bar"},
   518  		},
   519  		{
   520  			name: "BadPrivateKeyPath",
   521  			query: url.Values{
   522  				"private_key_path": {"/path/does/not/exist"},
   523  			},
   524  			wantErr: true,
   525  		},
   526  		{
   527  			name: "PrivateKeyPath",
   528  			query: url.Values{
   529  				"private_key_path": {pkFile.Name()},
   530  			},
   531  			wantOpts: Options{PrivateKey: privateKey},
   532  		},
   533  	}
   534  
   535  	for _, test := range tests {
   536  		t.Run(test.name, func(t *testing.T) {
   537  			o := &URLOpener{Options: test.currOpts}
   538  			got, err := o.forParams(ctx, test.query)
   539  			if (err != nil) != test.wantErr {
   540  				t.Errorf("got err %v want error %v", err, test.wantErr)
   541  			}
   542  			if err != nil {
   543  				return
   544  			}
   545  			if diff := cmp.Diff(got, &test.wantOpts); diff != "" {
   546  				t.Errorf("opener.forParams(...) diff (-want +got):\n%s", diff)
   547  			}
   548  		})
   549  	}
   550  }
   551  
   552  func TestOpenBucketFromURL(t *testing.T) {
   553  	cleanup := setup.FakeGCPDefaultCredentials(t)
   554  	defer cleanup()
   555  
   556  	pkFile, err := ioutil.TempFile("", "my-private-key")
   557  	if err != nil {
   558  		t.Fatal(err)
   559  	}
   560  	defer os.Remove(pkFile.Name())
   561  	if err := ioutil.WriteFile(pkFile.Name(), []byte("key"), 0666); err != nil {
   562  		t.Fatal(err)
   563  	}
   564  
   565  	tests := []struct {
   566  		URL     string
   567  		WantErr bool
   568  	}{
   569  		// OK.
   570  		{"gs://mybucket", false},
   571  		// OK, setting access_id.
   572  		{"gs://mybucket?access_id=foo", false},
   573  		// OK, setting private_key_path.
   574  		{"gs://mybucket?private_key_path=" + pkFile.Name(), false},
   575  		// Invalid private_key_path.
   576  		{"gs://mybucket?private_key_path=invalid-path", true},
   577  		// Invalid parameter.
   578  		{"gs://mybucket?param=value", true},
   579  	}
   580  
   581  	ctx := context.Background()
   582  	for _, test := range tests {
   583  		b, err := blob.OpenBucket(ctx, test.URL)
   584  		if b != nil {
   585  			defer b.Close()
   586  		}
   587  		if (err != nil) != test.WantErr {
   588  			t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr)
   589  		}
   590  	}
   591  }