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 }