// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package storage import ( "context" "errors" "fmt" "log" "net/url" "os" "strconv" "strings" "testing" "time" "cloud.google.com/go/iam/apiv1/iampb" "github.com/google/go-cmp/cmp" "github.com/googleapis/gax-go/v2" "github.com/googleapis/gax-go/v2/apierror" "github.com/googleapis/gax-go/v2/callctx" "google.golang.org/api/iterator" "google.golang.org/grpc/codes" ) var emulatorClients map[string]storageClient var veneerClient *Client func TestCreateBucketEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { want := &BucketAttrs{ Name: bucket, Logging: &BucketLogging{ LogBucket: bucket, }, } got, err := client.CreateBucket(context.Background(), project, want.Name, want, nil) if err != nil { t.Fatal(err) } want.Location = "US" if diff := cmp.Diff(got.Name, want.Name); diff != "" { t.Errorf("Name got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.Location, want.Location); diff != "" { t.Errorf("Location got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.Logging.LogBucket, want.Logging.LogBucket); diff != "" { t.Errorf("LogBucket got(-),want(+):\n%s", diff) } }) } func TestDeleteBucketEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { b := &BucketAttrs{ Name: bucket, } // Create the bucket that will be deleted. _, err := client.CreateBucket(context.Background(), project, b.Name, b, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } // Delete the bucket that was just created. err = client.DeleteBucket(context.Background(), b.Name, nil) if err != nil { t.Fatalf("client.DeleteBucket: %v", err) } }) } func TestGetBucketEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { want := &BucketAttrs{ Name: bucket, } // Create the bucket that will be retrieved. _, err := client.CreateBucket(context.Background(), project, want.Name, want, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } got, err := client.GetBucket(context.Background(), want.Name, &BucketConditions{MetagenerationMatch: 1}) if err != nil { t.Fatal(err) } if diff := cmp.Diff(got.Name, want.Name); diff != "" { t.Errorf("got(-),want(+):\n%s", diff) } }) } func TestUpdateBucketEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { bkt := &BucketAttrs{ Name: bucket, } // Create the bucket that will be updated. _, err := client.CreateBucket(context.Background(), project, bkt.Name, bkt, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } ua := &BucketAttrsToUpdate{ VersioningEnabled: false, RequesterPays: false, DefaultEventBasedHold: false, Encryption: &BucketEncryption{DefaultKMSKeyName: "key2"}, Lifecycle: &Lifecycle{ Rules: []LifecycleRule{ { Action: LifecycleAction{Type: "Delete"}, Condition: LifecycleCondition{AgeInDays: 30}, }, }, }, Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, StorageClass: "NEARLINE", RPO: RPOAsyncTurbo, } want := &BucketAttrs{ Name: bucket, VersioningEnabled: false, RequesterPays: false, DefaultEventBasedHold: false, Encryption: &BucketEncryption{DefaultKMSKeyName: "key2"}, Lifecycle: Lifecycle{ Rules: []LifecycleRule{ { Action: LifecycleAction{Type: "Delete"}, Condition: LifecycleCondition{AgeInDays: 30}, }, }, }, Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, StorageClass: "NEARLINE", RPO: RPOAsyncTurbo, } got, err := client.UpdateBucket(context.Background(), bucket, ua, &BucketConditions{MetagenerationMatch: 1}) if err != nil { t.Fatal(err) } if diff := cmp.Diff(got.Name, want.Name); diff != "" { t.Errorf("Name: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.VersioningEnabled, want.VersioningEnabled); diff != "" { t.Errorf("VersioningEnabled: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.RequesterPays, want.RequesterPays); diff != "" { t.Errorf("RequesterPays: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.DefaultEventBasedHold, want.DefaultEventBasedHold); diff != "" { t.Errorf("DefaultEventBasedHold: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.Encryption, want.Encryption); diff != "" { t.Errorf("Encryption: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.Lifecycle, want.Lifecycle); diff != "" { t.Errorf("Lifecycle: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.Logging, want.Logging); diff != "" { t.Errorf("Logging: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.Website, want.Website); diff != "" { t.Errorf("Website: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.RPO, want.RPO); diff != "" { t.Errorf("RPO: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.StorageClass, want.StorageClass); diff != "" { t.Errorf("StorageClass: got(-),want(+):\n%s", diff) } }) } func TestGetServiceAccountEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { _, err := client.GetServiceAccount(context.Background(), project) if err != nil { t.Fatalf("client.GetServiceAccount: %v", err) } }) } func TestGetSetTestIamPolicyEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { battrs, err := client.CreateBucket(context.Background(), project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } got, err := client.GetIamPolicy(context.Background(), battrs.Name, 0) if err != nil { t.Fatalf("client.GetIamPolicy: %v", err) } err = client.SetIamPolicy(context.Background(), battrs.Name, &iampb.Policy{ Etag: got.GetEtag(), Bindings: []*iampb.Binding{{Role: "roles/viewer", Members: []string{"allUsers"}}}, }) if err != nil { t.Fatalf("client.SetIamPolicy: %v", err) } want := []string{"storage.foo", "storage.bar"} perms, err := client.TestIamPermissions(context.Background(), battrs.Name, want) if err != nil { t.Fatalf("client.TestIamPermissions: %v", err) } if diff := cmp.Diff(perms, want); diff != "" { t.Errorf("got(-),want(+):\n%s", diff) } }) } func TestDeleteObjectEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test object that will be deleted. _, err := client.CreateBucket(context.Background(), project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } want := ObjectAttrs{ Bucket: bucket, Name: fmt.Sprintf("testObject-%d", time.Now().Nanosecond()), } w := veneerClient.Bucket(bucket).Object(want.Name).NewWriter(context.Background()) if _, err := w.Write(randomBytesToWrite); err != nil { t.Fatalf("failed to populate test object: %v", err) } if err := w.Close(); err != nil { t.Fatalf("closing object: %v", err) } err = client.DeleteObject(context.Background(), bucket, want.Name, defaultGen, nil) if err != nil { t.Fatalf("client.DeleteBucket: %v", err) } }) } func TestGetObjectEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test object. _, err := client.CreateBucket(context.Background(), project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } want := ObjectAttrs{ Bucket: bucket, Name: fmt.Sprintf("testObject-%d", time.Now().Nanosecond()), } w := veneerClient.Bucket(bucket).Object(want.Name).NewWriter(context.Background()) if _, err := w.Write(randomBytesToWrite); err != nil { t.Fatalf("failed to populate test object: %v", err) } if err := w.Close(); err != nil { t.Fatalf("closing object: %v", err) } got, err := client.GetObject(context.Background(), &getObjectParams{bucket: bucket, object: want.Name, gen: defaultGen}) if err != nil { t.Fatal(err) } if diff := cmp.Diff(got.Name, want.Name); diff != "" { t.Errorf("got(-),want(+):\n%s", diff) } }) } func TestRewriteObjectEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test object. _, err := client.CreateBucket(context.Background(), project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } src := ObjectAttrs{ Bucket: bucket, Name: fmt.Sprintf("testObject-%d", time.Now().Nanosecond()), } w := veneerClient.Bucket(bucket).Object(src.Name).NewWriter(context.Background()) if _, err := w.Write(randomBytesToWrite); err != nil { t.Fatalf("failed to populate test object: %v", err) } if err := w.Close(); err != nil { t.Fatalf("closing object: %v", err) } req := &rewriteObjectRequest{ dstObject: destinationObject{ bucket: bucket, name: fmt.Sprintf("copy-of-%s", src.Name), attrs: &ObjectAttrs{}, }, srcObject: sourceObject{ bucket: bucket, name: src.Name, gen: defaultGen, }, } got, err := client.RewriteObject(context.Background(), req) if err != nil { t.Fatal(err) } if !got.done { t.Fatal("didn't finish writing!") } if want := int64(len(randomBytesToWrite)); got.written != want { t.Errorf("Bytes written: got %d, want %d", got.written, want) } }) } func TestUpdateObjectEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test object. _, err := client.CreateBucket(context.Background(), project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } ct := time.Date(2022, 5, 25, 12, 12, 12, 0, time.UTC) o := ObjectAttrs{ Bucket: bucket, Name: fmt.Sprintf("testObject-%d", time.Now().Nanosecond()), CustomTime: ct, } w := veneerClient.Bucket(bucket).Object(o.Name).NewWriter(context.Background()) if _, err := w.Write(randomBytesToWrite); err != nil { t.Fatalf("failed to populate test object: %v", err) } if err := w.Close(); err != nil { t.Fatalf("closing object: %v", err) } want := &ObjectAttrsToUpdate{ EventBasedHold: false, TemporaryHold: false, ContentType: "text/html", ContentLanguage: "en", ContentEncoding: "gzip", ContentDisposition: "", CacheControl: "", CustomTime: ct.Add(10 * time.Hour), } params := &updateObjectParams{bucket: bucket, object: o.Name, uattrs: want, gen: defaultGen, conds: &Conditions{MetagenerationMatch: 1}} got, err := client.UpdateObject(context.Background(), params) if err != nil { t.Fatalf("client.UpdateObject: %v", err) } if diff := cmp.Diff(got.Name, o.Name); diff != "" { t.Errorf("Name: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.EventBasedHold, want.EventBasedHold); diff != "" { t.Errorf("EventBasedHold: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.TemporaryHold, want.TemporaryHold); diff != "" { t.Errorf("TemporaryHold: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.ContentType, want.ContentType); diff != "" { t.Errorf("ContentType: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.ContentLanguage, want.ContentLanguage); diff != "" { t.Errorf("ContentLanguage: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.ContentEncoding, want.ContentEncoding); diff != "" { t.Errorf("ContentEncoding: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.ContentDisposition, want.ContentDisposition); diff != "" { t.Errorf("ContentDisposition: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.CacheControl, want.CacheControl); diff != "" { t.Errorf("CacheControl: got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.CustomTime, want.CustomTime); diff != "" { t.Errorf("CustomTime: got(-),want(+):\n%s", diff) } }) } func TestListObjectsEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test data. _, err := client.CreateBucket(context.Background(), project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } prefix := time.Now().Nanosecond() want := []*ObjectAttrs{ { Bucket: bucket, Name: fmt.Sprintf("%d-object-%d", prefix, time.Now().Nanosecond()), }, { Bucket: bucket, Name: fmt.Sprintf("%d-object-%d", prefix, time.Now().Nanosecond()), }, { Bucket: bucket, Name: fmt.Sprintf("object-%d", time.Now().Nanosecond()), }, } for _, obj := range want { w := veneerClient.Bucket(bucket).Object(obj.Name).NewWriter(context.Background()) if _, err := w.Write(randomBytesToWrite); err != nil { t.Fatalf("failed to populate test data: %v", err) } if err := w.Close(); err != nil { t.Fatalf("closing object: %v", err) } } // Simple list, no query. it := client.ListObjects(context.Background(), bucket, nil) var o *ObjectAttrs var got int for i := 0; err == nil && i <= len(want); i++ { o, err = it.Next() if err != nil { break } got++ if diff := cmp.Diff(o.Name, want[i].Name); diff != "" { t.Errorf("got(-),want(+):\n%s", diff) } } if err != iterator.Done { t.Fatalf("expected %q but got %q", iterator.Done, err) } expected := len(want) if got != expected { t.Errorf("expected to get %d objects, but got %d", expected, got) } }) } func TestListObjectsWithPrefixEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test data. _, err := client.CreateBucket(context.Background(), project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } prefix := time.Now().Nanosecond() want := []*ObjectAttrs{ { Bucket: bucket, Name: fmt.Sprintf("%d-object-%d", prefix, time.Now().Nanosecond()), }, { Bucket: bucket, Name: fmt.Sprintf("%d-object-%d", prefix, time.Now().Nanosecond()), }, { Bucket: bucket, Name: fmt.Sprintf("object-%d", time.Now().Nanosecond()), }, } for _, obj := range want { w := veneerClient.Bucket(bucket).Object(obj.Name).NewWriter(context.Background()) if _, err := w.Write(randomBytesToWrite); err != nil { t.Fatalf("failed to populate test data: %v", err) } if err := w.Close(); err != nil { t.Fatalf("closing object: %v", err) } } // Query with Prefix. it := client.ListObjects(context.Background(), bucket, &Query{Prefix: strconv.Itoa(prefix)}) var o *ObjectAttrs var got int want = want[:2] for i := 0; i <= len(want); i++ { o, err = it.Next() if err != nil { break } got++ if diff := cmp.Diff(o.Name, want[i].Name); diff != "" { t.Errorf("got(-),want(+):\n%s", diff) } } if err != iterator.Done { t.Fatalf("expected %q but got %q", iterator.Done, err) } expected := len(want) if got != expected { t.Errorf("expected to get %d objects, but got %d", expected, got) } }) } func TestListBucketsEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { prefix := time.Now().Nanosecond() want := []*BucketAttrs{ {Name: fmt.Sprintf("%d-%s-%d", prefix, bucket, time.Now().Nanosecond())}, {Name: fmt.Sprintf("%d-%s-%d", prefix, bucket, time.Now().Nanosecond())}, {Name: fmt.Sprintf("%s-%d", bucket, time.Now().Nanosecond())}, } // Create the buckets that will be listed. for _, b := range want { _, err := client.CreateBucket(context.Background(), project, b.Name, b, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } } it := client.ListBuckets(context.Background(), project) it.Prefix = strconv.Itoa(prefix) // Drop the non-prefixed bucket from the expected results. want = want[:2] var err error var b *BucketAttrs var got int for i := 0; err == nil && i <= len(want); i++ { b, err = it.Next() if err != nil { break } got++ if diff := cmp.Diff(b.Name, want[i].Name); diff != "" { t.Errorf("got(-),want(+):\n%s", diff) } } if err != iterator.Done { t.Fatalf("expected %q but got %q", iterator.Done, err) } expected := len(want) if got != expected { t.Errorf("expected to get %d buckets, but got %d", expected, got) } }) } func TestListBucketACLsEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { ctx := context.Background() attrs := &BucketAttrs{ Name: bucket, PredefinedACL: "publicRead", } // Create the bucket that will be retrieved. if _, err := client.CreateBucket(ctx, project, attrs.Name, attrs, nil); err != nil { t.Fatalf("client.CreateBucket: %v", err) } acls, err := client.ListBucketACLs(ctx, bucket) if err != nil { t.Fatalf("client.ListBucketACLs: %v", err) } if want, got := len(acls), 2; want != got { t.Errorf("ListBucketACLs: got %v, want %v items", acls, want) } }) } func TestUpdateBucketACLEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { ctx := context.Background() attrs := &BucketAttrs{ Name: bucket, PredefinedACL: "authenticatedRead", } // Create the bucket that will be retrieved. if _, err := client.CreateBucket(ctx, project, attrs.Name, attrs, nil); err != nil { t.Fatalf("client.CreateBucket: %v", err) } var listAcls []ACLRule var err error // Assert bucket has two BucketACL entities, including project owner and predefinedACL. if listAcls, err = client.ListBucketACLs(ctx, bucket); err != nil { t.Fatalf("client.ListBucketACLs: %v", err) } if got, want := len(listAcls), 2; got != want { t.Errorf("ListBucketACLs: got %v, want %v items", listAcls, want) } entity := AllUsers role := RoleReader err = client.UpdateBucketACL(ctx, bucket, entity, role) if err != nil { t.Fatalf("client.UpdateBucketACL: %v", err) } // Assert bucket now has three BucketACL entities, including existing ACLs. if listAcls, err = client.ListBucketACLs(ctx, bucket); err != nil { t.Fatalf("client.ListBucketACLs: %v", err) } if got, want := len(listAcls), 3; got != want { t.Errorf("ListBucketACLs: got %v, want %v items", listAcls, want) } }) } func TestDeleteBucketACLEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { ctx := context.Background() attrs := &BucketAttrs{ Name: bucket, PredefinedACL: "publicRead", } // Create the bucket that will be retrieved. if _, err := client.CreateBucket(ctx, project, attrs.Name, attrs, nil); err != nil { t.Fatalf("client.CreateBucket: %v", err) } // Assert bucket has two BucketACL entities, including project owner and predefinedACL. acls, err := client.ListBucketACLs(ctx, bucket) if err != nil { t.Fatalf("client.ListBucketACLs: %v", err) } if got, want := len(acls), 2; got != want { t.Errorf("ListBucketACLs: got %v, want %v items", acls, want) } // Delete one BucketACL with AllUsers entity. if err := client.DeleteBucketACL(ctx, bucket, AllUsers); err != nil { t.Fatalf("client.DeleteBucketACL: %v", err) } // Assert bucket has one BucketACL. acls, err = client.ListBucketACLs(ctx, bucket) if err != nil { t.Fatalf("client.ListBucketACLs: %v", err) } if got, want := len(acls), 1; got != want { t.Errorf("ListBucketACLs: got %v, want %v items", acls, want) } }) } func TestDefaultObjectACLCRUDEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { ctx := context.Background() attrs := &BucketAttrs{ Name: bucket, PredefinedDefaultObjectACL: "publicRead", } // Create the bucket that will be retrieved. if _, err := client.CreateBucket(ctx, project, attrs.Name, attrs, nil); err != nil { t.Fatalf("client.CreateBucket: %v", err) } // Assert bucket has 2 DefaultObjectACL entities, including project owner and PredefinedDefaultObjectACL. acls, err := client.ListDefaultObjectACLs(ctx, bucket) if err != nil { t.Fatalf("client.ListDefaultObjectACLs: %v", err) } if got, want := len(acls), 2; got != want { t.Errorf("ListDefaultObjectACLs: got %v, want %v items", acls, want) } entity := AllAuthenticatedUsers role := RoleOwner err = client.UpdateDefaultObjectACL(ctx, bucket, entity, role) if err != nil { t.Fatalf("UpdateDefaultObjectCL: %v", err) } // Assert there are now 3 DefaultObjectACL entities, including existing DefaultObjectACLs. acls, err = client.ListDefaultObjectACLs(ctx, bucket) if err != nil { t.Fatalf("client.ListDefaultObjectACLs: %v", err) } if got, want := len(acls), 3; got != want { t.Errorf("ListDefaultObjectACLs: %v got %v, want %v items", len(acls), acls, want) } // Delete 1 DefaultObjectACL with AllUsers entity. if err := client.DeleteDefaultObjectACL(ctx, bucket, AllUsers); err != nil { t.Fatalf("client.DeleteDefaultObjectACL: %v", err) } // Assert bucket has 2 DefaultObjectACL entities. acls, err = client.ListDefaultObjectACLs(ctx, bucket) if err != nil { t.Fatalf("client.ListDefaultObjectACLs: %v", err) } if got, want := len(acls), 2; got != want { t.Errorf("ListDefaultObjectACLs: %v got %v, want %v items", len(acls), acls, want) } }) } func TestObjectACLCRUDEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test object. ctx := context.Background() _, err := client.CreateBucket(ctx, project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("CreateBucket: %v", err) } o := ObjectAttrs{ Bucket: bucket, Name: fmt.Sprintf("testObject-%d", time.Now().Nanosecond()), } w := veneerClient.Bucket(bucket).Object(o.Name).NewWriter(context.Background()) if _, err := w.Write(randomBytesToWrite); err != nil { t.Fatalf("failed to populate test object: %v", err) } if err := w.Close(); err != nil { t.Fatalf("closing object: %v", err) } var listAcls []ACLRule // Assert there are 4 ObjectACL entities, including object owner and project owners/editors/viewers. if listAcls, err = client.ListObjectACLs(ctx, bucket, o.Name); err != nil { t.Fatalf("ListObjectACLs: %v", err) } if got, want := len(listAcls), 4; got != want { t.Errorf("ListObjectACLs: got %v, want %v items", listAcls, want) } entity := AllUsers role := RoleReader err = client.UpdateObjectACL(ctx, bucket, o.Name, entity, role) if err != nil { t.Fatalf("UpdateObjectCL: %v", err) } // Assert there are now 5 ObjectACL entities, including existing ACLs. if listAcls, err = client.ListObjectACLs(ctx, bucket, o.Name); err != nil { t.Fatalf("ListObjectACLs: %v", err) } if got, want := len(listAcls), 5; got != want { t.Errorf("ListObjectACLs: got %v, want %v items", listAcls, want) } if err = client.DeleteObjectACL(ctx, bucket, o.Name, AllUsers); err != nil { t.Fatalf("client.DeleteObjectACL: %v", err) } // Assert there are now 4 ObjectACL entities after deletion. if listAcls, err = client.ListObjectACLs(ctx, bucket, o.Name); err != nil { t.Fatalf("ListObjectACLs: %v", err) } if got, want := len(listAcls), 4; got != want { t.Errorf("ListObjectACLs: got %v, want %v items", listAcls, want) } }) } func TestOpenReaderEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test data. _, err := client.CreateBucket(context.Background(), project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } prefix := time.Now().Nanosecond() want := &ObjectAttrs{ Bucket: bucket, Name: fmt.Sprintf("%d-object-%d", prefix, time.Now().Nanosecond()), } w := veneerClient.Bucket(bucket).Object(want.Name).NewWriter(context.Background()) if _, err := w.Write(randomBytesToWrite); err != nil { t.Fatalf("failed to populate test data: %v", err) } if err := w.Close(); err != nil { t.Fatalf("closing object: %v", err) } params := &newRangeReaderParams{ bucket: bucket, object: want.Name, gen: defaultGen, offset: 0, length: -1, } r, err := client.NewRangeReader(context.Background(), params) if err != nil { t.Fatalf("opening reading: %v", err) } wantLen := len(randomBytesToWrite) got := make([]byte, wantLen) n, err := r.Read(got) if n != wantLen { t.Fatalf("expected to read %d bytes, but got %d", wantLen, n) } if diff := cmp.Diff(got, randomBytesToWrite); diff != "" { t.Fatalf("Read: got(-),want(+):\n%s", diff) } }) } func TestOpenWriterEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test data. _, err := client.CreateBucket(context.Background(), project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } prefix := time.Now().Nanosecond() want := &ObjectAttrs{ Bucket: bucket, Name: fmt.Sprintf("%d-object-%d", prefix, time.Now().Nanosecond()), Generation: defaultGen, } var gotAttrs *ObjectAttrs params := &openWriterParams{ attrs: want, bucket: bucket, ctx: context.Background(), donec: make(chan struct{}), setError: func(_ error) {}, // no-op progress: func(_ int64) {}, // no-op setObj: func(o *ObjectAttrs) { gotAttrs = o }, } pw, err := client.OpenWriter(params) if err != nil { t.Fatalf("failed to open writer: %v", err) } if _, err := pw.Write(randomBytesToWrite); err != nil { t.Fatalf("failed to populate test data: %v", err) } if err := pw.Close(); err != nil { t.Fatalf("closing object: %v", err) } select { case <-params.donec: } if gotAttrs == nil { t.Fatalf("Writer finished, but resulting object wasn't set") } if diff := cmp.Diff(gotAttrs.Name, want.Name); diff != "" { t.Fatalf("Resulting object name: got(-),want(+):\n%s", diff) } r, err := veneerClient.Bucket(bucket).Object(want.Name).NewReader(context.Background()) if err != nil { t.Fatalf("opening reading: %v", err) } wantLen := len(randomBytesToWrite) got := make([]byte, wantLen) n, err := r.Read(got) if n != wantLen { t.Fatalf("expected to read %d bytes, but got %d", wantLen, n) } if diff := cmp.Diff(got, randomBytesToWrite); diff != "" { t.Fatalf("checking written content: got(-),want(+):\n%s", diff) } }) } func TestListNotificationsEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test object. ctx := context.Background() _, err := client.CreateBucket(ctx, project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } _, err = client.CreateNotification(ctx, bucket, &Notification{ TopicProjectID: project, TopicID: "go-storage-notification-test", PayloadFormat: "JSON_API_V1", }) if err != nil { t.Fatalf("client.CreateNotification: %v", err) } n, err := client.ListNotifications(ctx, bucket) if err != nil { t.Fatalf("client.ListNotifications: %v", err) } if want, got := 1, len(n); want != got { t.Errorf("ListNotifications: got %v, want %v items", n, want) } }) } func TestCreateNotificationEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test object. ctx := context.Background() _, err := client.CreateBucket(ctx, project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } want := &Notification{ TopicProjectID: project, TopicID: "go-storage-notification-test", PayloadFormat: "JSON_API_V1", } got, err := client.CreateNotification(ctx, bucket, want) if err != nil { t.Fatalf("client.CreateNotification: %v", err) } if diff := cmp.Diff(got.TopicID, want.TopicID); diff != "" { t.Errorf("CreateNotification topic: got(-),want(+):\n%s", diff) } }) } func TestDeleteNotificationEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { // Populate test object. ctx := context.Background() _, err := client.CreateBucket(ctx, project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } var n *Notification n, err = client.CreateNotification(ctx, bucket, &Notification{ TopicProjectID: project, TopicID: "go-storage-notification-test", PayloadFormat: "JSON_API_V1", }) if err != nil { t.Fatalf("client.CreateNotification: %v", err) } err = client.DeleteNotification(ctx, bucket, n.ID) if err != nil { t.Fatalf("client.DeleteNotification: %v", err) } }) } func initEmulatorClients() func() error { noopCloser := func() error { return nil } if !isEmulatorEnvironmentSet() { return noopCloser } ctx := context.Background() grpcClient, err := newGRPCStorageClient(ctx) if err != nil { log.Fatalf("Error setting up gRPC client for emulator tests: %v", err) return noopCloser } httpClient, err := newHTTPStorageClient(ctx) if err != nil { log.Fatalf("Error setting up HTTP client for emulator tests: %v", err) return noopCloser } emulatorClients = map[string]storageClient{ "http": httpClient, "grpc": grpcClient, } veneerClient, err = NewClient(ctx) if err != nil { log.Fatalf("Error setting up Veneer client for emulator tests: %v", err) return noopCloser } return func() error { gerr := grpcClient.Close() herr := httpClient.Close() verr := veneerClient.Close() if gerr != nil { return gerr } else if herr != nil { return herr } return verr } } func TestLockBucketRetentionPolicyEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { b := &BucketAttrs{ Name: bucket, RetentionPolicy: &RetentionPolicy{ RetentionPeriod: time.Minute, }, } // Create the bucket that will be locked. _, err := client.CreateBucket(context.Background(), project, b.Name, b, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } // Lock the bucket's retention policy. err = client.LockBucketRetentionPolicy(context.Background(), b.Name, &BucketConditions{MetagenerationMatch: 1}) if err != nil { t.Fatalf("client.LockBucketRetentionPolicy: %v", err) } got, err := client.GetBucket(context.Background(), bucket, nil) if err != nil { t.Fatalf("client.GetBucket: %v", err) } if !got.RetentionPolicy.IsLocked { t.Error("Expected bucket retention policy to be locked, but was not.") } }) } func TestComposeEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { ctx := context.Background() // Populate test data. _, err := client.CreateBucket(context.Background(), project, bucket, &BucketAttrs{ Name: bucket, }, nil) if err != nil { t.Fatalf("client.CreateBucket: %v", err) } prefix := time.Now().Nanosecond() srcNames := []string{ fmt.Sprintf("%d-object1", prefix), fmt.Sprintf("%d-object2", prefix), } for _, n := range srcNames { w := veneerClient.Bucket(bucket).Object(n).NewWriter(ctx) if _, err := w.Write(randomBytesToWrite); err != nil { t.Fatalf("failed to populate test data: %v", err) } if err := w.Close(); err != nil { t.Fatalf("closing object: %v", err) } } dstName := fmt.Sprintf("%d-object3", prefix) req := composeObjectRequest{ dstBucket: bucket, dstObject: destinationObject{ name: dstName, attrs: &ObjectAttrs{StorageClass: "COLDLINE"}, }, srcs: []sourceObject{ {name: srcNames[0]}, {name: srcNames[1]}, }, } attrs, err := client.ComposeObject(ctx, &req) if err != nil { t.Fatalf("client.ComposeObject(): %v", err) } if got := attrs.Name; got != dstName { t.Errorf("attrs.Name: got %v, want %v", got, dstName) } // Check that the destination object size is equal to the sum of its // sources. if got, want := attrs.Size, 2*len(randomBytesToWrite); got != int64(want) { t.Errorf("attrs.Size: got %v, want %v", got, want) } // Check that destination attrs set via object attrs are preserved. if got, want := attrs.StorageClass, "COLDLINE"; got != want { t.Errorf("attrs.StorageClass: got %v, want %v", got, want) } }) } func TestHMACKeyCRUDEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { ctx := context.Background() serviceAccountEmail := "test@test-project.iam.gserviceaccount.com" want, err := client.CreateHMACKey(ctx, project, serviceAccountEmail) if err != nil { t.Fatalf("CreateHMACKey: %v", err) } if want == nil { t.Fatal("CreateHMACKey: Unexpectedly got back a nil HMAC key") } if want.State != Active { t.Fatalf("CreateHMACKey: Unexpected state %q, expected %q", want.State, Active) } got, err := client.GetHMACKey(ctx, project, want.AccessID) if err != nil { t.Fatalf("GetHMACKey: %v", err) } if diff := cmp.Diff(got.ID, want.ID); diff != "" { t.Errorf("GetHMACKey ID:got(-),want(+):\n%s", diff) } if diff := cmp.Diff(got.UpdatedTime, want.UpdatedTime); diff != "" { t.Errorf("GetHMACKey UpdatedTime: got(-),want(+):\n%s", diff) } attr := &HMACKeyAttrsToUpdate{ State: Inactive, } got, err = client.UpdateHMACKey(ctx, project, serviceAccountEmail, want.AccessID, attr) if err != nil { t.Fatalf("UpdateHMACKey: %v", err) } if got.State != attr.State { t.Errorf("UpdateHMACKey State: got %v, want %v", got.State, attr.State) } showDeletedKeys := false it := client.ListHMACKeys(ctx, project, serviceAccountEmail, showDeletedKeys) var count int var e error for ; ; count++ { _, e = it.Next() if e != nil { break } } if e != iterator.Done { t.Fatalf("ListHMACKeys: expected %q but got %q", iterator.Done, err) } if expected := 1; count != expected { t.Errorf("ListHMACKeys: expected to get %d hmacKeys, but got %d", expected, count) } err = client.DeleteHMACKey(ctx, project, want.AccessID) if err != nil { t.Fatalf("DeleteHMACKey: %v", err) } got, err = client.GetHMACKey(ctx, project, want.AccessID) if err == nil { t.Fatalf("GetHMACKey unexcepted error: wanted 404") } }) } func TestBucketConditionsEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { ctx := context.Background() cases := []struct { name string call func(bucket string, metaGen int64) error }{ { name: "get", call: func(bucket string, metaGen int64) error { _, err := client.GetBucket(ctx, bucket, &BucketConditions{MetagenerationMatch: metaGen}) return err }, }, { name: "update", call: func(bucket string, metaGen int64) error { _, err := client.UpdateBucket(ctx, bucket, &BucketAttrsToUpdate{StorageClass: "ARCHIVE"}, &BucketConditions{MetagenerationMatch: metaGen}) return err }, }, { name: "delete", call: func(bucket string, metaGen int64) error { return client.DeleteBucket(ctx, bucket, &BucketConditions{MetagenerationMatch: metaGen}) }, }, { name: "lockRetentionPolicy", call: func(bucket string, metaGen int64) error { return client.LockBucketRetentionPolicy(ctx, bucket, &BucketConditions{MetagenerationMatch: metaGen}) }, }, } for _, c := range cases { t.Run(c.name, func(r *testing.T) { bucket, metaGen, err := createBucket(ctx, project) if err != nil { r.Fatalf("creating bucket: %v", err) } if err := c.call(bucket, metaGen); err != nil { r.Errorf("error: %v", err) } }) } }) } func TestObjectConditionsEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { ctx := context.Background() // Create test bucket if _, err := client.CreateBucket(context.Background(), project, bucket, &BucketAttrs{Name: bucket}, nil); err != nil { t.Fatalf("client.CreateBucket: %v", err) } cases := []struct { name string call func() error }{ { name: "update generation", call: func() error { objName, gen, _, err := createObject(ctx, bucket) if err != nil { return fmt.Errorf("creating object: %w", err) } uattrs := &ObjectAttrsToUpdate{CustomTime: time.Now()} _, err = client.UpdateObject(ctx, &updateObjectParams{bucket: bucket, object: objName, uattrs: uattrs, gen: gen}) return err }, }, { name: "update ifMetagenerationMatch", call: func() error { objName, gen, metaGen, err := createObject(ctx, bucket) if err != nil { return fmt.Errorf("creating object: %w", err) } uattrs := &ObjectAttrsToUpdate{CustomTime: time.Now()} conds := &Conditions{ GenerationMatch: gen, MetagenerationMatch: metaGen, } _, err = client.UpdateObject(ctx, &updateObjectParams{bucket: bucket, object: objName, uattrs: uattrs, gen: gen, conds: conds}) return err }, }, { name: "write ifGenerationMatch", call: func() error { var err error _, err = client.OpenWriter(&openWriterParams{ ctx: ctx, chunkSize: 256 * 1024, chunkRetryDeadline: 0, bucket: bucket, attrs: &ObjectAttrs{}, conds: &Conditions{DoesNotExist: true}, encryptionKey: nil, sendCRC32C: false, donec: nil, setError: func(e error) { if e != nil { err = e } }, progress: nil, setObj: nil, }) return err }, }, { name: "rewrite ifMetagenerationMatch", call: func() error { objName, gen, metaGen, err := createObject(ctx, bucket) if err != nil { return fmt.Errorf("creating object: %w", err) } _, err = client.RewriteObject(ctx, &rewriteObjectRequest{ srcObject: sourceObject{ name: objName, bucket: bucket, gen: gen, conds: &Conditions{ GenerationMatch: gen, MetagenerationMatch: metaGen, }, }, dstObject: destinationObject{ name: fmt.Sprintf("%d-object", time.Now().Nanosecond()), bucket: bucket, conds: &Conditions{ DoesNotExist: true, }, attrs: &ObjectAttrs{}, }, }) return err }, }, { name: "compose ifGenerationMatch", call: func() error { obj1, obj1Gen, _, err := createObject(ctx, bucket) if err != nil { return fmt.Errorf("creating object: %w", err) } obj2, obj2Gen, _, err := createObject(ctx, bucket) if err != nil { return fmt.Errorf("creating object: %w", err) } _, err = client.ComposeObject(ctx, &composeObjectRequest{ dstBucket: bucket, dstObject: destinationObject{ name: fmt.Sprintf("%d-object", time.Now().Nanosecond()), bucket: bucket, conds: &Conditions{DoesNotExist: true}, attrs: &ObjectAttrs{}, }, srcs: []sourceObject{ { name: obj1, bucket: bucket, gen: obj1Gen, conds: &Conditions{ GenerationMatch: obj1Gen, }, }, { name: obj2, bucket: bucket, conds: &Conditions{ GenerationMatch: obj2Gen, }, }, }, }) return err }, }, { name: "delete ifGenerationMatch", call: func() error { objName, gen, _, err := createObject(ctx, bucket) if err != nil { return fmt.Errorf("creating object: %w", err) } err = client.DeleteObject(ctx, bucket, objName, gen, &Conditions{GenerationMatch: gen}) return err }, }, { name: "get ifMetagenerationMatch", call: func() error { objName, gen, metaGen, err := createObject(ctx, bucket) if err != nil { return fmt.Errorf("creating object: %w", err) } _, err = client.GetObject(ctx, &getObjectParams{bucket: bucket, object: objName, gen: gen, conds: &Conditions{GenerationMatch: gen, MetagenerationMatch: metaGen}}) return err }, }, } for _, c := range cases { t.Run(c.name, func(r *testing.T) { if err := c.call(); err != nil { r.Errorf("error: %v", err) } }) } }) } // Test that RetryNever prevents any retries from happening in both transports. func TestRetryNeverEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { ctx := context.Background() instructions := map[string][]string{"storage.buckets.get": {"return-503"}} testID := createRetryTest(t, project, bucket, client, instructions) ctx = callctx.SetHeaders(ctx, "x-retry-test-id", testID) _, err := client.GetBucket(ctx, bucket, nil, withRetryConfig(&retryConfig{policy: RetryNever})) var ae *apierror.APIError if errors.As(err, &ae) { // We expect a 503/UNAVAILABLE error. For anything else including a nil // error, the test should fail. if ae.GRPCStatus().Code() != codes.Unavailable && ae.HTTPCode() != 503 { t.Errorf("GetBucket: got unexpected error %v; want 503", err) } } }) } // Test that errors are wrapped correctly if retry happens until a timeout. func TestRetryTimeoutEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { ctx := context.Background() instructions := map[string][]string{"storage.buckets.get": {"return-503", "return-503", "return-503", "return-503", "return-503"}} testID := createRetryTest(t, project, bucket, client, instructions) ctx = callctx.SetHeaders(ctx, "x-retry-test-id", testID) ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() _, err := client.GetBucket(ctx, bucket, nil, idempotent(true)) var ae *apierror.APIError if errors.As(err, &ae) { // We expect a 503/UNAVAILABLE error. For anything else including a nil // error, the test should fail. if ae.GRPCStatus().Code() != codes.Unavailable && ae.HTTPCode() != 503 { t.Errorf("GetBucket: got unexpected error: %v; want 503", err) } } // Error should be wrapped so it's also equivalent to a context timeout. if !errors.Is(err, context.DeadlineExceeded) { t.Errorf("GetBucket: got unexpected error %v, want to match DeadlineExceeded.", err) } }) } // Test that errors are wrapped correctly if retry happens until max attempts. func TestRetryMaxAttemptsEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { ctx := context.Background() instructions := map[string][]string{"storage.buckets.get": {"return-503", "return-503", "return-503", "return-503", "return-503"}} testID := createRetryTest(t, project, bucket, client, instructions) ctx = callctx.SetHeaders(ctx, "x-retry-test-id", testID) config := &retryConfig{maxAttempts: expectedAttempts(3), backoff: &gax.Backoff{Initial: 10 * time.Millisecond}} _, err := client.GetBucket(ctx, bucket, nil, idempotent(true), withRetryConfig(config)) var ae *apierror.APIError if errors.As(err, &ae) { // We expect a 503/UNAVAILABLE error. For anything else including a nil // error, the test should fail. if ae.GRPCStatus().Code() != codes.Unavailable && ae.HTTPCode() != 503 { t.Errorf("GetBucket: got unexpected error %v; want 503", err) } } // Error should be wrapped so it indicates that MaxAttempts has been reached. if got, want := err.Error(), "retry failed after 3 attempts"; !strings.Contains(got, want) { t.Errorf("got error: %q, want to contain: %q", got, want) } }) } // createRetryTest creates a bucket in the emulator and sets up a test using the // Retry Test API for the given instructions. This is intended for emulator tests // of retry behavior that are not covered by conformance tests. func createRetryTest(t *testing.T, project, bucket string, client storageClient, instructions map[string][]string) string { t.Helper() ctx := context.Background() _, err := client.CreateBucket(ctx, project, bucket, &BucketAttrs{}, nil) if err != nil { t.Fatalf("creating bucket: %v", err) } // Need the HTTP hostname to set up a retry test, as well as knowledge of // underlying transport to specify instructions. host := os.Getenv("STORAGE_EMULATOR_HOST") endpoint, err := url.Parse(host) if err != nil { t.Fatalf("parsing endpoint: %v", err) } var transport string if _, ok := client.(*httpStorageClient); ok { transport = "http" } else { transport = "grpc" } et := emulatorTest{T: t, name: t.Name(), resources: resources{}, host: endpoint} et.create(instructions, transport) t.Cleanup(func() { et.delete() }) return et.id } // createObject creates an object in the emulator and returns its name, generation, and // metageneration. func createObject(ctx context.Context, bucket string) (string, int64, int64, error) { prefix := time.Now().Nanosecond() objName := fmt.Sprintf("%d-object", prefix) w := veneerClient.Bucket(bucket).Object(objName).NewWriter(ctx) if _, err := w.Write(randomBytesToWrite); err != nil { return "", 0, 0, fmt.Errorf("failed to populate test data: %w", err) } if err := w.Close(); err != nil { return "", 0, 0, fmt.Errorf("closing object: %w", err) } attrs, err := veneerClient.Bucket(bucket).Object(objName).Attrs(ctx) if err != nil { return "", 0, 0, fmt.Errorf("get object: %w", err) } return objName, attrs.Generation, attrs.Metageneration, nil } // createBucket creates a new bucket in the emulator and returns its name and // metageneration. func createBucket(ctx context.Context, projectID string) (string, int64, error) { prefix := time.Now().Nanosecond() bucket := fmt.Sprintf("%d-bucket", prefix) if err := veneerClient.Bucket(bucket).Create(ctx, projectID, nil); err != nil { return "", 0, fmt.Errorf("Bucket.Create: %w", err) } attrs, err := veneerClient.Bucket(bucket).Attrs(ctx) if err != nil { return "", 0, fmt.Errorf("Bucket.Attrs: %w", err) } return bucket, attrs.MetaGeneration, nil } // transportClienttest executes the given function with a sub-test, a project name // based on the transport, a unique bucket name also based on the transport, and // the transport-specific client to run the test with. It also checks the environment // to ensure it is suitable for emulator-based tests, or skips. func transportClientTest(t *testing.T, test func(*testing.T, string, string, storageClient)) { checkEmulatorEnvironment(t) for transport, client := range emulatorClients { t.Run(transport, func(t *testing.T) { project := fmt.Sprintf("%s-project", transport) bucket := fmt.Sprintf("%s-bucket-%d", transport, time.Now().Nanosecond()) test(t, project, bucket, client) }) } } // checkEmulatorEnvironment skips the test if the emulator environment variables // are not set. func checkEmulatorEnvironment(t *testing.T) { if !isEmulatorEnvironmentSet() { t.Skip("Emulator tests skipped without emulator environment variables set") } } // isEmulatorEnvironmentSet checks if the emulator environment variables are set. func isEmulatorEnvironmentSet() bool { return os.Getenv("STORAGE_EMULATOR_HOST_GRPC") != "" && os.Getenv("STORAGE_EMULATOR_HOST") != "" }