Repository: googleapis/java-storage
Pull request #1510 (2022-07-14 23:00:22)

test: update testbench to support grpc endpoint
Support grpc api testing with testbench

blocked until https://github.com/googleapis/storage-testbench/pull/376 is released.


Patch:
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
index 5abd8d2c..32ecbad3 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
@@ -23,6 +23,7 @@ import static com.google.cloud.storage.Utils.toImmutableListOf;
 import static com.google.cloud.storage.Utils.todo;
 
 import com.google.cloud.storage.BlobInfo.CustomerEncryption;
+import com.google.cloud.storage.BucketInfo.LifecycleRule;
 import com.google.cloud.storage.Conversions.Codec;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
@@ -46,6 +47,7 @@ import java.time.LocalDate;
 import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
 import java.util.List;
+import java.util.Map;
 
 final class GrpcConversions {
   static final GrpcConversions INSTANCE = new GrpcConversions();
@@ -178,7 +180,13 @@ final class GrpcConversions {
   }
 
   private BucketInfo bucketInfoDecode(Bucket from) {
-    BucketInfo.Builder to = BucketInfo.newBuilder(BucketName.parse(from.getName()).getBucket());
+    BucketName bucketName = BucketName.parse(from.getName());
+    // `name` above is a read-only value from gcs, which enforces non-emptiness.
+    //   BucketName.parse will return null of name is empty. Since name can't be empty
+    //   we don't need to explicitly check for null, as the only other failure condition
+    //   would be a parse exception.
+    //noinspection ConstantConditions
+    BucketInfo.Builder to = BucketInfo.newBuilder(bucketName.getBucket());
     to.setProject(from.getProject());
     to.setGeneratedId(from.getBucketId());
     if (from.hasRetentionPolicy()) {
@@ -193,33 +201,58 @@ final class GrpcConversions {
           timestampCodec::decode,
           to::setRetentionEffectiveTimeOffsetDateTime);
     }
-    ifNonNull(from.getBucketId(), to::setGeneratedId);
     ifNonNull(from.getLocation(), to::setLocation);
     ifNonNull(from.getLocationType(), to::setLocationType);
     ifNonNull(from.getMetageneration(), to::setMetageneration);
-    ifNonNull(from.getBilling(), Billing::getRequesterPays, to::setRequesterPays);
-    ifNonNull(from.getCreateTime(), timestampCodec::decode, to::setCreateTimeOffsetDateTime);
-    ifNonNull(from.getUpdateTime(), timestampCodec::decode, to::setUpdateTimeOffsetDateTime);
-    ifNonNull(from.getEncryption(), Bucket.Encryption::getDefaultKmsKey, to::setDefaultKmsKeyName);
+    if (from.hasBilling()) {
+      Billing billing = from.getBilling();
+      to.setRequesterPays(billing.getRequesterPays());
+    }
+    if (from.hasCreateTime()) {
+      to.setCreateTimeOffsetDateTime(timestampCodec.decode(from.getCreateTime()));
+    }
+    if (from.hasUpdateTime()) {
+      to.setUpdateTimeOffsetDateTime(timestampCodec.decode(from.getUpdateTime()));
+    }
+    if (from.hasEncryption()) {
+      to.setDefaultKmsKeyName(from.getEncryption().getDefaultKmsKey());
+    }
     ifNonNull(from.getRpo(), Rpo::valueOf, to::setRpo);
     ifNonNull(from.getStorageClass(), StorageClass::valueOf, to::setStorageClass);
-    ifNonNull(from.getVersioning(), Bucket.Versioning::getEnabled, to::setVersioningEnabled);
+    if (from.hasVersioning()) {
+      to.setVersioningEnabled(from.getVersioning().getEnabled());
+    }
     ifNonNull(from.getDefaultEventBasedHold(), to::setDefaultEventBasedHold);
-    ifNonNull(from.getLabels(), to::setLabels);
+    Map<String, String> labelsMap = from.getLabelsMap();
+    if (!labelsMap.isEmpty()) {
+      to.setLabels(labelsMap);
+    }
     if (from.hasWebsite()) {
       to.setIndexPage(from.getWebsite().getMainPageSuffix());
       to.setNotFoundPage(from.getWebsite().getNotFoundPage());
     }
-    ifNonNull(
-        from.getLifecycle(),
-        lift(Bucket.Lifecycle::getRuleList).andThen(toImmutableListOf(lifecycleRule()::decode)),
-        to::setLifecycleRules);
-    ifNonNull(from.getCorsList(), toImmutableListOf(cors()::decode), to::setCors);
-    ifNonNull(from.getLogging(), loggingCodec::decode, to::setLogging);
-    ifNonNull(from.getOwner(), lift(Owner::getEntity).andThen(this::entityDecode), to::setOwner);
-    ifNonNull(
-        from.getDefaultObjectAclList(), toImmutableListOf(objectAcl()::decode), to::setDefaultAcl);
-    ifNonNull(from.getIamConfig(), iamConfiguration()::decode, to::setIamConfiguration);
+    if (from.hasLifecycle()) {
+      to.setLifecycleRules(
+          toImmutableListOf(lifecycleRuleCodec::decode).apply(from.getLifecycle().getRuleList()));
+    }
+    List<Bucket.Cors> corsList = from.getCorsList();
+    if (!corsList.isEmpty()) {
+      to.setCors(toImmutableListOf(corsCodec::decode).apply(corsList));
+    }
+    if (from.hasLogging()) {
+      to.setLogging(loggingCodec.decode(from.getLogging()));
+    }
+    if (from.hasOwner()) {
+      to.setOwner(entityCodec.decode(from.getOwner().getEntity()));
+    }
+
+    List<ObjectAccessControl> defaultObjectAclList = from.getDefaultObjectAclList();
+    if (!defaultObjectAclList.isEmpty()) {
+      to.setDefaultAcl(toImmutableListOf(objectAclCodec::decode).apply(defaultObjectAclList));
+    }
+    if (from.hasIamConfig()) {
+      to.setIamConfiguration(iamConfigurationCodec.decode(from.getIamConfig()));
+    }
     // TODO(frankyn): Add SelfLink when the field is available
     // TODO(frankyn): Add Etag when support is available
     return to.build();
@@ -229,7 +262,8 @@ final class GrpcConversions {
     Bucket.Builder to = Bucket.newBuilder();
     to.setProject(from.getProject());
     to.setName(BucketName.format(from.getProject(), from.getName()));
-    to.setBucketId(from.getGeneratedId());
+    // TODO: We need to clean up bucketId handling
+    ifNonNull(from.getGeneratedId(), to::setBucketId);
     if (from.getRetentionPeriodDuration() != null) {
       Bucket.RetentionPolicy.Builder retentionPolicyBuilder = to.getRetentionPolicyBuilder();
       ifNonNull(
@@ -248,53 +282,62 @@ final class GrpcConversions {
     ifNonNull(from.getLocation(), to::setLocation);
     ifNonNull(from.getLocationType(), to::setLocationType);
     ifNonNull(from.getMetageneration(), to::setMetageneration);
-    Bucket.Billing.Builder billingBuilder = Billing.newBuilder();
-    ifNonNull(from.requesterPays(), billingBuilder::setRequesterPays);
-    to.setBilling(billingBuilder.build());
+    if (from.requesterPays() != null) {
+      Bucket.Billing.Builder billingBuilder = Billing.newBuilder();
+      ifNonNull(from.requesterPays(), billingBuilder::setRequesterPays);
+      to.setBilling(billingBuilder.build());
+    }
     ifNonNull(from.getCreateTimeOffsetDateTime(), timestampCodec::encode, to::setCreateTime);
     ifNonNull(from.getUpdateTimeOffsetDateTime(), timestampCodec::encode, to::setUpdateTime);
-    Bucket.Encryption.Builder encryptionBuilder = Bucket.Encryption.newBuilder();
-    ifNonNull(from.getDefaultKmsKeyName(), encryptionBuilder::setDefaultKmsKey);
-    to.setEncryption(encryptionBuilder.build());
-    Bucket.Website.Builder websiteBuilder = Bucket.Website.newBuilder();
-    ifNonNull(from.getIndexPage(), websiteBuilder::setMainPageSuffix);
-    ifNonNull(from.getNotFoundPage(), websiteBuilder::setNotFoundPage);
-    to.setWebsite(websiteBuilder.build());
+    if (from.getDefaultKmsKeyName() != null) {
+      Bucket.Encryption.Builder encryptionBuilder = Bucket.Encryption.newBuilder();
+      ifNonNull(from.getDefaultKmsKeyName(), encryptionBuilder::setDefaultKmsKey);
+      to.setEncryption(encryptionBuilder.build());
+    }
+    if (from.getIndexPage() != null || from.getNotFoundPage() != null) {
+      Bucket.Website.Builder websiteBuilder = Bucket.Website.newBuilder();
+      ifNonNull(from.getIndexPage(), websiteBuilder::setMainPageSuffix);
+      ifNonNull(from.getNotFoundPage(), websiteBuilder::setNotFoundPage);
+      to.setWebsite(websiteBuilder.build());
+    }
     ifNonNull(from.getRpo(), Rpo::toString, to::setRpo);
     ifNonNull(from.getStorageClass(), StorageClass::toString, to::setStorageClass);
-    Bucket.Versioning.Builder versioningBuilder = Bucket.Versioning.newBuilder();
-    ifNonNull(from.versioningEnabled(), versioningBuilder::setEnabled);
-    to.setVersioning(versioningBuilder.build());
+    if (from.versioningEnabled() != null) {
+      Bucket.Versioning.Builder versioningBuilder = Bucket.Versioning.newBuilder();
+      ifNonNull(from.versioningEnabled(), versioningBuilder::setEnabled);
+      to.setVersioning(versioningBuilder.build());
+    }
     ifNonNull(from.getDefaultEventBasedHold(), to::setDefaultEventBasedHold);
     ifNonNull(from.getLabels(), to::putAllLabels);
-    to.setLifecycle(buildLifecyclePolicy(from));
+    // Do not use, #getLifecycleRules, it can not return null, which is important to our logic here
+    List<? extends LifecycleRule> lifecycleRules = from.lifecycleRules;
+    if (lifecycleRules != null) {
+      Bucket.Lifecycle.Builder lifecycleBuilder = Bucket.Lifecycle.newBuilder();
+      if (!lifecycleRules.isEmpty()) {
+        ImmutableSet<Bucket.Lifecycle.Rule> set =
+            from.getLifecycleRules().stream()
+                .map(lifecycleRuleCodec::encode)
+                .collect(ImmutableSet.toImmutableSet());
+        lifecycleBuilder.addAllRule(ImmutableList.copyOf(set));
+      }
+      to.setLifecycle(lifecycleBuilder.build());
+    }
     ifNonNull(from.getLogging(), loggingCodec::encode, to::setLogging);
-    ifNonNull(from.getCors(), toImmutableListOf(cors()::encode), to::addAllCors);
+    ifNonNull(from.getCors(), toImmutableListOf(corsCodec::encode), to::addAllCors);
     ifNonNull(
         from.getOwner(),
         lift(entity()::encode).andThen(o -> Owner.newBuilder().setEntity(o).build()),
         to::setOwner);
     ifNonNull(
-        from.getDefaultAcl(), toImmutableListOf(objectAcl()::encode), to::addAllDefaultObjectAcl);
-    ifNonNull(from.getIamConfiguration(), iamConfiguration()::encode, to::setIamConfig);
+        from.getDefaultAcl(),
+        toImmutableListOf(objectAclCodec::encode),
+        to::addAllDefaultObjectAcl);
+    ifNonNull(from.getIamConfiguration(), iamConfigurationCodec::encode, to::setIamConfig);
     // TODO(frankyn): Add SelfLink when the field is available
     // TODO(frankyn): Add Etag when support is avialable
     return to.build();
   }
 
-  private Bucket.Lifecycle buildLifecyclePolicy(BucketInfo from) {
-    // Handle duplicate rules introduced by deleteRules using a backing ImmutableSet
-    Bucket.Lifecycle.Builder lifecycleBuilder = Bucket.Lifecycle.newBuilder();
-    ImmutableSet.Builder<Bucket.Lifecycle.Rule> rules = new ImmutableSet.Builder<>();
-    if (from.getLifecycleRules() != null) {
-      rules.addAll(
-          from.getLifecycleRules().stream()
-              .map(lifecycleRule()::encode)
-              .collect(ImmutableSet.toImmutableSet()));
-    }
-    return lifecycleBuilder.addAllRule(rules.build()).build();
-  }
-
   private Bucket.Logging loggingEncode(BucketInfo.Logging from) {
     Bucket.Logging.Builder to = Bucket.Logging.newBuilder();
     if (from.getLogObjectPrefix() != null) {
@@ -494,8 +537,8 @@ final class GrpcConversions {
     return rule.hasAction()
         && rule.getAction().getType().equals(BucketInfo.LifecycleRule.DeleteLifecycleAction.TYPE)
         && rule.getCondition().getAllFields().keySet().size() == 1
-        && !rule.getCondition().getAllFields().keySet().stream()
-            .noneMatch(SUPPORTED_CONDITIONS_DELETE_RULE::contains);
+        && rule.getCondition().getAllFields().keySet().stream()
+            .anyMatch(SUPPORTED_CONDITIONS_DELETE_RULE::contains);
   }
 
   @SuppressWarnings("deprecation")
@@ -745,7 +788,7 @@ final class GrpcConversions {
     // TODO(sydmunro): Add Selflink when available
     // TODO(sydmunro): Add etag when available
     // TODO(sydmunro): Add Owner
-    // TODO(sydmunro): Add user metadata
+    ifNonNull(from.getMetadata(), toBuilder::putAllMetadata);
     // TODO(sydmunro): Object ACL
     return toBuilder.build();
   }
@@ -760,36 +803,50 @@ final class GrpcConversions {
     ifNonNull(from.getContentDisposition(), toBuilder::setContentDisposition);
     ifNonNull(from.getContentLanguage(), toBuilder::setContentLanguage);
     ifNonNull(from.getComponentCount(), toBuilder::setComponentCount);
-    if (from.getChecksums() != null) {
-      if (from.getChecksums().hasCrc32C()) {
-        toBuilder.setCrc32c(crc32cCodec.encode(from.getChecksums().getCrc32C()));
+    if (from.hasChecksums()) {
+      ObjectChecksums checksums = from.getChecksums();
+      if (checksums.hasCrc32C()) {
+        toBuilder.setCrc32c(crc32cCodec.encode(checksums.getCrc32C()));
       }
-      if (from.getChecksums().getMd5Hash() != null) {
-        toBuilder.setMd5(
-            BaseEncoding.base64().encode(from.getChecksums().getMd5Hash().toByteArray()));
+      if (!checksums.getMd5Hash().equals(ByteString.empty())) {
+        toBuilder.setMd5(BaseEncoding.base64().encode(checksums.getMd5Hash().toByteArray()));
       }
     }
     ifNonNull(from.getMetageneration(), toBuilder::setMetageneration);
-    ifNonNull(from.getDeleteTime(), timestampCodec::decode, toBuilder::setDeleteTimeOffsetDateTime);
-    ifNonNull(from.getUpdateTime(), timestampCodec::decode, toBuilder::setUpdateTimeOffsetDateTime);
-    ifNonNull(from.getCreateTime(), timestampCodec::decode, toBuilder::setCreateTimeOffsetDateTime);
-    ifNonNull(from.getCustomTime(), timestampCodec::decode, toBuilder::setCustomTimeOffsetDateTime);
-    ifNonNull(
-        from.getCustomerEncryption(),
-        customerEncryptionCodec::decode,
-        toBuilder::setCustomerEncryption);
+    if (from.hasDeleteTime()) {
+      toBuilder.setDeleteTimeOffsetDateTime(timestampCodec.decode(from.getDeleteTime()));
+    }
+    if (from.hasUpdateTime()) {
+      toBuilder.setUpdateTimeOffsetDateTime(timestampCodec.decode(from.getUpdateTime()));
+    }
+    if (from.hasCreateTime()) {
+      toBuilder.setCreateTimeOffsetDateTime(timestampCodec.decode(from.getCreateTime()));
+    }
+    if (from.hasCustomTime()) {
+      toBuilder.setCustomTimeOffsetDateTime(timestampCodec.decode(from.getCustomTime()));
+    }
+    if (from.hasCustomerEncryption()) {
+      toBuilder.setCustomerEncryption(customerEncryptionCodec.decode(from.getCustomerEncryption()));
+    }
     ifNonNull(from.getStorageClass(), StorageClass::valueOf, toBuilder::setStorageClass);
-    ifNonNull(
-        from.getUpdateStorageClassTime(),
-        timestampCodec::decode,
-        toBuilder::setTimeStorageClassUpdatedOffsetDateTime);
-    ifNonNull(from.getKmsKey(), toBuilder::setKmsKeyName);
-    ifNonNull(from.getEventBasedHold(), toBuilder::setEventBasedHold);
+    if (from.hasUpdateStorageClassTime()) {
+      toBuilder.setTimeStorageClassUpdatedOffsetDateTime(
+          timestampCodec.decode(from.getUpdateStorageClassTime()));
+    }
+    if (!from.getKmsKey().isEmpty()) {
+      toBuilder.setKmsKeyName(from.getKmsKey());
+    }
+    if (from.hasEventBasedHold()) {
+      toBuilder.setEventBasedHold(from.getEventBasedHold());
+    }
     ifNonNull(from.getTemporaryHold(), toBuilder::setTemporaryHold);
-    ifNonNull(
-        from.getRetentionExpireTime(),
-        timestampCodec::decode,
-        toBuilder::setRetentionExpirationTimeOffsetDateTime);
+    if (from.hasRetentionExpireTime()) {
+      toBuilder.setRetentionExpirationTimeOffsetDateTime(
+          timestampCodec.decode(from.getRetentionExpireTime()));
+    }
+    if (!from.getMetadataMap().isEmpty()) {
+      toBuilder.setMetadata(from.getMetadataMap());
+    }
     return toBuilder.build();
   }
 
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
index 89119e45..f5f3fd90 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
@@ -66,6 +66,7 @@ import com.google.storage.v2.ListBucketsRequest;
 import com.google.storage.v2.ListHmacKeysRequest;
 import com.google.storage.v2.ListObjectsRequest;
 import com.google.storage.v2.Object;
+import com.google.storage.v2.ProjectName;
 import com.google.storage.v2.ReadObjectRequest;
 import com.google.storage.v2.StorageClient.ListBucketsPage;
 import com.google.storage.v2.StorageClient.ListBucketsPagedResponse;
@@ -140,8 +141,10 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   public Bucket create(BucketInfo bucketInfo, BucketTargetOption... options) {
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    CreateBucketRequest.Builder builder =
-        CreateBucketRequest.newBuilder().setBucket(codecs.bucketInfo().encode(bucketInfo));
+    com.google.storage.v2.Bucket bucket = codecs.bucketInfo().encode(bucketInfo);
+    CreateBucketRequest.Builder builder = CreateBucketRequest.newBuilder().setBucket(bucket);
+    builder.setBucketId(bucketInfo.getName());
+    builder.setParent(ProjectName.format(getOptions().getProjectId()));
     ZOpt.applyAll(
         optionsMap,
         ZOpt.PREDEFINED_ACL.consumeVia(builder::setPredefinedAcl),
@@ -377,7 +380,8 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
     String projectId =
         firstNonNull(ZOpt.PROJECT_ID.get(optionsMap), this.getOptions().getProjectId());
-    ListBucketsRequest.Builder builder = ListBucketsRequest.newBuilder().setParent(projectId);
+    ListBucketsRequest.Builder builder =
+        ListBucketsRequest.newBuilder().setParent(ProjectName.format(projectId));
     ZOpt.applyAll(
         optionsMap,
         ZOpt.MAX_RESULTS.mapThenConsumeVia(Long::intValue, builder::setPageSize),


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BaseConvertablePropertyTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BaseConvertablePropertyTest.java
index 4e020a73..262ad555 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BaseConvertablePropertyTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BaseConvertablePropertyTest.java
@@ -54,10 +54,11 @@ abstract class BaseConvertablePropertyTest<ModelT, ProtoT extends Message> {
     ModelT model = codec.decode(p);
     ProtoT proto = codec.encode(model);
 
-    assertThat(p).isEqualTo(proto);
+    assertThat(proto).isEqualTo(p);
   }
 
-  private static TypeUsage findBaseTypeUsage(Class<? extends BaseConvertablePropertyTest> c) {
+  private static TypeUsage findBaseTypeUsage(
+      @SuppressWarnings("rawtypes") Class<? extends BaseConvertablePropertyTest> c) {
     TypeUsage curr = TypeUsage.of(c);
     while (curr.getRawType() != BaseConvertablePropertyTest.class) {
       Optional<TypeUsage> superclass = curr.getSuperclass();
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/JqwikTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/JqwikTest.java
index 664e23a3..d869dd78 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/JqwikTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/JqwikTest.java
@@ -16,14 +16,20 @@
 
 package com.google.cloud.storage;
 
+import static com.google.cloud.storage.StorageV2ProtoUtils.fmtProto;
 import static net.jqwik.api.providers.TypeUsage.of;
 
+import com.google.protobuf.Message;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 import net.jqwik.api.Arbitraries;
 import net.jqwik.api.Arbitrary;
+import net.jqwik.api.EdgeCases;
 import net.jqwik.api.Example;
 import net.jqwik.api.providers.TypeUsage;
+import net.jqwik.engine.support.JqwikStringSupport;
 
 public class JqwikTest {
   @Example
@@ -46,6 +52,21 @@ public class JqwikTest {
   }
 
   public static void report(TypeUsage t, Arbitrary<?> objectArbitrary) {
-    System.out.printf("%-25s = %s%n", t, objectArbitrary.edgeCases());
+    EdgeCases<?> cases = objectArbitrary.edgeCases();
+    // inspired from net.jqwik.engine.properties.arbitraries.EdgeCasesSupport$1#toString()
+    String formattedCases =
+        StreamSupport.stream(cases.spliterator(), false)
+            .map(
+                s -> {
+                  Object value = s.value();
+                  if (value instanceof Message) {
+                    Message m = (Message) value;
+                    return fmtProto(m);
+                  } else {
+                    return JqwikStringSupport.displayString(value);
+                  }
+                })
+            .collect(Collectors.joining(",\n  ", "[\n  ", "]"));
+    System.out.printf("%-25s = EdgeCases%s%n", t, formattedCases);
   }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/PackagePrivateMethodWorkarounds.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/PackagePrivateMethodWorkarounds.java
index 4cf73143..7e0fd087 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/PackagePrivateMethodWorkarounds.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/PackagePrivateMethodWorkarounds.java
@@ -20,7 +20,9 @@ import com.google.api.services.storage.model.StorageObject;
 import com.google.cloud.WriteChannel;
 import com.google.cloud.storage.BucketInfo.BuilderImpl;
 import java.util.Optional;
+import java.util.function.Consumer;
 import java.util.function.Function;
+import javax.annotation.Nullable;
 
 /**
  * Several classes in the High Level Model for storage include package-local constructors and
@@ -67,4 +69,12 @@ public final class PackagePrivateMethodWorkarounds {
   public static <T> T todo() {
     return Utils.todo();
   }
+
+  public static <T> void ifNonNull(@Nullable T t, Consumer<T> c) {
+    Utils.ifNonNull(t, c);
+  }
+
+  public static <T1, T2> void ifNonNull(@Nullable T1 t, Function<T1, T2> map, Consumer<T2> c) {
+    Utils.ifNonNull(t, map, c);
+  }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestBench.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestBench.java
index fe723270..4786c4ff 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestBench.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestBench.java
@@ -48,6 +48,7 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
 import java.util.logging.Logger;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
@@ -68,6 +69,7 @@ public final class TestBench implements TestRule {
 
   private final boolean ignorePullError;
   private final String baseUri;
+  private final String gRPCBaseUri;
   private final String dockerImageName;
   private final String dockerImageTag;
   private final CleanupStrategy cleanupStrategy;
@@ -79,12 +81,14 @@ public final class TestBench implements TestRule {
   private TestBench(
       boolean ignorePullError,
       String baseUri,
+      String gRPCBaseUri,
       String dockerImageName,
       String dockerImageTag,
       CleanupStrategy cleanupStrategy,
       String containerName) {
     this.ignorePullError = ignorePullError;
     this.baseUri = baseUri;
+    this.gRPCBaseUri = gRPCBaseUri;
     this.dockerImageName = dockerImageName;
     this.dockerImageTag = dockerImageTag;
     this.cleanupStrategy = cleanupStrategy;
@@ -107,6 +111,10 @@ public final class TestBench implements TestRule {
     return baseUri;
   }
 
+  public String getGRPCBaseUri() {
+    return gRPCBaseUri;
+  }
+
   public RetryTestResource createRetryTest(RetryTestResource retryTestResource) throws IOException {
     GenericUrl url = new GenericUrl(baseUri + "/retry_test");
     String jsonString = gson.toJson(retryTestResource);
@@ -149,6 +157,14 @@ public final class TestBench implements TestRule {
     return b.build();
   }
 
+  private boolean startGRPCServer(int gRPCPort) throws IOException {
+    GenericUrl url = new GenericUrl(baseUri + "/start_grpc?port=9090");
+    HttpRequest req = requestFactory.buildGetRequest(url);
+    HttpResponse resp = req.execute();
+    resp.disconnect();
+    return resp.getStatusCode() == 200;
+  }
+
   @Override
   public Statement apply(final Statement base, Description description) {
     return new Statement() {
@@ -189,20 +205,26 @@ public final class TestBench implements TestRule {
         }
 
         int port = URI.create(baseUri).getPort();
+        int gRPCPort = URI.create(gRPCBaseUri).getPort();
+        final List<String> command =
+            ImmutableList.of(
+                "docker",
+                "run",
+                "-i",
+                "--rm",
+                "--publish",
+                port + ":9000",
+                "--publish",
+                gRPCPort + ":9090",
+                String.format("--name=%s", fullContainerName),
+                dockerImage);
         final Process process =
             new ProcessBuilder()
-                .command(
-                    "docker",
-                    "run",
-                    "-i",
-                    "--rm",
-                    "--publish",
-                    port + ":9000",
-                    String.format("--name=%s", fullContainerName),
-                    dockerImage)
+                .command(command)
                 .redirectOutput(outFile)
                 .redirectError(errFile)
                 .start();
+        LOGGER.log(Level.INFO, command.toString());
         boolean success = false;
         try {
           // wait a small amount of time for the server to come up before probing
@@ -229,6 +251,12 @@ public final class TestBench implements TestRule {
             LOGGER.info(
                 "Test Server already has retry tests in it, is it running outside the tests?");
           }
+          // Start gRPC Service
+          if (!startGRPCServer(gRPCPort)) {
+            throw new IllegalStateException(
+                "Failed to start server within a reasonable amount of time. Host url(gRPC): "
+                    + gRPCBaseUri);
+          }
           base.evaluate();
           success = true;
         } catch (RetryHelperException e) {
@@ -335,6 +363,7 @@ public final class TestBench implements TestRule {
 
   public static final class Builder {
     private static final String DEFAULT_BASE_URI = "http://localhost:9000";
+    private static final String DEFAULT_GRPC_BASE_URI = "http://localhost:9005";
     private static final String DEFAULT_IMAGE_NAME =
         "gcr.io/cloud-devrel-public-resources/storage-testbench";
     private static final String DEFAULT_IMAGE_TAG = "v0.15.0";
@@ -342,6 +371,7 @@ public final class TestBench implements TestRule {
 
     private boolean ignorePullError;
     private String baseUri;
+    private String gRPCBaseUri;
     private String dockerImageName;
     private String dockerImageTag;
     private CleanupStrategy cleanupStrategy;
@@ -351,6 +381,7 @@ public final class TestBench implements TestRule {
       this(
           false,
           DEFAULT_BASE_URI,
+          DEFAULT_GRPC_BASE_URI,
           DEFAULT_IMAGE_NAME,
           DEFAULT_IMAGE_TAG,
           CleanupStrategy.ALWAYS,
@@ -360,12 +391,14 @@ public final class TestBench implements TestRule {
     private Builder(
         boolean ignorePullError,
         String baseUri,
+        String gRPCBaseUri,
         String dockerImageName,
         String dockerImageTag,
         CleanupStrategy cleanupStrategy,
         String containerName) {
       this.ignorePullError = ignorePullError;
       this.baseUri = baseUri;
+      this.gRPCBaseUri = gRPCBaseUri;
       this.dockerImageName = dockerImageName;
       this.dockerImageTag = dockerImageTag;
       this.cleanupStrategy = cleanupStrategy;
@@ -387,6 +420,11 @@ public final class TestBench implements TestRule {
       return this;
     }
 
+    public Builder setGRPCBaseUri(String gRPCBaseUri) {
+      this.gRPCBaseUri = requireNonNull(gRPCBaseUri, "gRPC host must be non null");
+      return this;
+    }
+
     public Builder setDockerImageName(String dockerImageName) {
       this.dockerImageName = requireNonNull(dockerImageName, "dockerImageName must be non null");
       return this;
@@ -406,6 +444,7 @@ public final class TestBench implements TestRule {
       return new TestBench(
           ignorePullError,
           baseUri,
+          gRPCBaseUri,
           dockerImageName,
           dockerImageTag,
           cleanupStrategy,
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITGrpcTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITGrpcTest.java
new file mode 100644
index 00000000..36484b51
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITGrpcTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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 com.google.cloud.storage.it;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.cloud.NoCredentials;
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.StorageFixture;
+import com.google.cloud.storage.StorageOptions;
+import com.google.cloud.storage.conformance.retry.TestBench;
+import com.google.cloud.storage.testing.RemoteStorageHelper;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+
+public final class ITGrpcTest {
+  @ClassRule
+  public static final TestBench TEST_BENCH =
+      TestBench.newBuilder().setContainerName("it-grpc").setDockerImageTag("v0.26.0").build();
+
+  @Rule
+  public final StorageFixture storageFixture =
+      StorageFixture.from(
+          () ->
+              StorageOptions.grpc()
+                  .setHost(TEST_BENCH.getGRPCBaseUri())
+                  .setCredentials(NoCredentials.getInstance())
+                  .setProjectId("test-project-id")
+                  .build());
+
+  @Test
+  public void testCreateBucket() {
+    final String bucketName = RemoteStorageHelper.generateBucketName();
+    Bucket bucket = storageFixture.getInstance().create(BucketInfo.of(bucketName));
+    assertThat(bucket.getName()).isEqualTo(bucketName);
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/BucketArbitraryProvider.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/BucketArbitraryProvider.java
index dcf5c942..b4196926 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/BucketArbitraryProvider.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/BucketArbitraryProvider.java
@@ -16,15 +16,21 @@
 
 package com.google.cloud.storage.jqwik;
 
+import static com.google.cloud.storage.PackagePrivateMethodWorkarounds.ifNonNull;
+
 import com.google.storage.v2.Bucket;
+import com.google.storage.v2.BucketName;
 import java.util.Collections;
 import java.util.Set;
+import javax.annotation.ParametersAreNonnullByDefault;
 import net.jqwik.api.Arbitrary;
 import net.jqwik.api.Combinators;
 import net.jqwik.api.Tuple;
 import net.jqwik.api.providers.ArbitraryProvider;
 import net.jqwik.api.providers.TypeUsage;
+import org.checkerframework.checker.nullness.qual.NonNull;
 
+@ParametersAreNonnullByDefault
 public final class BucketArbitraryProvider implements ArbitraryProvider {
 
   @Override
@@ -32,6 +38,7 @@ public final class BucketArbitraryProvider implements ArbitraryProvider {
     return targetType.isOfType(Bucket.class);
   }
 
+  @NonNull
   @Override
   public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
     Arbitrary<Bucket> as =
@@ -39,56 +46,59 @@ public final class BucketArbitraryProvider implements ArbitraryProvider {
                 Combinators.combine(
                         StorageArbitraries.buckets().name(),
                         StorageArbitraries.buckets().name(),
-                        StorageArbitraries.buckets().storageClass(),
+                        StorageArbitraries.storageClass(),
                         StorageArbitraries.buckets().location(),
                         StorageArbitraries.buckets().locationType(),
                         StorageArbitraries.metageneration(),
-                        StorageArbitraries.buckets().versioning(),
-                        StorageArbitraries.timestamp())
+                        StorageArbitraries.buckets().versioning().injectNull(0.25),
+                        StorageArbitraries.timestamp()) // ctime
                     .as(Tuple::of),
                 Combinators.combine(
-                        StorageArbitraries.timestamp(),
-                        StorageArbitraries.buckets().website(),
+                        StorageArbitraries.timestamp(), // utime
+                        StorageArbitraries.buckets().website().injectNull(0.75),
                         StorageArbitraries.bool(),
                         StorageArbitraries.buckets().rpo(),
-                        StorageArbitraries.buckets().billing(),
-                        StorageArbitraries.buckets().encryption(),
-                        StorageArbitraries.buckets().retentionPolicy(),
-                        StorageArbitraries.buckets().lifecycle())
+                        StorageArbitraries.buckets().billing().injectNull(0.01),
+                        StorageArbitraries.buckets().encryption().injectNull(0.9),
+                        StorageArbitraries.buckets().retentionPolicy().injectNull(0.5),
+                        StorageArbitraries.buckets().lifecycle().injectNull(0.5))
                     .as(Tuple::of),
                 Combinators.combine(
-                        StorageArbitraries.buckets().logging(),
+                        StorageArbitraries.buckets().logging().injectNull(0.5),
                         StorageArbitraries.buckets().cors(),
-                        StorageArbitraries.buckets().objectAccessControl(),
-                        StorageArbitraries.buckets().owner(),
-                        StorageArbitraries.buckets().iamConfig())
+                        StorageArbitraries.buckets().objectAccessControl().injectNull(0.5),
+                        StorageArbitraries.owner().injectNull(0.01),
+                        StorageArbitraries.buckets().iamConfig().injectNull(0.5),
+                        StorageArbitraries.buckets().labels())
                     .as(Tuple::of))
             .as(
-                (t1, t2, t3) ->
-                    Bucket.newBuilder()
-                        .setBucketId(t1.get1().getBucket())
-                        .setName(t1.get2().toString())
-                        .setProject(t1.get2().getProject())
-                        .setStorageClass(t1.get3())
-                        .setLocation(t1.get4())
-                        .setLocationType(t1.get5())
-                        .setMetageneration(t1.get6())
-                        .setVersioning(t1.get7())
-                        .setCreateTime(t1.get8())
-                        .setUpdateTime(t2.get1())
-                        .setWebsite(t2.get2())
-                        .setDefaultEventBasedHold(t2.get3())
-                        .setRpo(t2.get4())
-                        .setBilling(t2.get5())
-                        .setEncryption(t2.get6())
-                        .setRetentionPolicy(t2.get7())
-                        .setLifecycle(t2.get8())
-                        .setLogging(t3.get1())
-                        .addAllCors(t3.get2())
-                        .addAllDefaultObjectAcl(t3.get3())
-                        .setOwner(t3.get4())
-                        .setIamConfig(t3.get5())
-                        .build());
+                (t1, t2, t3) -> {
+                  Bucket.Builder b = Bucket.newBuilder();
+                  ifNonNull(t1.get1(), BucketName::getBucket, b::setBucketId);
+                  ifNonNull(t1.get2(), BucketName::toString, b::setName);
+                  ifNonNull(t1.get2(), BucketName::getProject, b::setProject);
+                  ifNonNull(t1.get3(), b::setStorageClass);
+                  ifNonNull(t1.get4(), b::setLocation);
+                  ifNonNull(t1.get5(), b::setLocationType);
+                  ifNonNull(t1.get6(), b::setMetageneration);
+                  ifNonNull(t1.get7(), b::setVersioning);
+                  ifNonNull(t1.get8(), b::setCreateTime);
+                  ifNonNull(t2.get1(), b::setUpdateTime);
+                  ifNonNull(t2.get2(), b::setWebsite);
+                  ifNonNull(t2.get3(), b::setDefaultEventBasedHold);
+                  ifNonNull(t2.get4(), b::setRpo);
+                  ifNonNull(t2.get5(), b::setBilling);
+                  ifNonNull(t2.get6(), b::setEncryption);
+                  ifNonNull(t2.get7(), b::setRetentionPolicy);
+                  ifNonNull(t2.get8(), b::setLifecycle);
+                  ifNonNull(t3.get1(), b::setLogging);
+                  ifNonNull(t3.get2(), b::addAllCors);
+                  ifNonNull(t3.get3(), b::addAllDefaultObjectAcl);
+                  ifNonNull(t3.get4(), b::setOwner);
+                  ifNonNull(t3.get5(), b::setIamConfig);
+                  ifNonNull(t3.get6(), b::putAllLabels);
+                  return b.build();
+                });
     return Collections.singleton(as);
   }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/HmacKeyMetadataArbitraryProvider.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/HmacKeyMetadataArbitraryProvider.java
index cc1d84f1..fd305e6d 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/HmacKeyMetadataArbitraryProvider.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/HmacKeyMetadataArbitraryProvider.java
@@ -19,19 +19,23 @@ import com.google.cloud.storage.HmacKey;
 import com.google.storage.v2.HmacKeyMetadata;
 import java.util.Collections;
 import java.util.Set;
+import javax.annotation.ParametersAreNonnullByDefault;
 import net.jqwik.api.Arbitraries;
 import net.jqwik.api.Arbitrary;
 import net.jqwik.api.Combinators;
 import net.jqwik.api.providers.ArbitraryProvider;
 import net.jqwik.api.providers.TypeUsage;
 import net.jqwik.web.api.Web;
+import org.checkerframework.checker.nullness.qual.NonNull;
 
+@ParametersAreNonnullByDefault
 public class HmacKeyMetadataArbitraryProvider implements ArbitraryProvider {
   @Override
   public boolean canProvideFor(TypeUsage targetType) {
     return targetType.isOfType(HmacKeyMetadata.class);
   }
 
+  @NonNull
   @Override
   public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
 
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ObjectArbitraryProvider.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ObjectArbitraryProvider.java
index e62e97ae..c8c4dca6 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ObjectArbitraryProvider.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ObjectArbitraryProvider.java
@@ -16,16 +16,22 @@
 
 package com.google.cloud.storage.jqwik;
 
+import static com.google.cloud.storage.PackagePrivateMethodWorkarounds.ifNonNull;
+
+import com.google.storage.v2.BucketName;
 import com.google.storage.v2.Object;
 import java.util.Collections;
 import java.util.Set;
+import javax.annotation.ParametersAreNonnullByDefault;
 import net.jqwik.api.Arbitraries;
 import net.jqwik.api.Arbitrary;
 import net.jqwik.api.Combinators;
 import net.jqwik.api.Tuple;
 import net.jqwik.api.providers.ArbitraryProvider;
 import net.jqwik.api.providers.TypeUsage;
+import org.checkerframework.checker.nullness.qual.NonNull;
 
+@ParametersAreNonnullByDefault
 public final class ObjectArbitraryProvider implements ArbitraryProvider {
 
   @Override
@@ -33,9 +39,10 @@ public final class ObjectArbitraryProvider implements ArbitraryProvider {
     return targetType.isOfType(Object.class);
   }
 
+  @NonNull
   @Override
   public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
-    Arbitrary<Integer> size = Arbitraries.integers().greaterOrEqual(0);
+    Arbitrary<Long> size = Arbitraries.longs().greaterOrEqual(0);
     Arbitrary<Object> objectArbitrary =
         Combinators.combine(
                 Combinators.combine(
@@ -43,60 +50,67 @@ public final class ObjectArbitraryProvider implements ArbitraryProvider {
                         StorageArbitraries.buckets().name(),
                         StorageArbitraries.generation(),
                         StorageArbitraries.metageneration(),
-                        StorageArbitraries.objects().storageClass(),
+                        StorageArbitraries.storageClass(),
                         size,
-                        StorageArbitraries.randomString(),
-                        StorageArbitraries.randomString())
+                        StorageArbitraries.httpHeaders().contentEncoding(),
+                        StorageArbitraries.httpHeaders().contentDisposition())
                     .as(Tuple::of),
                 Combinators.combine(
-                        StorageArbitraries.randomString(),
-                        StorageArbitraries.randomString(),
-                        StorageArbitraries.timestamp(),
-                        StorageArbitraries.randomString(),
-                        StorageArbitraries.timestamp(),
-                        Arbitraries.integers().greaterOrEqual(0),
+                        StorageArbitraries.httpHeaders().cacheControl(),
+                        StorageArbitraries.httpHeaders().contentLanguage(),
+                        StorageArbitraries.timestamp(), // dtime
+                        StorageArbitraries.httpHeaders().contentType(),
+                        StorageArbitraries.timestamp(), // ctime
+                        // componentCount is populated if the object is made from compose
+                        Arbitraries.integers().greaterOrEqual(0).injectNull(0.85),
                         StorageArbitraries.objects().objectChecksumsArbitrary())
                     .as(Tuple::of),
                 Combinators.combine(
-                        StorageArbitraries.timestamp(),
-                        StorageArbitraries.randomString(),
-                        StorageArbitraries.timestamp(),
+                        StorageArbitraries.timestamp(), // utime
+                        StorageArbitraries.kmsKey(),
+                        StorageArbitraries.timestamp(), // UpdateStorageClassTime
                         StorageArbitraries.bool(),
-                        StorageArbitraries.timestamp(),
+                        StorageArbitraries.timestamp(), // RetentionExpireTime
                         StorageArbitraries.bool(),
-                        StorageArbitraries.objects().customerEncryptionArbitrary(),
-                        StorageArbitraries.timestamp())
+                        StorageArbitraries.objects().customerEncryption().injectNull(0.90),
+                        StorageArbitraries.httpHeaders().customTime())
+                    .as(Tuple::of),
+                Combinators.combine(
+                        StorageArbitraries.objects().customMetadata(),
+                        StorageArbitraries.owner(),
+                        StorageArbitraries.objects().objectAccessControl())
                     .as(Tuple::of))
             .as(
-                (t1, t2, t3) ->
-                    Object.newBuilder()
-                        .setName(t1.get1())
-                        .setBucket(t1.get2().toString())
-                        .setGeneration(t1.get3())
-                        .setMetageneration(t1.get4())
-                        .setStorageClass(t1.get5())
-                        .setSize(t1.get6())
-                        .setContentEncoding(t1.get7())
-                        .setContentDisposition(t1.get8())
-                        .setCacheControl(t2.get1())
-                        // TODO: Object ACLs
-                        .setContentLanguage(t2.get2())
-                        .setDeleteTime(t2.get3())
-                        .setContentType(t2.get4())
-                        .setCreateTime(t2.get5())
-                        .setComponentCount(t2.get6())
-                        .setChecksums(t2.get7())
-                        .setUpdateTime(t3.get1())
-                        .setKmsKey(t3.get2())
-                        .setUpdateStorageClassTime(t3.get3())
-                        .setTemporaryHold(t3.get4())
-                        .setRetentionExpireTime(t3.get5())
-                        // TODO: User Metadata
-                        .setEventBasedHold(t3.get6())
-                        // TODO: Owner
-                        .setCustomerEncryption(t3.get7())
-                        .setCustomTime(t3.get8())
-                        .build());
+                (t1, t2, t3, t4) -> {
+                  Object.Builder b = Object.newBuilder();
+                  ifNonNull(t1.get1(), b::setName);
+                  ifNonNull(t1.get2(), BucketName::toString, b::setBucket);
+                  ifNonNull(t1.get3(), b::setGeneration);
+                  ifNonNull(t1.get4(), b::setMetageneration);
+                  ifNonNull(t1.get5(), b::setStorageClass);
+                  ifNonNull(t1.get6(), b::setSize);
+                  ifNonNull(t1.get7(), b::setContentEncoding);
+                  ifNonNull(t1.get8(), b::setContentDisposition);
+                  ifNonNull(t2.get1(), b::setCacheControl);
+                  ifNonNull(t4.get3(), b::addAllAcl);
+                  ifNonNull(t2.get2(), b::setContentLanguage);
+                  ifNonNull(t2.get3(), b::setDeleteTime);
+                  ifNonNull(t2.get4(), b::setContentType);
+                  ifNonNull(t2.get5(), b::setCreateTime);
+                  ifNonNull(t2.get6(), b::setComponentCount);
+                  ifNonNull(t2.get7(), b::setChecksums);
+                  ifNonNull(t3.get1(), b::setUpdateTime);
+                  ifNonNull(t3.get2(), b::setKmsKey);
+                  ifNonNull(t3.get3(), b::setUpdateStorageClassTime);
+                  ifNonNull(t3.get4(), b::setTemporaryHold);
+                  ifNonNull(t3.get5(), b::setRetentionExpireTime);
+                  ifNonNull(t4.get1(), b::putAllMetadata);
+                  ifNonNull(t3.get6(), b::setEventBasedHold);
+                  // ifNonNull(t4.get2(), b::setOwner); // TODO
+                  ifNonNull(t3.get7(), b::setCustomerEncryption);
+                  ifNonNull(t3.get8(), b::setCustomTime);
+                  return b.build();
+                });
     return Collections.singleton(objectArbitrary);
   }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ServiceAccountArbitraryProvider.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ServiceAccountArbitraryProvider.java
index 0a223ea4..83463140 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ServiceAccountArbitraryProvider.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ServiceAccountArbitraryProvider.java
@@ -19,12 +19,15 @@ package com.google.cloud.storage.jqwik;
 import com.google.storage.v2.ServiceAccount;
 import java.util.Collections;
 import java.util.Set;
+import javax.annotation.ParametersAreNonnullByDefault;
 import net.jqwik.api.Arbitrary;
 import net.jqwik.api.providers.ArbitraryProvider;
 import net.jqwik.api.providers.TypeUsage;
 import net.jqwik.web.api.EmailArbitrary;
 import net.jqwik.web.api.Web;
+import org.checkerframework.checker.nullness.qual.NonNull;
 
+@ParametersAreNonnullByDefault
 public final class ServiceAccountArbitraryProvider implements ArbitraryProvider {
 
   @Override
@@ -32,6 +35,7 @@ public final class ServiceAccountArbitraryProvider implements ArbitraryProvider
     return targetType.isOfType(ServiceAccount.class);
   }
 
+  @NonNull
   @Override
   public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
     EmailArbitrary emails = Web.emails();
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/StorageArbitraries.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/StorageArbitraries.java
index 40db1f38..094fa53f 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/StorageArbitraries.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/StorageArbitraries.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.storage.jqwik;
 
+import static com.google.cloud.storage.PackagePrivateMethodWorkarounds.ifNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.common.hash.Hashing;
@@ -37,12 +38,15 @@ import com.google.type.Date;
 import java.nio.charset.StandardCharsets;
 import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
+import java.util.Map;
 import net.jqwik.api.Arbitraries;
 import net.jqwik.api.Arbitrary;
 import net.jqwik.api.Combinators;
 import net.jqwik.api.Tuple;
 import net.jqwik.api.arbitraries.CharacterArbitrary;
 import net.jqwik.api.arbitraries.ListArbitrary;
+import net.jqwik.api.arbitraries.LongArbitrary;
+import net.jqwik.api.arbitraries.StringArbitrary;
 import net.jqwik.api.providers.TypeUsage;
 import net.jqwik.time.api.DateTimes;
 
@@ -63,28 +67,27 @@ public final class StorageArbitraries {
     return DateTimes.offsetDateTimes()
         .offsetBetween(ZoneOffset.UTC, ZoneOffset.UTC)
         .map(
-            odt -> {
-              return Date.newBuilder()
-                  .setYear(odt.getYear())
-                  .setMonth(odt.getMonthValue())
-                  .setDay(odt.getDayOfMonth())
-                  .build();
-            });
+            odt ->
+                Date.newBuilder()
+                    .setYear(odt.getYear())
+                    .setMonth(odt.getMonthValue())
+                    .setDay(odt.getDayOfMonth())
+                    .build());
   }
 
   public static Arbitrary<Boolean> bool() {
     return Arbitraries.defaultFor(TypeUsage.of(Boolean.class));
   }
 
-  public static Arbitrary<Long> metageneration() {
+  public static LongArbitrary metageneration() {
     return Arbitraries.longs().greaterOrEqual(0);
   }
 
-  public static Arbitrary<Long> generation() {
+  public static LongArbitrary generation() {
     return Arbitraries.longs().greaterOrEqual(0);
   }
 
-  public static Arbitrary<String> randomString() {
+  public static StringArbitrary randomString() {
     return Arbitraries.strings().all().ofMinLength(1).ofMaxLength(1024);
   }
 
@@ -92,6 +95,10 @@ public final class StorageArbitraries {
     return Arbitraries.chars().alpha().numeric();
   }
 
+  public static StringArbitrary alphaString() {
+    return Arbitraries.strings().alpha();
+  }
+
   public static Arbitrary<ProjectID> projectID() {
     return Combinators.combine(
             // must start with a letter
@@ -108,10 +115,30 @@ public final class StorageArbitraries {
         .as((first, mid, last) -> new ProjectID(first + mid + last));
   }
 
+  public static Arbitrary<String> kmsKey() {
+    return Arbitraries.of("kms-key1", "kms-key2").injectNull(0.75);
+  }
+
   public static Buckets buckets() {
     return Buckets.INSTANCE;
   }
 
+  public static Arbitrary<String> storageClass() {
+    return Arbitraries.of(
+        "STANDARD",
+        "NEARLINE",
+        "COLDLINE",
+        "ARCHIVE",
+        "MULTI_REGIONAL",
+        "REGIONAL",
+        "DURABLE_REDUCED_AVAILABILITY");
+  }
+
+  public static Arbitrary<Owner> owner() {
+    Arbitrary<String> entity = alphaString().ofMinLength(1).ofMaxLength(1024);
+    return entity.map(e -> Owner.newBuilder().setEntity(e).build());
+  }
+
   public static final class Buckets {
     private static final Buckets INSTANCE = new Buckets();
 
@@ -214,10 +241,24 @@ public final class StorageArbitraries {
     }
 
     public Arbitrary<Website> website() {
-      Arbitrary<String> indexPage = Arbitraries.strings().all().ofMinLength(1).ofMaxLength(1024);
-      Arbitrary<String> notFoundPage = Arbitraries.strings().all().ofMinLength(1).ofMaxLength(1024);
+      // TODO: create a "URLEncodedString we can use here
+      Arbitrary<String> indexPage =
+          Arbitraries.strings().all().ofMinLength(1).ofMaxLength(25).injectNull(0.75);
+      Arbitrary<String> notFoundPage =
+          Arbitraries.strings().all().ofMinLength(1).ofMaxLength(25).injectNull(0.75);
       return Combinators.combine(indexPage, notFoundPage)
-          .as((i, n) -> Website.newBuilder().setMainPageSuffix(i).setNotFoundPage(n).build());
+          .as(
+              (i, n) -> {
+                //noinspection ConstantConditions
+                if (i == null && n == null) {
+                  return null;
+                } else {
+                  Website.Builder b = Website.newBuilder();
+                  ifNonNull(i, b::setMainPageSuffix);
+                  ifNonNull(n, b::setNotFoundPage);
+                  return b.build();
+                }
+              });
     }
 
     public Arbitrary<Bucket.Logging> logging() {
@@ -247,14 +288,13 @@ public final class StorageArbitraries {
       ListArbitrary<String> origins = Arbitraries.of("*", "google.com").list().uniqueElements();
       return Combinators.combine(methods, responseHeaders, origins, maxAgeSeconds)
           .as(
-              (m, r, o, a) -> {
-                return Bucket.Cors.newBuilder()
-                    .addAllMethod(m)
-                    .addAllResponseHeader(r)
-                    .addAllOrigin(o)
-                    .setMaxAgeSeconds(a)
-                    .build();
-              })
+              (m, r, o, a) ->
+                  Bucket.Cors.newBuilder()
+                      .addAllMethod(m)
+                      .addAllResponseHeader(r)
+                      .addAllOrigin(o)
+                      .setMaxAgeSeconds(a)
+                      .build())
           .list()
           .ofMinSize(0)
           .ofMaxSize(10);
@@ -265,16 +305,13 @@ public final class StorageArbitraries {
     }
 
     public ListArbitrary<ObjectAccessControl> objectAccessControl() {
-      Arbitrary<String> entity = Arbitraries.strings().alpha().ofMinLength(1).ofMaxLength(1024);
-      Arbitrary<String> role = Arbitraries.strings().alpha().ofMinLength(1).ofMaxLength(1024);
+      Arbitrary<String> entity = alphaString().ofMinLength(1).ofMaxLength(25);
+      Arbitrary<String> role = alphaString().ofMinLength(1).ofMaxLength(25);
       return Combinators.combine(entity, role)
           .as((e, r) -> ObjectAccessControl.newBuilder().setEntity(e).setRole(r).build())
-          .list();
-    }
-
-    public Arbitrary<Owner> owner() {
-      Arbitrary<String> entity = Arbitraries.strings().alpha().ofMinLength(1).ofMaxLength(1024);
-      return entity.map(e -> Owner.newBuilder().setEntity(e).build());
+          .list()
+          .ofMinSize(0)
+          .ofMaxSize(10);
     }
 
     public Arbitrary<Bucket.IamConfig.UniformBucketLevelAccess> uniformBucketLevelAccess() {
@@ -292,8 +329,6 @@ public final class StorageArbitraries {
     }
 
     public Arbitrary<Bucket.IamConfig> iamConfig() {
-      Arbitrary<Bucket.IamConfig.UniformBucketLevelAccess> uniformBucketLevelAccess =
-          uniformBucketLevelAccess();
       Arbitrary<String> pap = Arbitraries.of("enforced", "inherited");
       return Combinators.combine(pap, uniformBucketLevelAccess())
           .as(
@@ -333,17 +368,6 @@ public final class StorageArbitraries {
       return bool().map(b -> Versioning.newBuilder().setEnabled(b).build());
     }
 
-    public Arbitrary<String> storageClass() {
-      return Arbitraries.of(
-          "STANDARD",
-          "NEARLINE",
-          "COLDLINE",
-          "ARCHIVE",
-          "MULTI_REGIONAL",
-          "REGIONAL",
-          "DURABLE_REDUCED_AVAILABILITY");
-    }
-
     public Arbitrary<String> rpo() {
       return Arbitraries.of("DEFAULT", "ASYNC_TURBO");
     }
@@ -356,6 +380,10 @@ public final class StorageArbitraries {
     public Arbitrary<String> locationType() {
       return Arbitraries.of("region", "dual-region", "multi-region");
     }
+
+    public Arbitrary<Map<String, String>> labels() {
+      return objects().customMetadata();
+    }
   }
 
   public static final class ProjectID {
@@ -398,11 +426,6 @@ public final class StorageArbitraries {
           .filter(s -> !s.startsWith(".well-known/acme-challenge/"));
     }
 
-    public Arbitrary<String> storageClass() {
-      // TODO: return each of the real values and edge cases (including invalid values)
-      return Arbitraries.strings().all().ofMinLength(1).ofLength(1024);
-    }
-
     public Arbitrary<ObjectChecksums> objectChecksumsArbitrary() {
       return Combinators.combine(
               Arbitraries.integers().greaterOrEqual(1),
@@ -419,7 +442,7 @@ public final class StorageArbitraries {
                       .build());
     }
 
-    public Arbitrary<CustomerEncryption> customerEncryptionArbitrary() {
+    public Arbitrary<CustomerEncryption> customerEncryption() {
       return Combinators.combine(
               Arbitraries.strings().ofMinLength(1).ofMaxLength(1024),
               Arbitraries.strings()
@@ -432,6 +455,88 @@ public final class StorageArbitraries {
                       .setKeySha256Bytes(key)
                       .build());
     }
+
+    /**
+     * Custom metadata from <a target="_blank" rel="noopener noreferrer"
+     * href="https://cloud.google.com/storage/docs/metadata">https://cloud.google.com/storage/docs/metadata</a>
+     */
+    public Arbitrary<Map<String, String>> customMetadata() {
+      // TODO: are we going to need to care about non-url encoded characters?
+      //   Not for grpc itself, but possibly for compatibility tests.
+      return Arbitraries.maps(
+              alphaString().ofMinLength(1).ofMaxLength(32),
+              alphaString().ofMinLength(1).ofMaxLength(128))
+          .ofMinSize(0)
+          .ofMaxSize(15)
+          .injectNull(0.5);
+    }
+
+    public ListArbitrary<ObjectAccessControl> objectAccessControl() {
+      return Arbitraries.of(ObjectAccessControl.getDefaultInstance())
+          .list()
+          .ofMaxSize(0) /*.ofMinSize(0).ofMaxSize(10)*/;
+    }
+  }
+
+  public static HttpHeaders httpHeaders() {
+    return HttpHeaders.INSTANCE;
+  }
+
+  /**
+   * Fixed-key metadata from <a target="_blank" rel="noopener noreferrer"
+   * href="https://cloud.google.com/storage/docs/metadata">https://cloud.google.com/storage/docs/metadata</a>
+   */
+  public static final class HttpHeaders {
+    private static final HttpHeaders INSTANCE = new HttpHeaders();
+
+    public Arbitrary<String> cacheControl() {
+      return Combinators.combine(
+              Arbitraries.of("public", "private", "no-cache", "no-store"),
+              // bound to 10K to ease exhaustion processing
+              Arbitraries.integers().between(0, 10_000).injectNull(0.5),
+              Arbitraries.of("no-transform").injectNull(0.5))
+          .as(
+              (visibility, maxAge, transform) -> {
+                //noinspection ConstantConditions
+                if (maxAge == null && transform == null) {
+                  return visibility;
+                } else {
+                  //noinspection ConstantConditions
+                  if (maxAge != null) {
+                    return String.format("%s, max-age=%d", visibility, maxAge);
+                  } else if (transform != null) {
+                    return String.format("%s, %s", visibility, transform);
+                  } else {
+                    return String.format("%s, max-age=%d, %s", visibility, maxAge, transform);
+                  }
+                }
+              });
+    }
+
+    public Arbitrary<String> contentDisposition() {
+      return Arbitraries.of("inline", "attachment;filename=blob.bin").injectNull(0.75);
+    }
+
+    public Arbitrary<String> contentEncoding() {
+      return Arbitraries.of("gzip").injectNull(0.5);
+    }
+
+    public Arbitrary<String> contentLanguage() {
+      return Arbitraries.of("en", "es", "zh").injectNull(0.75);
+    }
+
+    public Arbitrary<String> contentType() {
+      return Arbitraries.of(
+              "text/plain",
+              "application/json",
+              "application/octet-stream",
+              "application/x-www-form-urlencoded")
+          .injectNull(0.33);
+    }
+
+    public Arbitrary<Timestamp> customTime() {
+      return timestamp().injectNull(0.75);
+    }
   }
 
   private static char[] enumerate(int lower, int upperInclusive) {