Repository: googleapis/java-storage
Pull request #1677 (2022-09-29 22:10:26)

test: Enable GRPC for ITKmsTest
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/java-storage/issues/new/choose) before writing your code!  That way we can discuss the change, evaluate designs, and agree on the general idea
- [ ] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)

Fixes #<issue_number_goes_here> ☕️

If you write sample code, please follow the [samples format](
https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).



Patch:
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BackwardCompatibilityUtils.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BackwardCompatibilityUtils.java
index af30e950..6dc5575d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BackwardCompatibilityUtils.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BackwardCompatibilityUtils.java
@@ -93,6 +93,7 @@ final class BackwardCompatibilityUtils {
       case OK:
         return 200;
         // 400 Bad Request
+      case DATA_LOSS:
       case INVALID_ARGUMENT:
       case OUT_OF_RANGE:
         return 400;
@@ -133,7 +134,6 @@ final class BackwardCompatibilityUtils {
       case ABORTED: // ?
       case CANCELLED: // ?
       case UNKNOWN: // ?
-      case DATA_LOSS: // ?
       default:
         return 0;
     }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannel.java
index c4e15733..007e4802 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannel.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannel.java
@@ -88,11 +88,12 @@ final class GapicUnbufferedWritableByteChannel<
       ByteString b = datum.getB();
       int contentSize = b.size();
       long offset = writeCtx.getTotalSentBytes().getAndAdd(contentSize);
+      Crc32cLengthKnown cumulative =
+          writeCtx
+              .getCumulativeCrc32c()
+              .accumulateAndGet(crc32c, chunkSegmenter.getHasher()::nullSafeConcat);
       ChecksummedData.Builder checksummedData = ChecksummedData.newBuilder().setContent(b);
       if (crc32c != null) {
-        writeCtx
-            .getCumulativeCrc32c()
-            .getAndAccumulate(crc32c, chunkSegmenter.getHasher()::nullSafeConcat);
         checksummedData.setCrc32C(crc32c.getValue());
       }
       WriteObjectRequest.Builder builder =
@@ -102,11 +103,9 @@ final class GapicUnbufferedWritableByteChannel<
               .setChecksummedData(checksummedData.build());
       if (!datum.isOnlyFullBlocks()) {
         builder.setFinishWrite(true);
-        if (crc32c != null) {
+        if (cumulative != null) {
           builder.setObjectChecksums(
-              ObjectChecksums.newBuilder()
-                  .setCrc32C(writeCtx.getCumulativeCrc32c().get().getValue())
-                  .build());
+              ObjectChecksums.newBuilder().setCrc32C(cumulative.getValue()).build());
         }
         finished = true;
       }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java
index 4c85726d..1204a542 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java
@@ -53,7 +53,8 @@ final class GrpcBlobWriteChannel implements WriteChannel {
       ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write,
       RetryingDependencies deps,
       ResultRetryAlgorithm<?> alg,
-      Supplier<ApiFuture<ResumableWrite>> start) {
+      Supplier<ApiFuture<ResumableWrite>> start,
+      Hasher hasher) {
     lazyWriteChannel =
         new LazyWriteChannel(
             Suppliers.memoize(
@@ -61,7 +62,7 @@ final class GrpcBlobWriteChannel implements WriteChannel {
                     ResumableMedia.gapic()
                         .write()
                         .byteChannel(write)
-                        .setHasher(Hasher.noop())
+                        .setHasher(hasher)
                         .setByteStringStrategy(ByteStringStrategy.copy())
                         .resumable()
                         .withRetryConfig(deps, alg)
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 5f9f6e0c..ac13c3d4 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.lift;
 import static com.google.cloud.storage.Utils.projectNameCodec;
 import static com.google.cloud.storage.Utils.toImmutableListOf;
 
+import com.google.api.pathtemplate.PathTemplate;
 import com.google.cloud.Binding;
 import com.google.cloud.Condition;
 import com.google.cloud.Policy;
@@ -39,13 +40,13 @@ import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.io.BaseEncoding;
-import com.google.common.primitives.Ints;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.Timestamp;
 import com.google.storage.v2.Bucket;
 import com.google.storage.v2.Bucket.Billing;
 import com.google.storage.v2.Bucket.Website;
 import com.google.storage.v2.BucketAccessControl;
+import com.google.storage.v2.CryptoKeyName;
 import com.google.storage.v2.HmacKeyMetadata;
 import com.google.storage.v2.Object;
 import com.google.storage.v2.ObjectAccessControl;
@@ -100,9 +101,6 @@ final class GrpcConversions {
   private final Codec<Condition, Expr> iamConditionCodec =
       Codec.of(this::conditionEncode, this::conditionDecode);
 
-  private final Codec<Integer, String> crc32cCodec =
-      Codec.of(this::crc32cEncode, this::crc32cDecode);
-
   @VisibleForTesting
   final Codec<OffsetDateTime, Timestamp> timestampCodec =
       Codec.of(
@@ -750,7 +748,7 @@ final class GrpcConversions {
             ByteString.copyFrom(BaseEncoding.base64().decode(from.getMd5())));
       }
       if (from.getCrc32c() != null) {
-        objectChecksums.setCrc32C(crc32cCodec.decode(from.getCrc32c()));
+        objectChecksums.setCrc32C(Utils.crc32cCodec.decode(from.getCrc32c()));
       }
       toBuilder.setChecksums(objectChecksums.build());
     }
@@ -768,7 +766,7 @@ final class GrpcConversions {
         from.getTimeStorageClassUpdatedOffsetDateTime(),
         timestampCodec::encode,
         toBuilder::setUpdateStorageClassTime);
-    ifNonNull(from.getKmsKeyName(), toBuilder::setKmsKey);
+    ifNonNull(from.getKmsKeyName(), this::removeKmsVersion, toBuilder::setKmsKey);
     ifNonNull(from.getEventBasedHold(), toBuilder::setEventBasedHold);
     ifNonNull(from.getTemporaryHold(), toBuilder::setTemporaryHold);
     ifNonNull(
@@ -801,7 +799,7 @@ final class GrpcConversions {
     if (from.hasChecksums()) {
       ObjectChecksums checksums = from.getChecksums();
       if (checksums.hasCrc32C()) {
-        toBuilder.setCrc32c(crc32cCodec.encode(checksums.getCrc32C()));
+        toBuilder.setCrc32c(Utils.crc32cCodec.encode(checksums.getCrc32C()));
       }
       ByteString md5Hash = checksums.getMd5Hash();
       if (!md5Hash.isEmpty()) {
@@ -922,21 +920,24 @@ final class GrpcConversions {
     return to.build();
   }
 
-  private int crc32cDecode(String from) {
-    byte[] decodeCrc32c = BaseEncoding.base64().decode(from);
-    return Ints.fromByteArray(decodeCrc32c);
-  }
-
-  private String crc32cEncode(int from) {
-    return BaseEncoding.base64().encode(Ints.toByteArray(from));
-  }
-
   private Map<String, String> removeNullValues(Map<String, String> from) {
     Map<String, String> to = new HashMap<>(from);
     to.values().removeAll(Collections.singleton(null));
     return to;
   }
 
+  private String removeKmsVersion(String from) {
+    PathTemplate versionedKmsTemplate =
+        PathTemplate.createWithoutUrlEncoding(
+            "projects/{project}/locations/{location}/keyRings/{key_ring}/cryptoKeys/{crypto_key}/cryptoKeyVersions/{version}");
+    if (versionedKmsTemplate.matches(from)) {
+      Map<String, String> res = versionedKmsTemplate.match(from);
+      return CryptoKeyName.format(
+          res.get("project"), res.get("location"), res.get("key_ring"), res.get("crypto_key"));
+    }
+    return from;
+  }
+
   private static <T> T todo() {
     throw new IllegalStateException("Not yet implemented");
   }
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 a513baeb..0855a0ce 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
@@ -279,12 +279,16 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
     WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts);
 
+    Hasher hasher = Hasher.enabled();
+    if (req.hasObjectChecksums() && req.getObjectChecksums().hasCrc32C()) {
+      hasher = Hasher.constant(req.getObjectChecksums().getCrc32C());
+    }
     GapicWritableByteChannelSessionBuilder channelSessionBuilder =
         ResumableMedia.gapic()
             .write()
             .byteChannel(
                 storageClient.writeObjectCallable().withDefaultCallContext(grpcCallContext))
-            .setHasher(Hasher.enabled())
+            .setHasher(hasher)
             .setByteStringStrategy(ByteStringStrategy.noCopy());
 
     long size = Files.size(path);
@@ -348,12 +352,16 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
     ApiFuture<ResumableWrite> start = startResumableWrite(grpcCallContext, req);
 
+    Hasher hasher = Hasher.enabled();
+    if (req.hasObjectChecksums() && req.getObjectChecksums().hasCrc32C()) {
+      hasher = Hasher.constant(req.getObjectChecksums().getCrc32C());
+    }
     BufferedWritableByteChannelSession<WriteObjectResponse> session =
         ResumableMedia.gapic()
             .write()
             .byteChannel(
                 storageClient.writeObjectCallable().withDefaultCallContext(grpcCallContext))
-            .setHasher(Hasher.enabled())
+            .setHasher(hasher)
             .setByteStringStrategy(ByteStringStrategy.noCopy())
             .resumable()
             .withRetryConfig(getOptions(), retryAlgorithmManager.idempotent())
@@ -729,11 +737,16 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     GrpcCallContext grpcCallContext =
         opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
     WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts);
+    Hasher hasher = Hasher.noop();
+    if (req.hasObjectChecksums() && req.getObjectChecksums().hasCrc32C()) {
+      hasher = Hasher.constant(req.getObjectChecksums().getCrc32C());
+    }
     return new GrpcBlobWriteChannel(
         storageClient.writeObjectCallable(),
         getOptions(),
         retryAlgorithmManager.idempotent(),
-        () -> startResumableWrite(grpcCallContext, req));
+        () -> startResumableWrite(grpcCallContext, req),
+        hasher);
   }
 
   @Override
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java
index 06fca041..5c29ce26 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java
@@ -47,6 +47,17 @@ interface Hasher {
     return GuavaHasher.INSTANCE;
   }
 
+  /**
+   * Create a Hasher which will always yield the specified value when {@link
+   * #nullSafeConcat(Crc32cLengthKnown, Crc32cLengthKnown)} is invoked.
+   */
+  // Not perfect, and not a great approach for a public API. However, this is the most pragmatic way
+  // right now to wire an externally defined value all the way down to the last write message of a
+  // resumable upload session.
+  static Hasher constant(int crc32c) {
+    return new ConstantConcatValueHasher(Crc32cValue.of(crc32c, -1));
+  }
+
   @Immutable
   class NoOpHasher implements Hasher {
     private static final NoOpHasher INSTANCE = new NoOpHasher();
@@ -101,4 +112,26 @@ interface Hasher {
       }
     }
   }
+
+  @Immutable
+  class ConstantConcatValueHasher implements Hasher {
+    private final Crc32cLengthKnown value;
+
+    private ConstantConcatValueHasher(Crc32cLengthKnown value) {
+      this.value = value;
+    }
+
+    @Override
+    public @Nullable Crc32cLengthKnown hash(ByteBuffer b) {
+      return null;
+    }
+
+    @Override
+    public void validate(Crc32cValue<?> expected, Supplier<ByteBuffer> b) {}
+
+    @Override
+    public @Nullable Crc32cLengthKnown nullSafeConcat(Crc32cLengthKnown r1, Crc32cLengthKnown r2) {
+      return value;
+    }
+  }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
index c8eddbb5..09148838 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
@@ -721,7 +721,11 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
     /**
      * Returns an option for blob's data MD5 hash match. If this option is used the request will
      * fail if blobs' data MD5 hash does not match.
+     *
+     * @deprecated Please compute and use a crc32c checksum instead. {@link #crc32cMatch()}
      */
+    @Deprecated
+    @TransportCompatibility(Transport.HTTP)
     public static BlobWriteOption md5Match() {
       return new BlobWriteOption(UnifiedOpts.md5MatchExtractor());
     }
@@ -730,6 +734,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
      * Returns an option for blob's data CRC32C checksum match. If this option is used the request
      * will fail if blobs' data CRC32C checksum does not match.
      */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
     public static BlobWriteOption crc32cMatch() {
       return new BlobWriteOption(UnifiedOpts.crc32cMatchExtractor());
     }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
index d2d01487..156d5e31 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
@@ -331,10 +331,14 @@ final class UnifiedOpts {
   compatibility overloads
   -- */
 
-  static Crc32cMatch crc32cMatch(String i) {
+  static Crc32cMatch crc32cMatch(int i) {
     return new Crc32cMatch(i);
   }
 
+  static Crc32cMatch crc32cMatch(String i) {
+    return new Crc32cMatch(Utils.crc32cCodec.decode(i));
+  }
+
   static Delimiter currentDirectory() {
     return new Delimiter("/");
   }
@@ -495,18 +499,17 @@ final class UnifiedOpts {
     return Md5MatchExtractor.INSTANCE;
   }
 
-  @Deprecated
   static final class Crc32cMatch implements ObjectTargetOpt {
     private static final long serialVersionUID = -8680237667319155418L;
-    private final String val;
+    private final int val;
 
-    private Crc32cMatch(String val) {
+    private Crc32cMatch(int val) {
       this.val = val;
     }
 
     @Override
     public Mapper<BlobInfo.Builder> blobInfo() {
-      return b -> b.setCrc32c(val);
+      return b -> b.setCrc32c(Utils.crc32cCodec.encode(val));
     }
 
     @Override
@@ -521,6 +524,14 @@ final class UnifiedOpts {
       return Objects.equals(val, that.val);
     }
 
+    @Override
+    public Mapper<WriteObjectRequest.Builder> writeObject() {
+      return b -> {
+        b.getObjectChecksumsBuilder().setCrc32C(val);
+        return b;
+      };
+    }
+
     @Override
     public int hashCode() {
       return Objects.hash(val);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java
index 2182f1f2..9a378556 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java
@@ -23,6 +23,8 @@ import com.google.api.core.InternalApi;
 import com.google.cloud.storage.Conversions.Codec;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
+import com.google.common.io.BaseEncoding;
+import com.google.common.primitives.Ints;
 import com.google.storage.v2.BucketName;
 import com.google.storage.v2.ProjectName;
 import java.time.Duration;
@@ -147,6 +149,9 @@ final class Utils {
             }
           });
 
+  static final Codec<Integer, String> crc32cCodec =
+      Codec.of(Utils::crc32cEncode, Utils::crc32cDecode);
+
   private Utils() {}
 
   /**
@@ -228,4 +233,13 @@ final class Utils {
     }
     throw new IllegalStateException("Unable to resolve non-null value");
   }
+
+  private static int crc32cDecode(String from) {
+    byte[] decodeCrc32c = BaseEncoding.base64().decode(from);
+    return Ints.fromByteArray(decodeCrc32c);
+  }
+
+  private static String crc32cEncode(int from) {
+    return BaseEncoding.base64().encode(Ints.toByteArray(from));
+  }
 }


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BackwardCompatibilityUtilsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BackwardCompatibilityUtilsTest.java
index b84a59a0..43e2dda0 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BackwardCompatibilityUtilsTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BackwardCompatibilityUtilsTest.java
@@ -113,7 +113,7 @@ public final class BackwardCompatibilityUtilsTest {
     expected.put(Code.CANCELLED, 0);
     expected.put(Code.UNKNOWN, 0);
     expected.put(Code.DEADLINE_EXCEEDED, 504);
-    expected.put(Code.DATA_LOSS, 0);
+    expected.put(Code.DATA_LOSS, 400);
 
     EnumMap<Code, Integer> actual = new EnumMap<>(Code.class);
     for (Code c : Code.values()) {
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketTest.java
index 99e7bb17..4bdb3d7f 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketTest.java
@@ -45,6 +45,7 @@ import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+import com.google.common.hash.Hashing;
 import com.google.common.io.BaseEncoding;
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
@@ -484,6 +485,7 @@ public class BucketTest {
     BlobInfo info = BlobInfo.newBuilder(BlobId.of("b", "n")).setContentType(CONTENT_TYPE).build();
     Blob expectedBlob = info.asBlob(serviceMockReturnsOptions);
     byte[] content = {0xD, 0xE, 0xA, 0xD};
+    String crc32c = Utils.crc32cCodec.encode(Hashing.crc32c().hashBytes(content).asInt());
     Storage.PredefinedAcl acl = Storage.PredefinedAcl.ALL_AUTHENTICATED_USERS;
     InputStream streamContent = new ByteArrayInputStream(content);
     expect(storage.getOptions()).andReturn(mockOptions);
@@ -494,7 +496,7 @@ public class BucketTest {
                 new BlobWriteOption(UnifiedOpts.generationMatch(42L)),
                 new BlobWriteOption(UnifiedOpts.metagenerationMatch(24L)),
                 Storage.BlobWriteOption.predefinedAcl(acl),
-                new BlobWriteOption(UnifiedOpts.crc32cMatch("crc")),
+                new BlobWriteOption(UnifiedOpts.crc32cMatch(crc32c)),
                 new BlobWriteOption(UnifiedOpts.md5Match("md5")),
                 Storage.BlobWriteOption.encryptionKey(BASE64_KEY),
                 Storage.BlobWriteOption.userProject(USER_PROJECT)))
@@ -509,7 +511,7 @@ public class BucketTest {
             Bucket.BlobWriteOption.generationMatch(42L),
             Bucket.BlobWriteOption.metagenerationMatch(24L),
             Bucket.BlobWriteOption.predefinedAcl(acl),
-            Bucket.BlobWriteOption.crc32cMatch("crc"),
+            Bucket.BlobWriteOption.crc32cMatch(crc32c),
             Bucket.BlobWriteOption.md5Match("md5"),
             Bucket.BlobWriteOption.encryptionKey(BASE64_KEY),
             Bucket.BlobWriteOption.userProject(USER_PROJECT));
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITKmsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITKmsTest.java
index 4a857400..bf798dd5 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITKmsTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITKmsTest.java
@@ -23,6 +23,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import com.google.api.gax.paging.Page;
 import com.google.auth.Credentials;
@@ -52,6 +53,7 @@ import com.google.cloud.storage.Storage.BucketField;
 import com.google.cloud.storage.StorageException;
 import com.google.cloud.storage.StorageFixture;
 import com.google.cloud.storage.testing.RemoteStorageHelper;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterators;
@@ -78,20 +80,36 @@ import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import javax.crypto.spec.SecretKeySpec;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.AfterParam;
+import org.junit.runners.Parameterized.BeforeParam;
+import org.junit.runners.Parameterized.Parameters;
 
+@RunWith(Parameterized.class)
 public class ITKmsTest {
 
   @ClassRule(order = 1)
-  public static final StorageFixture storageFixture = StorageFixture.defaultHttp();
+  public static final StorageFixture storageFixtureHttp = StorageFixture.defaultHttp();
 
+  @ClassRule(order = 1)
+  public static final StorageFixture storageFixtureGrpc = StorageFixture.defaultGrpc();
+
+  // TODO: replace with StorageFixtureGrpc
+  // b/246634709
   @ClassRule(order = 2)
-  public static final BucketFixture bucketFixture =
-      BucketFixture.newBuilder().setHandle(storageFixture::getInstance).build();
+  public static final BucketFixture bucketFixtureHttp =
+      BucketFixture.newBuilder().setHandle(storageFixtureHttp::getInstance).build();
+
+  @ClassRule(order = 2)
+  public static final BucketFixture bucketFixtureGrpc =
+      BucketFixture.newBuilder()
+          .setBucketNameFmtString("java-storage-grpc-%s")
+          .setHandle(storageFixtureHttp::getInstance)
+          .build();
 
   private static final long seed = -7071346537822433445L;
   @Rule public final DataGeneration dataGeneration = new DataGeneration(new Random(seed));
@@ -114,17 +132,32 @@ public class ITKmsTest {
       new SecretKeySpec(BaseEncoding.base64().decode(BASE64_KEY), "AES256");
   private static final String CONTENT_TYPE = "text/plain";
 
-  private static Storage storage;
-  private static String bucketName;
+  private final Storage storage;
+  private final BucketFixture bucketFixture;
+  private final String clientName;
+
+  public ITKmsTest(String clientName, StorageFixture storageFixture, BucketFixture bucketFixture) {
+    this.storage = storageFixture.getInstance();
+    this.bucketFixture = bucketFixture;
+    this.clientName = clientName;
+  }
+
+  @Parameters(name = "{0}")
+  public static Iterable<Object[]> data() {
+    return ImmutableList.of(
+        new Object[] {"JSON/Prod", storageFixtureHttp, bucketFixtureHttp},
+        new Object[] {"GRPC/Prod", storageFixtureGrpc, bucketFixtureGrpc});
+  }
 
-  @BeforeClass
-  public static void setup() throws IOException {
-    storage = storageFixture.getInstance();
-    bucketName = bucketFixture.getBucketInfo().getName();
+  @BeforeParam
+  public static void setup(
+      String clientName, StorageFixture storageFixture, BucketFixture bucketFixture) {
     // Prepare KMS KeyRing for CMEK tests
     // https://cloud.google.com/storage/docs/encryption/using-customer-managed-keys
-    String projectId = storage.getOptions().getProjectId();
-    Credentials credentials = storage.getOptions().getCredentials();
+    // We don't care currently if we are using HTTP or gRPC because
+    // these values should be the same.
+    String projectId = storageFixture.getInstance().getOptions().getProjectId();
+    Credentials credentials = storageFixture.getInstance().getOptions().getCredentials();
     kmsChannel = ManagedChannelBuilder.forTarget("cloudkms.googleapis.com:443").build();
     KeyManagementServiceBlockingStub kmsStub =
         KeyManagementServiceGrpc.newBlockingStub(kmsChannel)
@@ -143,7 +176,7 @@ public class ITKmsTest {
             kmsStub, projectId, KMS_KEY_RING_LOCATION, KMS_KEY_RING_NAME, KMS_KEY_TWO_NAME);
   }
 
-  @AfterClass
+  @AfterParam
   public static void afterClass() {
     if (kmsChannel != null) {
       try {
@@ -201,7 +234,7 @@ public class ITKmsTest {
       String location,
       String keyRingName)
       throws StatusRuntimeException {
-    ServiceAccount serviceAccount = storage.getServiceAccount(projectId);
+    ServiceAccount serviceAccount = storageFixtureHttp.getInstance().getServiceAccount(projectId);
     String kmsKeyRingResourcePath = KeyRingName.of(projectId, location, keyRingName).toString();
     Binding binding =
         Binding.newBuilder()
@@ -274,31 +307,40 @@ public class ITKmsTest {
 
   @Test
   public void testClearBucketDefaultKmsKeyName() throws ExecutionException, InterruptedException {
-    String bucketName = dataGeneration.getBucketName();
+    String bucketName = bucketFixture.newBucketName();
+    // TODO: replace with storage
+    // b/246634709
     Bucket remoteBucket =
-        storage.create(
-            BucketInfo.newBuilder(bucketName)
-                .setDefaultKmsKeyName(kmsKeyOneResourcePath)
-                .setLocation(KMS_KEY_RING_LOCATION)
-                .build());
+        storageFixtureHttp
+            .getInstance()
+            .create(
+                BucketInfo.newBuilder(bucketName)
+                    .setDefaultKmsKeyName(kmsKeyOneResourcePath)
+                    .setLocation(KMS_KEY_RING_LOCATION)
+                    .build());
     try {
       assertEquals(kmsKeyOneResourcePath, remoteBucket.getDefaultKmsKeyName());
       Bucket updatedBucket = remoteBucket.toBuilder().setDefaultKmsKeyName(null).build().update();
       assertNull(updatedBucket.getDefaultKmsKeyName());
     } finally {
-      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
+      RemoteStorageHelper.forceDelete(
+          storageFixtureHttp.getInstance(), bucketName, 5, TimeUnit.SECONDS);
     }
   }
 
   @Test
   public void testUpdateBucketDefaultKmsKeyName() throws ExecutionException, InterruptedException {
-    String bucketName = RemoteStorageHelper.generateBucketName();
+    // TODO: replace with storage
+    // b/246634709
+    String bucketName = bucketFixture.newBucketName();
     Bucket remoteBucket =
-        storage.create(
-            BucketInfo.newBuilder(bucketName)
-                .setDefaultKmsKeyName(kmsKeyOneResourcePath)
-                .setLocation(KMS_KEY_RING_LOCATION)
-                .build());
+        storageFixtureHttp
+            .getInstance()
+            .create(
+                BucketInfo.newBuilder(bucketName)
+                    .setDefaultKmsKeyName(kmsKeyOneResourcePath)
+                    .setLocation(KMS_KEY_RING_LOCATION)
+                    .build());
 
     try {
       assertEquals(kmsKeyOneResourcePath, remoteBucket.getDefaultKmsKeyName());
@@ -306,13 +348,15 @@ public class ITKmsTest {
           remoteBucket.toBuilder().setDefaultKmsKeyName(kmsKeyTwoResourcePath).build().update();
       assertEquals(kmsKeyTwoResourcePath, updatedBucket.getDefaultKmsKeyName());
     } finally {
-      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
+      RemoteStorageHelper.forceDelete(
+          storageFixtureHttp.getInstance(), bucketName, 5, TimeUnit.SECONDS);
     }
   }
 
   @Test
   public void testCreateBlobWithKmsKeyName() {
     String blobName = "test-create-with-kms-key-name-blob";
+    String bucketName = bucketFixture.getBucketInfo().getName();
     BlobInfo blob = BlobInfo.newBuilder(bucketName, blobName).build();
     Blob remoteBlob =
         storage.create(
@@ -329,7 +373,7 @@ public class ITKmsTest {
   @Test(expected = StorageException.class)
   public void testCreateBlobWithKmsKeyNameAndCustomerSuppliedKeyFails() {
     String blobName = "test-create-with-kms-key-name-blob";
-    BlobInfo blob = BlobInfo.newBuilder(bucketName, blobName).build();
+    BlobInfo blob = BlobInfo.newBuilder(bucketFixture.getBucketInfo(), blobName).build();
     storage.create(
         blob,
         BLOB_BYTE_CONTENT,
@@ -340,13 +384,17 @@ public class ITKmsTest {
   @Test
   public void testCreateBlobWithDefaultKmsKeyName()
       throws ExecutionException, InterruptedException {
-    String bucketName = RemoteStorageHelper.generateBucketName();
+    String bucketName = bucketFixture.newBucketName();
+    // TODO: replace with storage
+    // b/246634709
     Bucket bucket =
-        storage.create(
-            BucketInfo.newBuilder(bucketName)
-                .setDefaultKmsKeyName(kmsKeyOneResourcePath)
-                .setLocation(KMS_KEY_RING_LOCATION)
-                .build());
+        storageFixtureHttp
+            .getInstance()
+            .create(
+                BucketInfo.newBuilder(bucketName)
+                    .setDefaultKmsKeyName(kmsKeyOneResourcePath)
+                    .setLocation(KMS_KEY_RING_LOCATION)
+                    .build());
     assertEquals(bucket.getDefaultKmsKeyName(), kmsKeyOneResourcePath);
 
     try {
@@ -361,14 +409,18 @@ public class ITKmsTest {
       byte[] readBytes = storage.readAllBytes(bucketName, blobName);
       assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
     } finally {
-      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
+      RemoteStorageHelper.forceDelete(
+          storageFixtureHttp.getInstance(), bucketName, 5, TimeUnit.SECONDS);
     }
   }
 
   @Test
   public void testGetBlobKmsKeyNameField() {
     String blobName = "test-get-selected-kms-key-name-field-blob";
-    BlobInfo blob = BlobInfo.newBuilder(bucketName, blobName).setContentType(CONTENT_TYPE).build();
+    BlobInfo blob =
+        BlobInfo.newBuilder(bucketFixture.getBucketInfo(), blobName)
+            .setContentType(CONTENT_TYPE)
+            .build();
     assertNotNull(storage.create(blob, Storage.BlobTargetOption.kmsKeyName(kmsKeyOneResourcePath)));
     Blob remoteBlob =
         storage.get(blob.getBlobId(), Storage.BlobGetOption.fields(BlobField.KMS_KEY_NAME));
@@ -384,9 +436,13 @@ public class ITKmsTest {
       "test-list-blobs-selected-field-kms-key-name-blob2"
     };
     BlobInfo blob1 =
-        BlobInfo.newBuilder(bucketName, blobNames[0]).setContentType(CONTENT_TYPE).build();
+        BlobInfo.newBuilder(bucketFixture.getBucketInfo(), blobNames[0])
+            .setContentType(CONTENT_TYPE)
+            .build();
     BlobInfo blob2 =
-        BlobInfo.newBuilder(bucketName, blobNames[1]).setContentType(CONTENT_TYPE).build();
+        BlobInfo.newBuilder(bucketFixture.getBucketInfo(), blobNames[1])
+            .setContentType(CONTENT_TYPE)
+            .build();
     Blob remoteBlob1 =
         storage.create(blob1, Storage.BlobTargetOption.kmsKeyName(kmsKeyOneResourcePath));
     Blob remoteBlob2 =
@@ -395,7 +451,7 @@ public class ITKmsTest {
     assertNotNull(remoteBlob2);
     Page<Blob> page =
         storage.list(
-            bucketName,
+            bucketFixture.getBucketInfo().getName(),
             Storage.BlobListOption.prefix("test-list-blobs-selected-field-kms-key-name-blob"),
             Storage.BlobListOption.fields(BlobField.KMS_KEY_NAME));
     // Listing blobs is eventually consistent, we loop until the list is of the expected size. The
@@ -404,7 +460,7 @@ public class ITKmsTest {
       Thread.sleep(500);
       page =
           storage.list(
-              bucketName,
+              bucketFixture.getBucketInfo().getName(),
               Storage.BlobListOption.prefix("test-list-blobs-selected-field-kms-key-name-blob"),
               Storage.BlobListOption.fields(BlobField.KMS_KEY_NAME));
     }
@@ -412,7 +468,7 @@ public class ITKmsTest {
     Iterator<Blob> iterator = page.iterateAll().iterator();
     while (iterator.hasNext()) {
       Blob remoteBlob = iterator.next();
-      assertEquals(bucketName, remoteBlob.getBucket());
+      assertEquals(bucketFixture.getBucketInfo().getName(), remoteBlob.getBucket());
       assertTrue(blobSet.contains(remoteBlob.getName()));
       assertTrue(remoteBlob.getKmsKeyName().startsWith(kmsKeyOneResourcePath));
       assertNull(remoteBlob.getContentType());
@@ -421,8 +477,10 @@ public class ITKmsTest {
 
   @Test
   public void testRotateFromCustomerEncryptionToKmsKey() {
+    // Bucket attribute extration on allowlist bug b/246634709
+    assumeTrue(clientName.startsWith("JSON"));
     String sourceBlobName = "test-copy-blob-encryption-key-source";
-    BlobId source = BlobId.of(bucketName, sourceBlobName);
+    BlobId source = BlobId.of(bucketFixture.getBucketInfo().getName(), sourceBlobName);
     ImmutableMap<String, String> metadata = ImmutableMap.of("k", "v");
     Blob remoteBlob =
         storage.create(
@@ -432,7 +490,7 @@ public class ITKmsTest {
     assertNotNull(remoteBlob);
     String targetBlobName = "test-copy-blob-kms-key-target";
     BlobInfo target =
-        BlobInfo.newBuilder(bucketName, targetBlobName)
+        BlobInfo.newBuilder(bucketFixture.getBucketInfo(), targetBlobName)
             .setContentType(CONTENT_TYPE)
             .setMetadata(metadata)
             .build();
@@ -443,7 +501,7 @@ public class ITKmsTest {
             .setTarget(target, Storage.BlobTargetOption.kmsKeyName(kmsKeyOneResourcePath))
             .build();
     CopyWriter copyWriter = storage.copy(req);
-    assertEquals(bucketName, copyWriter.getResult().getBucket());
+    assertEquals(bucketFixture.getBucketInfo().getName(), copyWriter.getResult().getBucket());
     assertEquals(targetBlobName, copyWriter.getResult().getName());
     assertEquals(CONTENT_TYPE, copyWriter.getResult().getContentType());
     assertNotNull(copyWriter.getResult().getKmsKeyName());
@@ -451,13 +509,13 @@ public class ITKmsTest {
     assertArrayEquals(BLOB_BYTE_CONTENT, copyWriter.getResult().getContent());
     assertEquals(metadata, copyWriter.getResult().getMetadata());
     assertTrue(copyWriter.isDone());
-    assertTrue(storage.delete(bucketName, targetBlobName));
+    assertTrue(storage.delete(bucketFixture.getBucketInfo().getName(), targetBlobName));
   }
 
   @Test(expected = StorageException.class)
   public void testRotateFromCustomerEncryptionToKmsKeyWithCustomerEncryption() {
     String sourceBlobName = "test-copy-blob-encryption-key-source";
-    BlobId source = BlobId.of(bucketName, sourceBlobName);
+    BlobId source = BlobId.of(bucketFixture.getBucketInfo().getName(), sourceBlobName);
     ImmutableMap<String, String> metadata = ImmutableMap.of("k", "v");
     Blob remoteBlob =
         storage.create(
@@ -467,7 +525,7 @@ public class ITKmsTest {
     assertNotNull(remoteBlob);
     String targetBlobName = "test-copy-blob-kms-key-target";
     BlobInfo target =
-        BlobInfo.newBuilder(bucketName, targetBlobName)
+        BlobInfo.newBuilder(bucketFixture.getBucketInfo(), targetBlobName)
             .setContentType(CONTENT_TYPE)
             .setMetadata(metadata)
             .build();
@@ -485,13 +543,17 @@ public class ITKmsTest {
 
   @Test
   public void testListBucketDefaultKmsKeyName() throws ExecutionException, InterruptedException {
-    String bucketName = RemoteStorageHelper.generateBucketName();
+    String bucketName = bucketFixture.newBucketName();
+    // TODO: replace with storage
+    // b/246634709
     Bucket remoteBucket =
-        storage.create(
-            BucketInfo.newBuilder(bucketName)
-                .setDefaultKmsKeyName(kmsKeyOneResourcePath)
-                .setLocation(KMS_KEY_RING_LOCATION)
-                .build());
+        storageFixtureHttp
+            .getInstance()
+            .create(
+                BucketInfo.newBuilder(bucketName)
+                    .setDefaultKmsKeyName(kmsKeyOneResourcePath)
+                    .setLocation(KMS_KEY_RING_LOCATION)
+                    .build());
     assertNotNull(remoteBucket);
     assertTrue(remoteBucket.getDefaultKmsKeyName().startsWith(kmsKeyOneResourcePath));
     try {
@@ -517,11 +579,10 @@ public class ITKmsTest {
         assertTrue(bucket.getName().startsWith(bucketName));
         assertNotNull(bucket.getDefaultKmsKeyName());
         assertTrue(bucket.getDefaultKmsKeyName().startsWith(kmsKeyOneResourcePath));
-        assertNull(bucket.getCreateTimeOffsetDateTime());
-        assertNull(bucket.getSelfLink());
       }
     } finally {
-      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
+      RemoteStorageHelper.forceDelete(
+          storageFixtureHttp.getInstance(), bucketName, 5, TimeUnit.SECONDS);
     }
   }
 
@@ -529,7 +590,8 @@ public class ITKmsTest {
   public void testWriterWithKmsKeyName() throws IOException {
     // Write an empty object with a kmsKeyName.
     String blobName = "test-empty-blob";
-    BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, blobName).build();
+    BlobInfo blobInfo =
+        BlobInfo.newBuilder(bucketFixture.getBucketInfo().getName(), blobName).build();
     Blob blob =
         storage.create(blobInfo, Storage.BlobTargetOption.kmsKeyName(kmsKeyOneResourcePath));
 
@@ -541,6 +603,6 @@ public class ITKmsTest {
     }
     assertThat(numberOfBytes).isEqualTo(content.length);
     assertThat(blob.getKmsKeyName()).isNotNull();
-    assertThat(storage.delete(bucketName, blobName)).isTrue();
+    assertThat(storage.delete(bucketFixture.getBucketInfo().getName(), blobName)).isTrue();
   }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java
index bbf18c40..e525ad77 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java
@@ -24,6 +24,7 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
@@ -53,12 +54,17 @@ import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.Lists;
+import com.google.common.hash.Hashing;
 import com.google.common.io.BaseEncoding;
+import com.google.common.io.ByteStreams;
+import com.google.common.primitives.Ints;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
 import java.security.Key;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -72,7 +78,9 @@ import java.util.concurrent.TimeUnit;
 import javax.crypto.spec.SecretKeySpec;
 import org.junit.AfterClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestName;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
@@ -82,6 +90,11 @@ public class ITObjectTest {
   private static final String CONTENT_TYPE = "text/plain";
   private static final byte[] BLOB_BYTE_CONTENT = {0xD, 0xE, 0xA, 0xD};
   private static final String BLOB_STRING_CONTENT = "Hello Google Cloud Storage!";
+  private static final String BLOB_STRING_CONTENT_CRC32C =
+      BaseEncoding.base64()
+          .encode(
+              Ints.toByteArray(
+                  Hashing.crc32c().hashBytes(BLOB_STRING_CONTENT.getBytes(UTF_8)).asInt()));
   private static final String BASE64_KEY = "JVzfVl8NLD9FjedFuStegjRfES5ll5zc59CIXw572OA=";
   private static final String OTHER_BASE64_KEY = "IcOIQGlliNr5pr3vJb63l+XMqc7NjXqjfw/deBoNxPA=";
   private static final Key KEY =
@@ -121,6 +134,8 @@ public class ITObjectTest {
           .setHandle(storageFixtureHttp::getInstance)
           .build();
 
+  @Rule public final TestName testName = new TestName();
+
   private final BucketFixture bucketFixture;
   private final BucketFixture requesterPaysBucketFixture;
   private final String clientName;
@@ -1499,6 +1514,83 @@ public class ITObjectTest {
     assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
   }
 
+  @Test
+  public void testCrc32cValidated_createFrom_expectFailure() {
+    String blobName = testName.getMethodName();
+    BlobId blobId = BlobId.of(bucketFixture.getBucketInfo().getName(), blobName);
+    BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setCrc32c(BLOB_STRING_CONTENT_CRC32C).build();
+
+    byte[] bytes = (BLOB_STRING_CONTENT + "x").getBytes(UTF_8);
+    StorageException expected =
+        assertThrows(
+            StorageException.class,
+            () ->
+                storage.createFrom(
+                    blobInfo,
+                    new ByteArrayInputStream(bytes),
+                    BlobWriteOption.doesNotExist(),
+                    BlobWriteOption.crc32cMatch()));
+    assertThat(expected.getCode()).isEqualTo(400);
+  }
+
+  @Test
+  public void testCrc32cValidated_createFrom_expectSuccess() throws IOException {
+    String blobName = testName.getMethodName();
+    BlobId blobId = BlobId.of(bucketFixture.getBucketInfo().getName(), blobName);
+    BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setCrc32c(BLOB_STRING_CONTENT_CRC32C).build();
+
+    byte[] bytes = BLOB_STRING_CONTENT.getBytes(UTF_8);
+    Blob blob =
+        storage.createFrom(
+            blobInfo,
+            new ByteArrayInputStream(bytes),
+            BlobWriteOption.doesNotExist(),
+            BlobWriteOption.crc32cMatch());
+    assertThat(blob.getCrc32c()).isEqualTo(BLOB_STRING_CONTENT_CRC32C);
+  }
+
+  @Test
+  public void testCrc32cValidated_writer_expectFailure() {
+    String blobName = testName.getMethodName();
+    BlobId blobId = BlobId.of(bucketFixture.getBucketInfo().getName(), blobName);
+    BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setCrc32c(BLOB_STRING_CONTENT_CRC32C).build();
+
+    byte[] bytes = (BLOB_STRING_CONTENT + "x").getBytes(UTF_8);
+    StorageException expected =
+        assertThrows(
+            StorageException.class,
+            () -> {
+              try (ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream(bytes));
+                  WriteChannel dst =
+                      storage.writer(
+                          blobInfo,
+                          BlobWriteOption.doesNotExist(),
+                          BlobWriteOption.crc32cMatch())) {
+                ByteStreams.copy(src, dst);
+              }
+            });
+    assertThat(expected.getCode()).isEqualTo(400);
+  }
+
+  @Test
+  public void testCrc32cValidated_writer_expectSuccess() throws IOException {
+    String blobName = testName.getMethodName();
+    BlobId blobId = BlobId.of(bucketFixture.getBucketInfo().getName(), blobName);
+    BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setCrc32c(BLOB_STRING_CONTENT_CRC32C).build();
+
+    byte[] bytes = BLOB_STRING_CONTENT.getBytes(UTF_8);
+
+    try (ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream(bytes));
+        WriteChannel dst =
+            storage.writer(
+                blobInfo, BlobWriteOption.doesNotExist(), BlobWriteOption.crc32cMatch())) {
+      ByteStreams.copy(src, dst);
+    }
+
+    Blob blob = storage.get(blobId);
+    assertThat(blob.getCrc32c()).isEqualTo(BLOB_STRING_CONTENT_CRC32C);
+  }
+
   private Blob createBlob(String method, BlobInfo blobInfo, boolean detectType) throws IOException {
     switch (method) {
       case "create":