github.com/thiagoyeds/go-cloud@v0.26.0/blob/fileblob/fileblob_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 fileblob
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"net/url"
    26  	"os"
    27  	"path/filepath"
    28  	"runtime"
    29  	"strings"
    30  	"testing"
    31  
    32  	"gocloud.dev/blob"
    33  	"gocloud.dev/blob/driver"
    34  	"gocloud.dev/blob/drivertest"
    35  	"gocloud.dev/gcerrors"
    36  )
    37  
    38  type harness struct {
    39  	dir         string
    40  	prefix      string
    41  	metadataHow metadataOption
    42  	server      *httptest.Server
    43  	urlSigner   URLSigner
    44  	closer      func()
    45  }
    46  
    47  func newHarness(ctx context.Context, t *testing.T, prefix string, metadataHow metadataOption) (drivertest.Harness, error) {
    48  	if metadataHow == MetadataDontWrite {
    49  		// Skip tests for if no metadata gets written.
    50  		// For these it is currently undefined whether any gets read (back).
    51  		switch name := t.Name(); {
    52  		case strings.HasSuffix(name, "TestAttributes"), strings.Contains(name, "TestMetadata/"):
    53  			t.SkipNow()
    54  			return nil, nil
    55  		}
    56  	}
    57  
    58  	dir := filepath.Join(os.TempDir(), "go-cloud-fileblob")
    59  	if err := os.MkdirAll(dir, os.ModePerm); err != nil {
    60  		return nil, err
    61  	}
    62  	if prefix != "" {
    63  		if err := os.MkdirAll(filepath.Join(dir, prefix), os.ModePerm); err != nil {
    64  			return nil, err
    65  		}
    66  	}
    67  	h := &harness{dir: dir, prefix: prefix, metadataHow: metadataHow}
    68  
    69  	localServer := httptest.NewServer(http.HandlerFunc(h.serveSignedURL))
    70  	h.server = localServer
    71  
    72  	u, err := url.Parse(h.server.URL)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	h.urlSigner = NewURLSignerHMAC(u, []byte("I'm a secret key"))
    77  
    78  	h.closer = func() { _ = os.RemoveAll(dir); localServer.Close() }
    79  
    80  	return h, nil
    81  }
    82  
    83  func (h *harness) serveSignedURL(w http.ResponseWriter, r *http.Request) {
    84  	objKey, err := h.urlSigner.KeyFromURL(r.Context(), r.URL)
    85  	if err != nil {
    86  		w.WriteHeader(http.StatusForbidden)
    87  		return
    88  	}
    89  
    90  	allowedMethod := r.URL.Query().Get("method")
    91  	if allowedMethod == "" {
    92  		allowedMethod = http.MethodGet
    93  	}
    94  	if allowedMethod != r.Method {
    95  		w.WriteHeader(http.StatusForbidden)
    96  		return
    97  	}
    98  	contentType := r.URL.Query().Get("contentType")
    99  	if r.Header.Get("Content-Type") != contentType {
   100  		w.WriteHeader(http.StatusForbidden)
   101  		return
   102  	}
   103  
   104  	bucket, err := OpenBucket(h.dir, &Options{})
   105  	if err != nil {
   106  		w.WriteHeader(http.StatusInternalServerError)
   107  		return
   108  	}
   109  	defer bucket.Close()
   110  
   111  	switch r.Method {
   112  	case http.MethodGet:
   113  		reader, err := bucket.NewReader(r.Context(), objKey, nil)
   114  		if err != nil {
   115  			w.WriteHeader(http.StatusNotFound)
   116  			return
   117  		}
   118  		defer reader.Close()
   119  		io.Copy(w, reader)
   120  	case http.MethodPut:
   121  		writer, err := bucket.NewWriter(r.Context(), objKey, &blob.WriterOptions{
   122  			ContentType: contentType,
   123  		})
   124  		if err != nil {
   125  			w.WriteHeader(http.StatusInternalServerError)
   126  			return
   127  		}
   128  		io.Copy(writer, r.Body)
   129  		if err := writer.Close(); err != nil {
   130  			w.WriteHeader(http.StatusInternalServerError)
   131  			return
   132  		}
   133  	case http.MethodDelete:
   134  		if err := bucket.Delete(r.Context(), objKey); err != nil {
   135  			w.WriteHeader(http.StatusNotFound)
   136  			return
   137  		}
   138  	default:
   139  		w.WriteHeader(http.StatusForbidden)
   140  	}
   141  }
   142  
   143  func (h *harness) HTTPClient() *http.Client {
   144  	return &http.Client{}
   145  }
   146  
   147  func (h *harness) MakeDriver(ctx context.Context) (driver.Bucket, error) {
   148  	opts := &Options{
   149  		URLSigner: h.urlSigner,
   150  		Metadata:  h.metadataHow,
   151  	}
   152  	drv, err := openBucket(h.dir, opts)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	if h.prefix == "" {
   157  		return drv, nil
   158  	}
   159  	return driver.NewPrefixedBucket(drv, h.prefix), nil
   160  }
   161  
   162  func (h *harness) MakeDriverForNonexistentBucket(ctx context.Context) (driver.Bucket, error) {
   163  	// Does not make sense for this driver, as it verifies
   164  	// that the directory exists in OpenBucket.
   165  	return nil, nil
   166  }
   167  
   168  func (h *harness) Close() {
   169  	h.closer()
   170  }
   171  
   172  func TestConformance(t *testing.T) {
   173  	newHarnessNoPrefix := func(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
   174  		return newHarness(ctx, t, "", MetadataInSidecar)
   175  	}
   176  	drivertest.RunConformanceTests(t, newHarnessNoPrefix, []drivertest.AsTest{verifyAs{}})
   177  }
   178  
   179  func TestConformanceWithPrefix(t *testing.T) {
   180  	const prefix = "some/prefix/dir/"
   181  	newHarnessWithPrefix := func(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
   182  		return newHarness(ctx, t, prefix, MetadataInSidecar)
   183  	}
   184  	drivertest.RunConformanceTests(t, newHarnessWithPrefix, []drivertest.AsTest{verifyAs{prefix: prefix}})
   185  }
   186  
   187  func TestConformanceSkipMetadata(t *testing.T) {
   188  	newHarnessSkipMetadata := func(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
   189  		return newHarness(ctx, t, "", MetadataDontWrite)
   190  	}
   191  	drivertest.RunConformanceTests(t, newHarnessSkipMetadata, []drivertest.AsTest{verifyAs{}})
   192  }
   193  
   194  func BenchmarkFileblob(b *testing.B) {
   195  	dir := filepath.Join(os.TempDir(), "go-cloud-fileblob")
   196  	if err := os.MkdirAll(dir, os.ModePerm); err != nil {
   197  		b.Fatal(err)
   198  	}
   199  	bkt, err := OpenBucket(dir, nil)
   200  	if err != nil {
   201  		b.Fatal(err)
   202  	}
   203  	drivertest.RunBenchmarks(b, bkt)
   204  }
   205  
   206  // File-specific unit tests.
   207  func TestNewBucket(t *testing.T) {
   208  	t.Run("BucketDirMissing", func(t *testing.T) {
   209  		dir, err := ioutil.TempDir("", "fileblob")
   210  		if err != nil {
   211  			t.Fatal(err)
   212  		}
   213  		defer os.RemoveAll(dir)
   214  		_, gotErr := OpenBucket(filepath.Join(dir, "notfound"), nil)
   215  		if gotErr == nil {
   216  			t.Errorf("got nil want error")
   217  		}
   218  	})
   219  	t.Run("BucketDirMissingWithCreateDir", func(t *testing.T) {
   220  		dir, err := ioutil.TempDir("", "fileblob")
   221  		if err != nil {
   222  			t.Fatal(err)
   223  		}
   224  		defer os.RemoveAll(dir)
   225  		b, gotErr := OpenBucket(filepath.Join(dir, "notfound"), &Options{CreateDir: true})
   226  		if gotErr != nil {
   227  			t.Errorf("got error %v", gotErr)
   228  		}
   229  		defer b.Close()
   230  
   231  		// Make sure the subdir has gotten permissions to be used.
   232  		gotErr = b.WriteAll(context.Background(), "key", []byte("delme"), nil)
   233  		if gotErr != nil {
   234  			t.Errorf("got error writing to bucket from CreateDir %v", gotErr)
   235  		}
   236  	})
   237  	t.Run("BucketIsFile", func(t *testing.T) {
   238  		f, err := ioutil.TempFile("", "fileblob")
   239  		if err != nil {
   240  			t.Fatal(err)
   241  		}
   242  		defer os.Remove(f.Name())
   243  		_, gotErr := OpenBucket(f.Name(), nil)
   244  		if gotErr == nil {
   245  			t.Errorf("got nil want error")
   246  		}
   247  	})
   248  }
   249  
   250  func TestSignedURLReturnsUnimplementedWithNoURLSigner(t *testing.T) {
   251  	dir, err := ioutil.TempDir("", "fileblob")
   252  	if err != nil {
   253  		t.Fatal(err)
   254  	}
   255  	defer os.RemoveAll(dir)
   256  	b, err := OpenBucket(dir, nil)
   257  	if err != nil {
   258  		t.Fatal(err)
   259  	}
   260  	defer b.Close()
   261  	_, gotErr := b.SignedURL(context.Background(), "key", nil)
   262  	if gcerrors.Code(gotErr) != gcerrors.Unimplemented {
   263  		t.Errorf("want Unimplemented error, got %v", gotErr)
   264  	}
   265  }
   266  
   267  type verifyAs struct {
   268  	prefix string
   269  }
   270  
   271  func (verifyAs) Name() string { return "verify As types for fileblob" }
   272  
   273  func (verifyAs) BucketCheck(b *blob.Bucket) error {
   274  	var fi os.FileInfo
   275  	if !b.As(&fi) {
   276  		return errors.New("Bucket.As failed")
   277  	}
   278  	return nil
   279  }
   280  func (verifyAs) BeforeRead(as func(interface{}) bool) error {
   281  	var f *os.File
   282  	if !as(&f) {
   283  		return errors.New("BeforeRead.As failed")
   284  	}
   285  	return nil
   286  }
   287  func (verifyAs) BeforeWrite(as func(interface{}) bool) error {
   288  	var f *os.File
   289  	if !as(&f) {
   290  		return errors.New("BeforeWrite.As failed")
   291  	}
   292  	return nil
   293  }
   294  func (verifyAs) BeforeCopy(as func(interface{}) bool) error {
   295  	var f *os.File
   296  	if !as(&f) {
   297  		return errors.New("BeforeCopy.As failed")
   298  	}
   299  	return nil
   300  }
   301  func (verifyAs) BeforeList(as func(interface{}) bool) error { return nil }
   302  func (verifyAs) BeforeSign(as func(interface{}) bool) error { return nil }
   303  func (verifyAs) AttributesCheck(attrs *blob.Attributes) error {
   304  	var fi os.FileInfo
   305  	if !attrs.As(&fi) {
   306  		return errors.New("Attributes.As failed")
   307  	}
   308  	return nil
   309  }
   310  func (verifyAs) ReaderCheck(r *blob.Reader) error {
   311  	var ior io.Reader
   312  	if !r.As(&ior) {
   313  		return errors.New("Reader.As failed")
   314  	}
   315  	return nil
   316  }
   317  func (verifyAs) ListObjectCheck(o *blob.ListObject) error {
   318  	var fi os.FileInfo
   319  	if !o.As(&fi) {
   320  		return errors.New("ListObject.As failed")
   321  	}
   322  	return nil
   323  }
   324  
   325  func (v verifyAs) ErrorCheck(b *blob.Bucket, err error) error {
   326  	var perr *os.PathError
   327  	if !b.ErrorAs(err, &perr) {
   328  		return errors.New("want ErrorAs to succeed for PathError")
   329  	}
   330  	wantSuffix := filepath.Join("go-cloud-fileblob", v.prefix, "key-does-not-exist")
   331  	if got := perr.Path; !strings.HasSuffix(got, wantSuffix) {
   332  		return fmt.Errorf("got path %q, want suffix %q", got, wantSuffix)
   333  	}
   334  	return nil
   335  }
   336  
   337  func TestOpenBucketFromURL(t *testing.T) {
   338  	const subdir = "mysubdir"
   339  	dir := filepath.Join(os.TempDir(), "fileblob")
   340  	if err := os.MkdirAll(dir, os.ModePerm); err != nil {
   341  		t.Fatal(err)
   342  	}
   343  	if err := os.MkdirAll(filepath.Join(dir, subdir), os.ModePerm); err != nil {
   344  		t.Fatal(err)
   345  	}
   346  	if err := ioutil.WriteFile(filepath.Join(dir, "myfile.txt"), []byte("hello world"), 0666); err != nil {
   347  		t.Fatal(err)
   348  	}
   349  	// To avoid making another temp dir, use the bucket directory to hold the secret key file.
   350  	secretKeyPath := filepath.Join(dir, "secret.key")
   351  	if err := ioutil.WriteFile(secretKeyPath, []byte("secret key"), 0666); err != nil {
   352  		t.Fatal(err)
   353  	}
   354  	if err := ioutil.WriteFile(filepath.Join(dir, subdir, "myfileinsubdir.txt"), []byte("hello world in subdir"), 0666); err != nil {
   355  		t.Fatal(err)
   356  	}
   357  	// Convert dir to a URL path, adding a leading "/" if needed on Windows.
   358  	dirpath := filepath.ToSlash(dir)
   359  	if os.PathSeparator != '/' && !strings.HasPrefix(dirpath, "/") {
   360  		dirpath = "/" + dirpath
   361  	}
   362  
   363  	tests := []struct {
   364  		URL         string
   365  		Key         string
   366  		WantErr     bool
   367  		WantReadErr bool
   368  		Want        string
   369  	}{
   370  		// Bucket doesn't exist -> error at construction time.
   371  		{"file:///bucket-not-found", "", true, false, ""},
   372  		// File doesn't exist -> error at read time.
   373  		{"file://" + dirpath, "filenotfound.txt", false, true, ""},
   374  		// Relative path using host="."; bucket is created but error at read time.
   375  		{"file://./../..", "filenotfound.txt", false, true, ""},
   376  		// OK.
   377  		{"file://" + dirpath, "myfile.txt", false, false, "hello world"},
   378  		// OK, host is ignored.
   379  		{"file://localhost" + dirpath, "myfile.txt", false, false, "hello world"},
   380  		// OK, with prefix.
   381  		{"file://" + dirpath + "?prefix=" + subdir + "/", "myfileinsubdir.txt", false, false, "hello world in subdir"},
   382  		// Subdir does not exist.
   383  		{"file://" + dirpath + "subdir", "", true, false, ""},
   384  		// Subdir does not exist, but create_dir creates it. Error is at file read time.
   385  		{"file://" + dirpath + "subdir2?create_dir=true", "filenotfound.txt", false, true, ""},
   386  		// Invalid query parameter.
   387  		{"file://" + dirpath + "?param=value", "myfile.txt", true, false, ""},
   388  		// Unrecognized value for parameter "metadata".
   389  		{"file://" + dirpath + "?metadata=nosuchstrategy", "myfile.txt", true, false, ""},
   390  		// OK, with params.
   391  		{
   392  			fmt.Sprintf("file://%s?base_url=/show&secret_key_path=%s", dirpath, secretKeyPath),
   393  			"myfile.txt", false, false, "hello world",
   394  		},
   395  		// Bad secret key filename.
   396  		{
   397  			fmt.Sprintf("file://%s?base_url=/show&secret_key_path=%s", dirpath, "bad"),
   398  			"myfile.txt", true, false, "",
   399  		},
   400  		// Missing base_url.
   401  		{
   402  			fmt.Sprintf("file://%s?secret_key_path=%s", dirpath, secretKeyPath),
   403  			"myfile.txt", true, false, "",
   404  		},
   405  		// Missing secret_key_path.
   406  		{"file://" + dirpath + "?base_url=/show", "myfile.txt", true, false, ""},
   407  	}
   408  
   409  	ctx := context.Background()
   410  	for i, test := range tests {
   411  		b, err := blob.OpenBucket(ctx, test.URL)
   412  		if b != nil {
   413  			defer b.Close()
   414  		}
   415  		if (err != nil) != test.WantErr {
   416  			t.Errorf("#%d: %s: got error %v, want error %v", i, test.URL, err, test.WantErr)
   417  		}
   418  		if err != nil {
   419  			continue
   420  		}
   421  		got, err := b.ReadAll(ctx, test.Key)
   422  		if (err != nil) != test.WantReadErr {
   423  			t.Errorf("%s: got read error %v, want error %v", test.URL, err, test.WantReadErr)
   424  		}
   425  		if err != nil {
   426  			continue
   427  		}
   428  		if string(got) != test.Want {
   429  			t.Errorf("%s: got %q want %q", test.URL, got, test.Want)
   430  		}
   431  	}
   432  }
   433  
   434  func TestListAtRoot(t *testing.T) {
   435  	if runtime.GOOS == "windows" {
   436  		t.Skip("/ as root is a unix concept")
   437  	}
   438  
   439  	ctx := context.Background()
   440  	b, err := OpenBucket("/", nil)
   441  	if err != nil {
   442  		t.Fatalf("Got error creating bucket; %#v", err)
   443  	}
   444  	defer b.Close()
   445  
   446  	dir, err := ioutil.TempDir("", "fileblob")
   447  	if err != nil {
   448  		t.Fatalf("Got error creating temp dir: %#v", err)
   449  	}
   450  	f, err := os.Create(filepath.Join(dir, "file.txt"))
   451  	if err != nil {
   452  		t.Fatalf("Got error creating file: %#v", err)
   453  	}
   454  	defer f.Close()
   455  
   456  	it := b.List(&blob.ListOptions{
   457  		Prefix: dir[1:],
   458  	})
   459  	obj, err := it.Next(ctx)
   460  	if err != nil {
   461  		t.Fatalf("Got error reading next item from list: %#v", err)
   462  	}
   463  	if obj.Key != filepath.Join(dir, "file.txt")[1:] {
   464  		t.Fatalf("Got unexpected filename in list: %q", obj.Key)
   465  	}
   466  	_, err = it.Next(ctx)
   467  	if err != io.EOF {
   468  		t.Fatalf("Expecting an EOF on next item in list, got: %#v", err)
   469  	}
   470  }
   471  
   472  func TestSkipMetadata(t *testing.T) {
   473  	dir, err := ioutil.TempDir("", "fileblob*")
   474  	if err != nil {
   475  		t.Fatalf("Got error creating temp dir: %#v", err)
   476  	}
   477  	defer os.RemoveAll(dir)
   478  	dirpath := filepath.ToSlash(dir)
   479  	if os.PathSeparator != '/' && !strings.HasPrefix(dirpath, "/") {
   480  		dirpath = "/" + dirpath
   481  	}
   482  
   483  	tests := []struct {
   484  		URL         string
   485  		wantSidecar bool
   486  	}{
   487  		{"file://" + dirpath + "?metadata=skip", false},
   488  		{"file://" + dirpath, true},                // Implicitly sets the default strategy…
   489  		{"file://" + dirpath + "?metadata=", true}, // … and explicitly.
   490  	}
   491  
   492  	ctx, cancel := context.WithCancel(context.Background())
   493  	defer cancel()
   494  	for _, test := range tests {
   495  		b, err := blob.OpenBucket(ctx, test.URL)
   496  		if b != nil {
   497  			defer b.Close()
   498  		}
   499  		if err != nil {
   500  			t.Fatal(err)
   501  		}
   502  
   503  		err = b.WriteAll(ctx, "key", []byte("hello world"), &blob.WriterOptions{
   504  			ContentType: "text/plain",
   505  		})
   506  		if err != nil {
   507  			t.Fatal(err)
   508  		}
   509  
   510  		_, err = os.Stat(filepath.Join(dir, "key"+attrsExt))
   511  		if gotSidecar := !errors.Is(err, os.ErrNotExist); test.wantSidecar != gotSidecar {
   512  			t.Errorf("Metadata sidecar file (extension %s) exists: %v, did we want it: %v",
   513  				attrsExt, gotSidecar, test.wantSidecar)
   514  		}
   515  		b.Delete(ctx, "key")
   516  	}
   517  }