blob: 0a98f9a624c8ba77f9efbef4cf0d7079273e97f4 [file] [log] [blame]
// 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") != ""
}