Repository: googleapis/java-storage
Pull request #1579 (2022-08-22 23:44:03)

test: ITStorageTest refactor part 2 Bucket operations
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/pom.xml b/google-cloud-storage/pom.xml
index 086db5a0..5ef4cf8a 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -104,6 +104,10 @@
       <groupId>com.google.api.grpc</groupId>
       <artifactId>proto-google-cloud-storage-v2</artifactId>
     </dependency>
+    <dependency>
+      <groupId>com.google.api.grpc</groupId>
+      <artifactId>grpc-google-cloud-storage-v2</artifactId>
+    </dependency>
     <dependency>
       <groupId>com.google.api.grpc</groupId>
       <artifactId>gapic-google-cloud-storage-v2</artifactId>
@@ -249,8 +253,9 @@
               grpc-netty-shaded is a runtime dep, we pull it up to our level to raise its transitive dependencies priority
               that overlap with our pubsub test deps.
               -->
-            <usedDependencies>io.grpc:grpc-netty-shaded,io.grpc:grpc-xds</usedDependencies>
+            <usedDependencies>io.grpc:grpc-xds</usedDependencies>
             <ignoredDependencies>
+              <dependency>io.grpc:grpc-netty-shaded</dependency>
               <!--
               We depend on `net.jqwik:jqwik` which itself has no classes, but depends on each of the other jars.
               In order to not churn on which of the sub apis we happen to be using at this time, we're flagging
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java
index b330fad9..37505948 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java
@@ -649,7 +649,7 @@ final class ApiaryConversions {
     to.setId(from.getId());
     to.setProjectId(from.getProjectId());
     ifNonNull(from.getServiceAccount(), ServiceAccount::getEmail, to::setServiceAccountEmail);
-    ifNonNull(from.getState(), Object::toString, to::setState);
+    ifNonNull(from.getState(), Enum::name, to::setState);
     ifNonNull(from.getCreateTimeOffsetDateTime(), dateTimeCodec::encode, to::setTimeCreated);
     ifNonNull(from.getUpdateTimeOffsetDateTime(), dateTimeCodec::encode, to::setUpdated);
     return to;
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ByteSizeConstants.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ByteSizeConstants.java
new file mode 100644
index 00000000..4fb27685
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ByteSizeConstants.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+final class ByteSizeConstants {
+
+  static final int _1KiB = 1024;
+  static final int _128KiB = 128 * _1KiB;
+  static final int _256KiB = 256 * _1KiB;
+  static final int _384KiB = 384 * _1KiB;
+  static final int _512KiB = 512 * _1KiB;
+  static final int _1MiB = 1024 * _1KiB;
+  static final int _2MiB = 2 * _1MiB;
+  static final int _15MiB = 15 * _1MiB;
+  static final int _16MiB = 16 * _1MiB;
+  static final int _32MiB = 32 * _1MiB;
+
+  private ByteSizeConstants() {}
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ChunkSegmenter.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ChunkSegmenter.java
index 9586eab4..b891bb3b 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ChunkSegmenter.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ChunkSegmenter.java
@@ -17,6 +17,7 @@
 package com.google.cloud.storage;
 
 import com.google.cloud.storage.Crc32cValue.Crc32cLengthKnown;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.protobuf.ByteString;
 import java.nio.ByteBuffer;
 import java.util.ArrayDeque;
@@ -28,6 +29,26 @@ import org.checkerframework.checker.nullness.qual.Nullable;
  * segments. Each resulting segment will then become an individual message.
  */
 final class ChunkSegmenter {
+  private final Hasher hasher;
+  private final ByteStringStrategy bss;
+  private final int maxSegmentSize;
+  private final int blockSize;
+
+  ChunkSegmenter(Hasher hasher, ByteStringStrategy bss, int maxSegmentSize) {
+    this(hasher, bss, maxSegmentSize, ByteSizeConstants._256KiB);
+  }
+
+  @VisibleForTesting
+  ChunkSegmenter(Hasher hasher, ByteStringStrategy bss, int maxSegmentSize, int blockSize) {
+    this.hasher = hasher;
+    this.bss = bss;
+    this.maxSegmentSize = maxSegmentSize;
+    this.blockSize = blockSize;
+  }
+
+  Hasher getHasher() {
+    return hasher;
+  }
 
   /**
    * Given {@code bbs}, yield N segments, where each segment is at most {@code maxSegmentSize}
@@ -47,20 +68,13 @@ final class ChunkSegmenter {
    *
    * Each segment will conditionally compute a crc32c value depending upon {@code hasher}.
    *
-   * @see #segmentBuffers(ByteBuffer[], Hasher, ByteStringStrategy, int, int, int)
+   * @see #segmentBuffers(ByteBuffer[], int, int)
    */
-  static ChunkSegment[] segmentBuffers(
-      ByteBuffer[] bbs, Hasher hasher, ByteStringStrategy bss, int maxSegmentSize) {
-    return segmentBuffers(bbs, hasher, bss, maxSegmentSize, 0, bbs.length);
+  ChunkSegment[] segmentBuffers(ByteBuffer[] bbs) {
+    return segmentBuffers(bbs, 0, bbs.length);
   }
 
-  static ChunkSegment[] segmentBuffers(
-      ByteBuffer[] bbs,
-      Hasher hasher,
-      ByteStringStrategy bss,
-      int maxSegmentSize,
-      int offset,
-      int length) {
+  ChunkSegment[] segmentBuffers(ByteBuffer[] bbs, int offset, int length) {
     Deque<ChunkSegment> data = new ArrayDeque<>();
 
     for (int i = offset; i < length; i++) {
@@ -71,13 +85,13 @@ final class ChunkSegmenter {
         ChunkSegment peekLast = data.peekLast();
         if (peekLast == null || peekLast.b.size() == maxSegmentSize) {
           int limit = Math.min(remaining, maxSegmentSize);
-          ChunkSegment datum = getData(buffer, hasher, bss, limit);
+          ChunkSegment datum = newSegment(buffer, limit);
           data.addLast(datum);
         } else {
           ChunkSegment chunkSoFar = data.pollLast();
           //noinspection ConstantConditions -- covered by peekLast check above
           int limit = Math.min(remaining, maxSegmentSize - chunkSoFar.b.size());
-          ChunkSegment datum = getData(buffer, hasher, bss, limit);
+          ChunkSegment datum = newSegment(buffer, limit);
           ChunkSegment plus = chunkSoFar.concat(datum);
           data.addLast(plus);
         }
@@ -87,8 +101,7 @@ final class ChunkSegmenter {
     return data.toArray(new ChunkSegment[0]);
   }
 
-  private static ChunkSegment getData(
-      ByteBuffer buffer, Hasher hasher, ByteStringStrategy bss, int limit) {
+  private ChunkSegment newSegment(ByteBuffer buffer, int limit) {
     final ByteBuffer slice = buffer.slice();
     slice.limit(limit);
 
@@ -99,12 +112,14 @@ final class ChunkSegmenter {
     return new ChunkSegment(byteString, hash);
   }
 
-  static final class ChunkSegment {
+  final class ChunkSegment {
     private final ByteString b;
     @Nullable private final Crc32cLengthKnown crc32c;
+    private final boolean onlyFullBlocks;
 
-    public ChunkSegment(ByteString b, @Nullable Crc32cLengthKnown crc32c) {
+    private ChunkSegment(ByteString b, @Nullable Crc32cLengthKnown crc32c) {
       this.b = b;
+      this.onlyFullBlocks = b.size() % blockSize == 0;
       this.crc32c = crc32c;
     }
 
@@ -113,7 +128,8 @@ final class ChunkSegmenter {
       if (crc32c != null && other.crc32c != null) {
         newCrc = crc32c.concat(other.crc32c);
       }
-      return new ChunkSegment(b.concat(other.b), newCrc);
+      ByteString concat = b.concat(other.b);
+      return new ChunkSegment(concat, newCrc);
     }
 
     public ByteString getB() {
@@ -124,5 +140,9 @@ final class ChunkSegmenter {
     public Crc32cLengthKnown getCrc32c() {
       return crc32c;
     }
+
+    public boolean isOnlyFullBlocks() {
+      return onlyFullBlocks;
+    }
   }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Crc32cValue.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Crc32cValue.java
index 4062b507..3222bc4e 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Crc32cValue.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Crc32cValue.java
@@ -49,6 +49,10 @@ abstract class Crc32cValue<Res extends Crc32cValue<Res>> {
 
   public abstract String debugString();
 
+  public boolean eqValue(Crc32cValue<?> other) {
+    return this.getValue() == other.getValue();
+  }
+
   static Crc32cLengthUnknown of(int value) {
     return new Crc32cLengthUnknown(value);
   }
@@ -57,12 +61,8 @@ abstract class Crc32cValue<Res extends Crc32cValue<Res>> {
     return new Crc32cLengthKnown(value, length);
   }
 
-  public static <Res extends Crc32cValue<Res>> Res nullSafeConcat(Res r1, Crc32cLengthKnown r2) {
-    if (r1 == null) {
-      return null;
-    } else {
-      return r1.concat(r2);
-    }
+  static String fmtCrc32cValue(int value1) {
+    return String.format("crc32c{0x%08x}", value1);
   }
 
   static final class Crc32cLengthUnknown extends Crc32cValue<Crc32cLengthUnknown> {
@@ -84,7 +84,7 @@ abstract class Crc32cValue<Res extends Crc32cValue<Res>> {
 
     @Override
     public String toString() {
-      return String.format("crc32c{0x%08x}", value);
+      return Crc32cLengthKnown.fmtCrc32cValue(value);
     }
 
     @Override
@@ -128,7 +128,7 @@ abstract class Crc32cValue<Res extends Crc32cValue<Res>> {
 
     @Override
     public String debugString() {
-      return String.format("crc32c{0x%08x}", value);
+      return fmtCrc32cValue(value);
     }
   }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBufferedReadableByteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBufferedReadableByteChannel.java
index 45359a22..4fae3718 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBufferedReadableByteChannel.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBufferedReadableByteChannel.java
@@ -133,7 +133,7 @@ final class DefaultBufferedReadableByteChannel implements BufferedReadableByteCh
 
   @Override
   public boolean isOpen() {
-    return channel.isOpen();
+    return !retEOF && channel.isOpen();
   }
 
   @Override
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicDownloadSessionBuilder.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicDownloadSessionBuilder.java
index 6e7ea548..e2a2dada 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicDownloadSessionBuilder.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicDownloadSessionBuilder.java
@@ -37,6 +37,10 @@ final class GapicDownloadSessionBuilder {
     return new GapicDownloadSessionBuilder();
   }
 
+  /**
+   * Any retry capability must be defined within the provided ServerStreamingCallable. The
+   * ultimately produced channel will not do any retries of its own.
+   */
   public ReadableByteChannelSessionBuilder byteChannel(
       ServerStreamingCallable<ReadObjectRequest, ReadObjectResponse> read) {
     return new ReadableByteChannelSessionBuilder(read);
@@ -45,12 +49,14 @@ final class GapicDownloadSessionBuilder {
   public static final class ReadableByteChannelSessionBuilder {
 
     private final ServerStreamingCallable<ReadObjectRequest, ReadObjectResponse> read;
+    private boolean autoGzipDecompression;
     private Hasher hasher;
 
     private ReadableByteChannelSessionBuilder(
         ServerStreamingCallable<ReadObjectRequest, ReadObjectResponse> read) {
       this.read = read;
       this.hasher = Hasher.noop();
+      this.autoGzipDecompression = false;
     }
 
     public BufferedReadableByteChannelSessionBuilder buffered() {
@@ -62,25 +68,37 @@ final class GapicDownloadSessionBuilder {
       return this;
     }
 
+    public ReadableByteChannelSessionBuilder setAutoGzipDecompression(
+        boolean autoGzipDecompression) {
+      this.autoGzipDecompression = autoGzipDecompression;
+      return this;
+    }
+
     public BufferedReadableByteChannelSessionBuilder buffered(int capacity) {
-      return new BufferedReadableByteChannelSessionBuilder(
-          BufferHandle.allocate(capacity), getF(read, hasher));
+      return new BufferedReadableByteChannelSessionBuilder(BufferHandle.allocate(capacity), getF());
     }
 
     public BufferedReadableByteChannelSessionBuilder buffered(ByteBuffer buffer) {
-      return new BufferedReadableByteChannelSessionBuilder(
-          BufferHandle.handleOf(buffer), getF(read, hasher));
+      return new BufferedReadableByteChannelSessionBuilder(BufferHandle.handleOf(buffer), getF());
     }
 
     public UnbufferedReadableByteChannelSessionBuilder unbuffered() {
-      return new UnbufferedReadableByteChannelSessionBuilder(getF(read, hasher));
+      return new UnbufferedReadableByteChannelSessionBuilder(getF());
     }
 
-    private static BiFunction<
-            ReadObjectRequest, SettableApiFuture<Object>, UnbufferedReadableByteChannel>
-        getF(ServerStreamingCallable<ReadObjectRequest, ReadObjectResponse> read, Hasher hasher) {
-      return (object, resultFuture) ->
-          new GapicUnbufferedReadableByteChannel(resultFuture, read, object, hasher);
+    private BiFunction<ReadObjectRequest, SettableApiFuture<Object>, UnbufferedReadableByteChannel>
+        getF() {
+      // for any non-final value, create a reference to the value at this point in time
+      Hasher hasher = this.hasher;
+      boolean autoGzipDecompression = this.autoGzipDecompression;
+      return (object, resultFuture) -> {
+        if (autoGzipDecompression) {
+          return new GzipReadableByteChannel(
+              new GapicUnbufferedReadableByteChannel(resultFuture, read, object, hasher));
+        } else {
+          return new GapicUnbufferedReadableByteChannel(resultFuture, read, object, hasher);
+        }
+      };
     }
 
     public static final class BufferedReadableByteChannelSessionBuilder {
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedReadableByteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedReadableByteChannel.java
index 91031c08..daea9281 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedReadableByteChannel.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedReadableByteChannel.java
@@ -16,17 +16,18 @@
 
 package com.google.cloud.storage;
 
-import static com.google.cloud.storage.StorageV2ProtoUtils.seekReadObjectRequest;
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.api.client.http.HttpStatusCodes;
+import com.google.api.core.ApiFuture;
 import com.google.api.core.SettableApiFuture;
 import com.google.api.gax.rpc.ServerStream;
 import com.google.api.gax.rpc.ServerStreamingCallable;
-import com.google.cloud.storage.Crc32cValue.Crc32cLengthUnknown;
+import com.google.cloud.storage.Crc32cValue.Crc32cLengthKnown;
 import com.google.cloud.storage.UnbufferedReadableByteChannelSession.UnbufferedReadableByteChannel;
 import com.google.storage.v2.ChecksummedData;
 import com.google.storage.v2.Object;
+import com.google.storage.v2.ObjectChecksums;
 import com.google.storage.v2.ReadObjectRequest;
 import com.google.storage.v2.ReadObjectResponse;
 import java.io.Closeable;
@@ -49,11 +50,12 @@ final class GapicUnbufferedReadableByteChannel
 
   private boolean open = true;
   private boolean complete = false;
-  private long blobOffset = 0;
-  private long blobLimit = Long.MAX_VALUE;
-  private long totalSize = Long.MAX_VALUE; // initial sentinel value, will be updated upon responses
+  private boolean ioExceptionAlreadyThrown = false;
+  private long blobOffset;
+  private Crc32cLengthKnown cumulativeCrc32c;
 
   private Object metadata;
+  private ObjectChecksums objectChecksums;
 
   private ByteBuffer leftovers;
 
@@ -66,6 +68,7 @@ final class GapicUnbufferedReadableByteChannel
     this.read = read;
     this.req = req;
     this.hasher = hasher;
+    this.blobOffset = req.getReadOffset();
     this.iter = new LazyServerStreamIterator();
   }
 
@@ -90,9 +93,7 @@ final class GapicUnbufferedReadableByteChannel
     }
 
     long totalBufferCapacity = Arrays.stream(dsts).mapToLong(Buffer::remaining).sum();
-    long toRead = Math.min(blobLimit, totalBufferCapacity);
-
-    ReadCursor c = new ReadCursor(blobOffset, blobOffset + toRead);
+    ReadCursor c = new ReadCursor(blobOffset, blobOffset + totalBufferCapacity);
     while (c.hasRemaining()) {
       if (leftovers != null) {
         copy(c, leftovers, dsts, offset, length);
@@ -108,27 +109,44 @@ final class GapicUnbufferedReadableByteChannel
           Object respMetadata = resp.getMetadata();
           if (metadata == null) {
             metadata = respMetadata;
-          } else if (metadata.getMetageneration() != respMetadata.getMetageneration()) {
+          } else if (metadata.getGeneration() != respMetadata.getGeneration()) {
             throw closeWithError(
                 String.format(
-                    "Mismatch Metageneration between subsequent reads. Expected %d but received %d",
-                    metadata.getMetageneration(), respMetadata.getMetageneration()));
+                    "Mismatch Generation between subsequent reads. Expected %d but received %d",
+                    metadata.getGeneration(), respMetadata.getGeneration()));
           }
 
           if (!result.isDone()) {
             result.set(metadata);
           }
-
-          totalSize = metadata.getSize();
+        }
+        if (resp.hasObjectChecksums()) {
+          ObjectChecksums checksums = resp.getObjectChecksums();
+          if (this.objectChecksums == null) {
+            this.objectChecksums = checksums;
+          } else if (!objectChecksums.equals(checksums)) {
+            throw closeWithError(
+                String.format(
+                    "Mismatch checksums between subsequent reads. Expected %s but received %s",
+                    Crc32cValue.fmtCrc32cValue(objectChecksums.getCrc32C()),
+                    Crc32cValue.fmtCrc32cValue(checksums.getCrc32C())));
+          }
         }
         ChecksummedData checksummedData = resp.getChecksummedData();
         ByteBuffer content = checksummedData.getContent().asReadOnlyByteBuffer();
-        Crc32cLengthUnknown expected = Crc32cValue.of(checksummedData.getCrc32C());
-        try {
-          hasher.validate(expected, content::duplicate);
-        } catch (IOException e) {
-          close();
-          throw e;
+        // very important to know whether a crc32c value is set. Without checking, protobuf will
+        // happily return 0, which is a valid crc32c value.
+        if (checksummedData.hasCrc32C()) {
+          Crc32cLengthKnown expected =
+              Crc32cValue.of(checksummedData.getCrc32C(), checksummedData.getContent().size());
+          cumulativeCrc32c = hasher.nullSafeConcat(cumulativeCrc32c, expected);
+          try {
+            hasher.validate(expected, content::duplicate);
+          } catch (IOException e) {
+            ioExceptionAlreadyThrown = true;
+            close();
+            throw e;
+          }
         }
         copy(c, content, dsts, offset, length);
         if (content.hasRemaining()) {
@@ -153,16 +171,33 @@ final class GapicUnbufferedReadableByteChannel
 
   @Override
   public void close() throws IOException {
+    if (!ioExceptionAlreadyThrown
+        && cumulativeCrc32c != null
+        && objectChecksums != null
+        && objectChecksums.hasCrc32C()) {
+      Crc32cLengthKnown expected = Crc32cValue.of(objectChecksums.getCrc32C(), metadata.getSize());
+      if (!expected.eqValue(cumulativeCrc32c)) {
+        throw new IOException(
+            String.format(
+                "Mismatch checksum value. Expected %s actual %s",
+                expected.debugString(), cumulativeCrc32c.debugString()));
+      }
+    }
     open = false;
     iter.close();
   }
 
+  ApiFuture<Object> getResult() {
+    return result;
+  }
+
   private void copy(ReadCursor c, ByteBuffer content, ByteBuffer[] dsts, int offset, int length) {
     long copiedBytes = Buffers.copy(content, dsts, offset, length);
     c.advance(copiedBytes);
   }
 
   private IOException closeWithError(String message) throws IOException {
+    ioExceptionAlreadyThrown = true;
     close();
     StorageException cause =
         new StorageException(HttpStatusCodes.STATUS_CODE_PRECONDITION_FAILED, message);
@@ -200,7 +235,7 @@ final class GapicUnbufferedReadableByteChannel
 
     @Override
     public String toString() {
-      return String.format("Cursor{begin=%d, offset=%d, limit=%d}", beginning, offset, limit);
+      return String.format("ReadCursor{begin=%d, offset=%d, limit=%d}", beginning, offset, limit);
     }
   }
 
@@ -236,8 +271,7 @@ final class GapicUnbufferedReadableByteChannel
         synchronized (this) {
           if (!streamInitialized) {
             if (serverStream == null) {
-              ReadObjectRequest request = seekReadObjectRequest(req, blobOffset, blobLimit);
-              serverStream = read.call(request);
+              serverStream = read.call(req);
             }
             responseIterator = serverStream.iterator();
             streamInitialized = true;
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 c88a7221..240113b8 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
@@ -17,7 +17,6 @@
 package com.google.cloud.storage;
 
 import com.google.api.core.SettableApiFuture;
-import com.google.api.gax.rpc.ClientStreamingCallable;
 import com.google.cloud.storage.ChunkSegmenter.ChunkSegment;
 import com.google.cloud.storage.Crc32cValue.Crc32cLengthKnown;
 import com.google.cloud.storage.UnbufferedWritableByteChannelSession.UnbufferedWritableByteChannel;
@@ -27,7 +26,6 @@ import com.google.cloud.storage.WriteFlushStrategy.FlusherFactory;
 import com.google.protobuf.ByteString;
 import com.google.storage.v2.ChecksummedData;
 import com.google.storage.v2.ObjectChecksums;
-import com.google.storage.v2.ServiceConstants.Values;
 import com.google.storage.v2.WriteObjectRequest;
 import com.google.storage.v2.WriteObjectResponse;
 import java.io.IOException;
@@ -41,11 +39,9 @@ final class GapicUnbufferedWritableByteChannel<
     implements UnbufferedWritableByteChannel {
 
   private final SettableApiFuture<WriteObjectResponse> resultFuture;
-  private final ByteStringStrategy byteStringStrategy;
-  private final Hasher hasher;
-  private final WriteCtx<RequestFactoryT> writeCtx;
-  private final int perMessageLimit;
+  private final ChunkSegmenter chunkSegmenter;
 
+  private final WriteCtx<RequestFactoryT> writeCtx;
   private final Flusher flusher;
 
   private boolean open = true;
@@ -53,19 +49,15 @@ final class GapicUnbufferedWritableByteChannel<
 
   GapicUnbufferedWritableByteChannel(
       SettableApiFuture<WriteObjectResponse> resultFuture,
-      ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write,
+      ChunkSegmenter chunkSegmenter,
       RequestFactoryT requestFactory,
-      ByteStringStrategy byteStringStrategy,
-      Hasher hasher,
-      FlusherFactory flusher) {
+      FlusherFactory flusherFactory) {
     this.resultFuture = resultFuture;
-    this.byteStringStrategy = byteStringStrategy;
-    this.hasher = hasher;
+    this.chunkSegmenter = chunkSegmenter;
 
     this.writeCtx = new WriteCtx<>(requestFactory);
-    this.perMessageLimit = Values.MAX_WRITE_CHUNK_BYTES_VALUE;
     this.flusher =
-        flusher.newFlusher(write, writeCtx.getConfirmedBytes()::addAndGet, resultFuture::set);
+        flusherFactory.newFlusher(writeCtx.getConfirmedBytes()::addAndGet, resultFuture::set);
   }
 
   @Override
@@ -84,9 +76,7 @@ final class GapicUnbufferedWritableByteChannel<
       throw new ClosedChannelException();
     }
 
-    ChunkSegment[] data =
-        ChunkSegmenter.segmentBuffers(
-            srcs, hasher, byteStringStrategy, perMessageLimit, srcsOffset, srcLength);
+    ChunkSegment[] data = chunkSegmenter.segmentBuffers(srcs, srcsOffset, srcLength);
 
     List<WriteObjectRequest> messages = new ArrayList<>();
 
@@ -98,7 +88,9 @@ final class GapicUnbufferedWritableByteChannel<
       long offset = writeCtx.getTotalSentBytes().getAndAdd(contentSize);
       ChecksummedData.Builder checksummedData = ChecksummedData.newBuilder().setContent(b);
       if (crc32c != null) {
-        writeCtx.getCumulativeCrc32c().getAndAccumulate(crc32c, Crc32cValue::nullSafeConcat);
+        writeCtx
+            .getCumulativeCrc32c()
+            .getAndAccumulate(crc32c, chunkSegmenter.getHasher()::nullSafeConcat);
         checksummedData.setCrc32C(crc32c.getValue());
       }
       WriteObjectRequest.Builder builder =
@@ -106,7 +98,7 @@ final class GapicUnbufferedWritableByteChannel<
               .newRequestBuilder()
               .setWriteOffset(offset)
               .setChecksummedData(checksummedData.build());
-      if (contentSize < perMessageLimit) {
+      if (!datum.isOnlyFullBlocks()) {
         builder.setFinishWrite(true);
         if (crc32c != null) {
           builder.setObjectChecksums(
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicWritableByteChannelSessionBuilder.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicWritableByteChannelSessionBuilder.java
index 553cda89..f34c1c67 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicWritableByteChannelSessionBuilder.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicWritableByteChannelSessionBuilder.java
@@ -27,6 +27,7 @@ import com.google.cloud.storage.ChannelSession.UnbufferedWriteSession;
 import com.google.cloud.storage.UnbufferedWritableByteChannelSession.UnbufferedWritableByteChannel;
 import com.google.cloud.storage.WriteCtx.WriteObjectRequestBuilderFactory;
 import com.google.cloud.storage.WriteFlushStrategy.FlusherFactory;
+import com.google.storage.v2.ServiceConstants.Values;
 import com.google.storage.v2.WriteObjectRequest;
 import com.google.storage.v2.WriteObjectResponse;
 import java.nio.ByteBuffer;
@@ -35,7 +36,7 @@ import java.util.function.Function;
 
 final class GapicWritableByteChannelSessionBuilder {
 
-  private static final int DEFAULT_BUFFER_CAPACITY = 16 * 1024 * 1024;
+  private static final int DEFAULT_BUFFER_CAPACITY = ByteSizeConstants._16MiB;
   private final ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write;
   private Hasher hasher;
   private ByteStringStrategy byteStringStrategy;
@@ -124,7 +125,10 @@ final class GapicWritableByteChannelSessionBuilder {
     Hasher boundHasher = hasher;
     return (start, resultFuture) ->
         new GapicUnbufferedWritableByteChannel<>(
-            resultFuture, write, f.apply(start), boundStrategy, boundHasher, flusherFactory);
+            resultFuture,
+            new ChunkSegmenter(boundHasher, boundStrategy, Values.MAX_WRITE_CHUNK_BYTES_VALUE),
+            f.apply(start),
+            flusherFactory);
   }
 
   final class DirectUploadBuilder {
@@ -212,7 +216,8 @@ final class GapicWritableByteChannelSessionBuilder {
     UnbufferedWritableByteChannelSession<WriteObjectResponse> build() {
       return new UnbufferedWriteSession<>(
           ApiFutures.immediateFuture(requireNonNull(req, "req must be non null")),
-          bindFunction(WriteFlushStrategy::fsyncOnClose, WriteObjectRequestBuilderFactory::simple)
+          bindFunction(
+                  WriteFlushStrategy.fsyncOnClose(write), WriteObjectRequestBuilderFactory::simple)
               .andThen(StorageByteChannels.writable()::createSynchronized));
     }
   }
@@ -235,7 +240,8 @@ final class GapicWritableByteChannelSessionBuilder {
     BufferedWritableByteChannelSession<WriteObjectResponse> build() {
       return new BufferedWriteSession<>(
           ApiFutures.immediateFuture(requireNonNull(req, "req must be non null")),
-          bindFunction(WriteFlushStrategy::fsyncOnClose, WriteObjectRequestBuilderFactory::simple)
+          bindFunction(
+                  WriteFlushStrategy.fsyncOnClose(write), WriteObjectRequestBuilderFactory::simple)
               .andThen(c -> new DefaultBufferedWritableByteChannel(bufferHandle, c))
               .andThen(StorageByteChannels.writable()::createSynchronized));
     }
@@ -257,7 +263,7 @@ final class GapicWritableByteChannelSessionBuilder {
     UnbufferedWritableByteChannelSession<WriteObjectResponse> build() {
       return new UnbufferedWriteSession<>(
           requireNonNull(start, "start must be non null"),
-          bindFunction(WriteFlushStrategy::fsyncEveryFlush, ResumableWrite::identity)
+          bindFunction(WriteFlushStrategy.fsyncEveryFlush(write), ResumableWrite::identity)
               .andThen(StorageByteChannels.writable()::createSynchronized));
     }
   }
@@ -284,7 +290,7 @@ final class GapicWritableByteChannelSessionBuilder {
     BufferedWritableByteChannelSession<WriteObjectResponse> build() {
       return new BufferedWriteSession<>(
           requireNonNull(start, "start must be non null"),
-          bindFunction(WriteFlushStrategy::fsyncEveryFlush, ResumableWrite::identity)
+          bindFunction(WriteFlushStrategy.fsyncEveryFlush(write), ResumableWrite::identity)
               .andThen(c -> new DefaultBufferedWritableByteChannel(bufferHandle, c))
               .andThen(StorageByteChannels.writable()::createSynchronized));
     }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java
index 83910610..411b5a7d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java
@@ -44,7 +44,8 @@ final class GrpcBlobReadChannel implements ReadChannel {
 
   GrpcBlobReadChannel(
       ServerStreamingCallable<ReadObjectRequest, ReadObjectResponse> read,
-      ReadObjectRequest request) {
+      ReadObjectRequest request,
+      boolean autoGzipDecompression) {
     this.lazyReadChannel =
         new LazyReadChannel(
             Suppliers.memoize(
@@ -54,6 +55,7 @@ final class GrpcBlobReadChannel implements ReadChannel {
                       .read()
                       .byteChannel(read)
                       .setHasher(Hasher.noop())
+                      .setAutoGzipDecompression(autoGzipDecompression)
                       .buffered(Buffers.allocate(chunkSize))
                       .setReadObjectRequest(req)
                       .build();
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 78f01b47..14fa1c2e 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
@@ -16,8 +16,8 @@
 
 package com.google.cloud.storage;
 
-import static com.google.cloud.storage.GrpcStorageImpl._15MiB;
-import static com.google.cloud.storage.GrpcStorageImpl._256KiB;
+import static com.google.cloud.storage.ByteSizeConstants._15MiB;
+import static com.google.cloud.storage.ByteSizeConstants._256KiB;
 import static com.google.cloud.storage.Utils.todo;
 
 import com.google.api.core.ApiFuture;
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 4b32d2b6..df0c4124 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
@@ -20,6 +20,7 @@ import static com.google.cloud.storage.Utils.bucketNameCodec;
 import static com.google.cloud.storage.Utils.durationMillisCodec;
 import static com.google.cloud.storage.Utils.ifNonNull;
 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 static com.google.cloud.storage.Utils.todo;
 
@@ -678,12 +679,12 @@ final class GrpcConversions {
 
   private HmacKeyMetadata hmacKeyMetadataEncode(HmacKey.HmacKeyMetadata from) {
     HmacKeyMetadata.Builder to = HmacKeyMetadata.newBuilder();
-    to.setAccessId(from.getAccessId());
-    // TODO etag
-    to.setId(from.getId());
-    to.setProject(from.getProjectId());
+    ifNonNull(from.getEtag(), to::setEtag);
+    ifNonNull(from.getId(), to::setId);
+    ifNonNull(from.getAccessId(), to::setAccessId);
+    ifNonNull(from.getProjectId(), projectNameCodec::encode, to::setProject);
     ifNonNull(from.getServiceAccount(), ServiceAccount::getEmail, to::setServiceAccountEmail);
-    ifNonNull(from.getState(), java.lang.Object::toString, to::setState);
+    ifNonNull(from.getState(), Enum::name, to::setState);
     ifNonNull(from.getCreateTimeOffsetDateTime(), timestampCodec::encode, to::setCreateTime);
     ifNonNull(from.getUpdateTimeOffsetDateTime(), timestampCodec::encode, to::setUpdateTime);
     return to.build();
@@ -694,7 +695,7 @@ final class GrpcConversions {
         .setAccessId(from.getAccessId())
         .setCreateTimeOffsetDateTime(timestampCodec.decode(from.getCreateTime()))
         .setId(from.getId())
-        .setProjectId(from.getProject())
+        .setProjectId(projectNameCodec.decode(from.getProject()))
         .setState(HmacKey.HmacKeyState.valueOf(from.getState()))
         .setUpdateTimeOffsetDateTime(timestampCodec.decode(from.getUpdateTime()))
         .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 16b5a5af..dcdf2952 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
@@ -16,6 +16,8 @@
 
 package com.google.cloud.storage;
 
+import static com.google.cloud.storage.ByteSizeConstants._15MiB;
+import static com.google.cloud.storage.ByteSizeConstants._256KiB;
 import static com.google.cloud.storage.Utils.bucketNameCodec;
 import static com.google.cloud.storage.Utils.ifNonNull;
 import static com.google.cloud.storage.Utils.projectNameCodec;
@@ -25,10 +27,14 @@ import static java.util.Objects.requireNonNull;
 
 import com.google.api.core.ApiFuture;
 import com.google.api.gax.grpc.GrpcCallContext;
+import com.google.api.gax.grpc.GrpcStatusCode;
 import com.google.api.gax.paging.AbstractPage;
 import com.google.api.gax.paging.Page;
+import com.google.api.gax.retrying.ResultRetryAlgorithm;
 import com.google.api.gax.rpc.ApiException;
+import com.google.api.gax.rpc.ApiExceptionFactory;
 import com.google.api.gax.rpc.ApiExceptions;
+import com.google.api.gax.rpc.StatusCode;
 import com.google.api.gax.rpc.UnaryCallable;
 import com.google.cloud.BaseService;
 import com.google.cloud.Policy;
@@ -46,6 +52,7 @@ import com.google.cloud.storage.UnifiedOpts.BucketListOpt;
 import com.google.cloud.storage.UnifiedOpts.BucketSourceOpt;
 import com.google.cloud.storage.UnifiedOpts.BucketTargetOpt;
 import com.google.cloud.storage.UnifiedOpts.HmacKeyListOpt;
+import com.google.cloud.storage.UnifiedOpts.HmacKeySourceOpt;
 import com.google.cloud.storage.UnifiedOpts.HmacKeyTargetOpt;
 import com.google.cloud.storage.UnifiedOpts.ObjectListOpt;
 import com.google.cloud.storage.UnifiedOpts.ObjectSourceOpt;
@@ -53,16 +60,20 @@ import com.google.cloud.storage.UnifiedOpts.ObjectTargetOpt;
 import com.google.cloud.storage.UnifiedOpts.Opts;
 import com.google.cloud.storage.UnifiedOpts.ProjectId;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.io.BaseEncoding;
 import com.google.common.io.ByteStreams;
+import com.google.protobuf.ByteString;
 import com.google.protobuf.FieldMask;
 import com.google.protobuf.Message;
 import com.google.storage.v2.ComposeObjectRequest;
 import com.google.storage.v2.ComposeObjectRequest.SourceObject;
 import com.google.storage.v2.CreateBucketRequest;
+import com.google.storage.v2.CreateHmacKeyRequest;
 import com.google.storage.v2.DeleteBucketRequest;
 import com.google.storage.v2.DeleteHmacKeyRequest;
 import com.google.storage.v2.DeleteObjectRequest;
 import com.google.storage.v2.GetBucketRequest;
+import com.google.storage.v2.GetHmacKeyRequest;
 import com.google.storage.v2.GetObjectRequest;
 import com.google.storage.v2.GetServiceAccountRequest;
 import com.google.storage.v2.ListBucketsRequest;
@@ -71,6 +82,7 @@ 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;
 import com.google.storage.v2.StorageClient.ListBucketsPage;
 import com.google.storage.v2.StorageClient.ListBucketsPagedResponse;
 import com.google.storage.v2.StorageClient.ListHmacKeysPage;
@@ -78,11 +90,12 @@ import com.google.storage.v2.StorageClient.ListHmacKeysPagedResponse;
 import com.google.storage.v2.StorageClient.ListObjectsPage;
 import com.google.storage.v2.StorageClient.ListObjectsPagedResponse;
 import com.google.storage.v2.UpdateBucketRequest;
+import com.google.storage.v2.UpdateHmacKeyRequest;
 import com.google.storage.v2.UpdateObjectRequest;
 import com.google.storage.v2.WriteObjectRequest;
 import com.google.storage.v2.WriteObjectResponse;
 import com.google.storage.v2.WriteObjectSpec;
-import com.google.storage.v2.stub.GrpcStorageStub;
+import io.grpc.Status.Code;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -99,11 +112,19 @@ import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.Arrays;
-import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
+import java.util.Spliterator;
+import java.util.Spliterators.AbstractSpliterator;
+import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
 final class GrpcStorageImpl extends BaseService<StorageOptions> implements Storage {
 
@@ -114,19 +135,30 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
           StandardOpenOption.WRITE,
           StandardOpenOption.CREATE,
           StandardOpenOption.TRUNCATE_EXISTING);
-  static final int _256KiB = 256 * 1024;
-  static final int _15MiB = 15 * 1024 * 1024;
-
-  private final GrpcStorageStub grpcStorageStub;
+  /**
+   * For use in {@link #resultRetryAlgorithmToCodes(ResultRetryAlgorithm)}. Resolve all codes and
+   * construct corresponding ApiExceptions.
+   *
+   * <p>Constructing the exceptions will walk the stack for each one. In order to avoid the stack
+   * walking overhead for every Code for every invocation of read, construct the set of exceptions
+   * only once and keep in this value.
+   */
+  private static final Set<ApiException> CODE_API_EXCEPTIONS =
+      Arrays.stream(StatusCode.Code.values())
+          .map(GrpcStorageImpl::statusCodeFor)
+          .map(c -> ApiExceptionFactory.createException(null, c, false))
+          .collect(Collectors.toSet());
+
+  private final StorageClient storageClient;
   private final GrpcConversions codecs;
   private final GrpcRetryAlgorithmManager retryAlgorithmManager;
   private final SyntaxDecoders syntaxDecoders;
 
-  private final transient ProjectId defaultProjectId;
+  @Deprecated private final transient ProjectId defaultProjectId;
 
-  GrpcStorageImpl(GrpcStorageOptions options, GrpcStorageStub grpcStorageStub) {
+  GrpcStorageImpl(GrpcStorageOptions options, StorageClient storageClient) {
     super(options);
-    this.grpcStorageStub = grpcStorageStub;
+    this.storageClient = storageClient;
     this.codecs = Conversions.grpc();
     this.retryAlgorithmManager = options.getRetryAlgorithmManager();
     this.syntaxDecoders = new SyntaxDecoders();
@@ -135,7 +167,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
   @Override
   public void close() throws Exception {
-    try (GrpcStorageStub s = grpcStorageStub) {
+    try (StorageClient s = storageClient) {
       s.shutdownNow();
     }
   }
@@ -155,7 +187,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     return Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
-        () -> grpcStorageStub.createBucketCallable().call(req, grpcCallContext),
+        () -> storageClient.createBucketCallable().call(req, grpcCallContext),
         syntaxDecoders.bucket);
   }
 
@@ -184,7 +216,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
           ResumableMedia.gapic()
               .write()
               .byteChannel(
-                  grpcStorageStub.writeObjectCallable().withDefaultCallContext(grpcCallContext))
+                  storageClient.writeObjectCallable().withDefaultCallContext(grpcCallContext))
               .setByteStringStrategy(ByteStringStrategy.noCopy())
               .setHasher(Hasher.enabled())
               .direct()
@@ -233,7 +265,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         ResumableMedia.gapic()
             .write()
             .byteChannel(
-                grpcStorageStub.writeObjectCallable().withDefaultCallContext(grpcCallContext))
+                storageClient.writeObjectCallable().withDefaultCallContext(grpcCallContext))
             .setHasher(Hasher.enabled())
             .setByteStringStrategy(ByteStringStrategy.noCopy());
 
@@ -248,7 +280,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
           ResumableMedia.gapic()
               .write()
               .resumableWrite(
-                  grpcStorageStub
+                  storageClient
                       .startResumableWriteCallable()
                       .withDefaultCallContext(grpcCallContext),
                   req);
@@ -290,16 +322,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         ResumableMedia.gapic()
             .write()
             .resumableWrite(
-                grpcStorageStub
-                    .startResumableWriteCallable()
-                    .withDefaultCallContext(grpcCallContext),
+                storageClient.startResumableWriteCallable().withDefaultCallContext(grpcCallContext),
                 req);
 
     BufferedWritableByteChannelSession<WriteObjectResponse> session =
         ResumableMedia.gapic()
             .write()
             .byteChannel(
-                grpcStorageStub.writeObjectCallable().withDefaultCallContext(grpcCallContext))
+                storageClient.writeObjectCallable().withDefaultCallContext(grpcCallContext))
             .setHasher(Hasher.enabled())
             .setByteStringStrategy(ByteStringStrategy.noCopy())
             .resumable()
@@ -331,7 +361,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     return Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
-        () -> grpcStorageStub.getBucketCallable().call(req, grpcCallContext),
+        () -> storageClient.getBucketCallable().call(req, grpcCallContext),
         syntaxDecoders.bucket);
   }
 
@@ -356,7 +386,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     return Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
-        () -> grpcStorageStub.getObjectCallable().call(req, grpcCallContext),
+        () -> storageClient.getObjectCallable().call(req, grpcCallContext),
         syntaxDecoders.blob);
   }
 
@@ -368,22 +398,26 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   @Override
   public Page<Bucket> list(BucketListOption... options) {
     UnaryCallable<ListBucketsRequest, ListBucketsPagedResponse> listBucketsCallable =
-        grpcStorageStub.listBucketsPagedCallable();
+        storageClient.listBucketsPagedCallable();
     Opts<BucketListOpt> opts = Opts.unwrap(options);
     GrpcCallContext grpcCallContext =
         opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
-    ListBucketsRequest.Builder builder =
-        ListBucketsRequest.newBuilder().setParent(projectNameCodec.encode(defaultProjectId.val));
-    ListBucketsRequest request = opts.listBucketsRequest().apply(builder).build();
+    ListBucketsRequest request =
+        defaultProjectId
+            .listBuckets()
+            .andThen(opts.listBucketsRequest())
+            .apply(ListBucketsRequest.newBuilder())
+            .build();
     ListBucketsPagedResponse call = listBucketsCallable.call(request, grpcCallContext);
     ListBucketsPage page = call.getPage();
-    return new TransformingPageDecorator<>(page, syntaxDecoders.bucket);
+    return new TransformingPageDecorator<>(
+        page, syntaxDecoders.bucket, getOptions(), retryAlgorithmManager.getFor(request));
   }
 
   @Override
   public Page<Blob> list(String bucket, BlobListOption... options) {
     UnaryCallable<ListObjectsRequest, ListObjectsPagedResponse> listObjectsCallable =
-        grpcStorageStub.listObjectsPagedCallable();
+        storageClient.listObjectsPagedCallable();
     Opts<ObjectListOpt> opts = Opts.unwrap(options);
     GrpcCallContext grpcCallContext =
         opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
@@ -392,7 +426,8 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     ListObjectsRequest req = opts.listObjectsRequest().apply(builder).build();
     ListObjectsPagedResponse call = listObjectsCallable.call(req, grpcCallContext);
     ListObjectsPage page = call.getPage();
-    return new TransformingPageDecorator<>(page, syntaxDecoders.blob);
+    return new TransformingPageDecorator<>(
+        page, syntaxDecoders.blob, getOptions(), retryAlgorithmManager.getFor(req));
   }
 
   @Override
@@ -411,7 +446,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     return Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
-        () -> grpcStorageStub.updateBucketCallable().call(req, grpcCallContext),
+        () -> storageClient.updateBucketCallable().call(req, grpcCallContext),
         syntaxDecoders.bucket);
   }
 
@@ -430,7 +465,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     return Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
-        () -> grpcStorageStub.updateObjectCallable().call(req, grpcCallContext),
+        () -> storageClient.updateObjectCallable().call(req, grpcCallContext),
         syntaxDecoders.blob);
   }
 
@@ -451,7 +486,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
       Retrying.run(
           getOptions(),
           retryAlgorithmManager.getFor(req),
-          () -> grpcStorageStub.deleteBucketCallable().call(req, grpcCallContext),
+          () -> storageClient.deleteBucketCallable().call(req, grpcCallContext),
           Decoder.identity());
       return true;
     } catch (ApiException e) {
@@ -478,7 +513,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
       Retrying.run(
           getOptions(),
           retryAlgorithmManager.getFor(req),
-          () -> grpcStorageStub.deleteObjectCallable().call(req, grpcCallContext),
+          () -> storageClient.deleteObjectCallable().call(req, grpcCallContext),
           Decoder.identity());
       return true;
     } catch (ApiException e) {
@@ -513,7 +548,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     return Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
-        () -> grpcStorageStub.composeObjectCallable().call(req, grpcCallContext),
+        () -> storageClient.composeObjectCallable().call(req, grpcCallContext),
         syntaxDecoders.blob);
   }
 
@@ -529,15 +564,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
   @Override
   public byte[] readAllBytes(BlobId blob, BlobSourceOption... options) {
-    Opts<ObjectSourceOpt> opts = Opts.unwrap(options).resolveFrom(blob);
-    ReadObjectRequest readObjectRequest = getReadObjectRequest(blob, opts);
-    UnbufferedReadableByteChannelSession<Object> session =
-        ResumableMedia.gapic()
-            .read()
-            .byteChannel(grpcStorageStub.readObjectCallable())
-            .unbuffered()
-            .setReadObjectRequest(readObjectRequest)
-            .build();
+    UnbufferedReadableByteChannelSession<Object> session = unbufferedReadSession(blob, options);
 
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     try (UnbufferedReadableByteChannel r = session.open();
@@ -563,24 +590,19 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   public GrpcBlobReadChannel reader(BlobId blob, BlobSourceOption... options) {
     Opts<ObjectSourceOpt> opts = Opts.unwrap(options).resolveFrom(blob);
     ReadObjectRequest request = getReadObjectRequest(blob, opts);
-    return new GrpcBlobReadChannel(grpcStorageStub.readObjectCallable(), request);
+    Set<StatusCode.Code> codes =
+        GrpcStorageImpl.resultRetryAlgorithmToCodes(retryAlgorithmManager.getFor(request));
+    GrpcCallContext grpcCallContext = GrpcCallContext.createDefault().withRetryableCodes(codes);
+    return new GrpcBlobReadChannel(
+        storageClient.readObjectCallable().withDefaultCallContext(grpcCallContext),
+        request,
+        !opts.autoGzipDecompression());
   }
 
   @Override
   public void downloadTo(BlobId blob, Path path, BlobSourceOption... options) {
 
-    Opts<ObjectSourceOpt> opts = Opts.unwrap(options).resolveFrom(blob);
-
-    // TODO: handle StorageRpc.Option.RETURN_RAW_INPUT_STREAM impacts
-
-    ReadObjectRequest readObjectRequest = getReadObjectRequest(blob, opts);
-    UnbufferedReadableByteChannelSession<Object> session =
-        ResumableMedia.gapic()
-            .read()
-            .byteChannel(grpcStorageStub.readObjectCallable())
-            .unbuffered()
-            .setReadObjectRequest(readObjectRequest)
-            .build();
+    UnbufferedReadableByteChannelSession<Object> session = unbufferedReadSession(blob, options);
 
     try (UnbufferedReadableByteChannel r = session.open();
         WritableByteChannel w = Files.newByteChannel(path, WRITE_OPS)) {
@@ -593,18 +615,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   @Override
   public void downloadTo(BlobId blob, OutputStream outputStream, BlobSourceOption... options) {
 
-    Opts<ObjectSourceOpt> opts = Opts.unwrap(options).resolveFrom(blob);
-
-    // TODO: handle StorageRpc.Option.RETURN_RAW_INPUT_STREAM impacts
-
-    ReadObjectRequest readObjectRequest = getReadObjectRequest(blob, opts);
-    UnbufferedReadableByteChannelSession<Object> session =
-        ResumableMedia.gapic()
-            .read()
-            .byteChannel(grpcStorageStub.readObjectCallable())
-            .unbuffered()
-            .setReadObjectRequest(readObjectRequest)
-            .build();
+    UnbufferedReadableByteChannelSession<Object> session = unbufferedReadSession(blob, options);
 
     try (UnbufferedReadableByteChannel r = session.open();
         WritableByteChannel w = Channels.newChannel(outputStream)) {
@@ -621,12 +632,12 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
     WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts);
     return new GrpcBlobWriteChannel(
-        grpcStorageStub.writeObjectCallable(),
+        storageClient.writeObjectCallable(),
         () ->
             ResumableMedia.gapic()
                 .write()
                 .resumableWrite(
-                    grpcStorageStub
+                    storageClient
                         .startResumableWriteCallable()
                         .withDefaultCallContext(grpcCallContext),
                     req));
@@ -840,30 +851,66 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
   @Override
   public HmacKey createHmacKey(ServiceAccount serviceAccount, CreateHmacKeyOption... options) {
-    return todo();
+    Opts<HmacKeyTargetOpt> opts = Opts.unwrap(options);
+    GrpcCallContext grpcCallContext =
+        opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
+    CreateHmacKeyRequest request =
+        defaultProjectId
+            .createHmacKey()
+            .andThen(opts.createHmacKeysRequest())
+            .apply(CreateHmacKeyRequest.newBuilder())
+            .setServiceAccountEmail(serviceAccount.getEmail())
+            .build();
+    return Retrying.run(
+        getOptions(),
+        retryAlgorithmManager.getFor(request),
+        () -> storageClient.createHmacKeyCallable().call(request, grpcCallContext),
+        resp -> {
+          ByteString secretKeyBytes = resp.getSecretKeyBytes();
+          String b64SecretKey = BaseEncoding.base64().encode(secretKeyBytes.toByteArray());
+          return HmacKey.newBuilder(b64SecretKey)
+              .setMetadata(codecs.hmacKeyMetadata().decode(resp.getMetadata()))
+              .build();
+        });
   }
 
   @Override
   public Page<HmacKeyMetadata> listHmacKeys(ListHmacKeysOption... options) {
     UnaryCallable<ListHmacKeysRequest, ListHmacKeysPagedResponse> listHmacKeysCallable =
-        grpcStorageStub.listHmacKeysPagedCallable();
+        storageClient.listHmacKeysPagedCallable();
     Opts<HmacKeyListOpt> opts = Opts.unwrap(options);
     GrpcCallContext grpcCallContext =
         opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
 
-    ProjectId projectId = opts.projectId().orElse(defaultProjectId);
-
-    ListHmacKeysRequest.Builder builder =
-        projectId.listHmacKeys().apply(ListHmacKeysRequest.newBuilder());
-    ListHmacKeysRequest request = opts.listHmacKeysRequest().apply(builder).build();
+    ListHmacKeysRequest request =
+        defaultProjectId
+            .listHmacKeys()
+            .andThen(opts.listHmacKeysRequest())
+            .apply(ListHmacKeysRequest.newBuilder())
+            .build();
     ListHmacKeysPagedResponse call = listHmacKeysCallable.call(request, grpcCallContext);
     ListHmacKeysPage page = call.getPage();
-    return new TransformingPageDecorator<>(page, codecs.hmacKeyMetadata());
+    return new TransformingPageDecorator<>(
+        page, codecs.hmacKeyMetadata(), getOptions(), retryAlgorithmManager.getFor(request));
   }
 
   @Override
   public HmacKeyMetadata getHmacKey(String accessId, GetHmacKeyOption... options) {
-    return todo();
+    Opts<HmacKeySourceOpt> opts = Opts.unwrap(options);
+    GrpcCallContext grpcCallContext =
+        opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
+    GetHmacKeyRequest request =
+        defaultProjectId
+            .getHmacKey()
+            .andThen(opts.getHmacKeysRequest())
+            .apply(GetHmacKeyRequest.newBuilder())
+            .setAccessId(accessId)
+            .build();
+    return Retrying.run(
+        getOptions(),
+        retryAlgorithmManager.getFor(request),
+        () -> storageClient.getHmacKeyCallable().call(request, grpcCallContext),
+        codecs.hmacKeyMetadata());
   }
 
   @Override
@@ -874,13 +921,13 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     DeleteHmacKeyRequest req =
         DeleteHmacKeyRequest.newBuilder()
             .setAccessId(hmacKeyMetadata.getAccessId())
-            .setProject(hmacKeyMetadata.getProjectId())
+            .setProject(projectNameCodec.encode(hmacKeyMetadata.getProjectId()))
             .build();
     Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
         () -> {
-          grpcStorageStub.deleteHmacKeyCallable().call(req, grpcCallContext);
+          storageClient.deleteHmacKeyCallable().call(req, grpcCallContext);
           return null;
         },
         Decoder.identity());
@@ -889,7 +936,21 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   @Override
   public HmacKeyMetadata updateHmacKeyState(
       HmacKeyMetadata hmacKeyMetadata, HmacKeyState state, UpdateHmacKeyOption... options) {
-    return todo();
+    Opts<HmacKeyTargetOpt> opts = Opts.unwrap(options);
+    GrpcCallContext grpcCallContext =
+        opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
+    com.google.storage.v2.HmacKeyMetadata encode =
+        codecs.hmacKeyMetadata().encode(hmacKeyMetadata).toBuilder().setState(state.name()).build();
+
+    UpdateHmacKeyRequest.Builder builder =
+        opts.updateHmacKeysRequest().apply(UpdateHmacKeyRequest.newBuilder()).setHmacKey(encode);
+    UpdateHmacKeyRequest request =
+        builder.setUpdateMask(FieldMask.newBuilder().addPaths("state").build()).build();
+    return Retrying.run(
+        getOptions(),
+        retryAlgorithmManager.getFor(request),
+        () -> storageClient.updateHmacKeyCallable().call(request, grpcCallContext),
+        codecs.hmacKeyMetadata());
   }
 
   @Override
@@ -915,7 +976,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     return Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
-        () -> grpcStorageStub.getServiceAccountCallable().call(req),
+        () -> storageClient.getServiceAccountCallable().call(req),
         codecs.serviceAccount());
   }
 
@@ -969,10 +1030,18 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
     private final PageT page;
     private final Decoder<ResourceT, ModelT> translator;
-
-    public TransformingPageDecorator(PageT page, Decoder<ResourceT, ModelT> translator) {
+    private final Retrying.RetryingDependencies deps;
+    private final ResultRetryAlgorithm<?> resultRetryAlgorithm;
+
+    TransformingPageDecorator(
+        PageT page,
+        Decoder<ResourceT, ModelT> translator,
+        Retrying.RetryingDependencies deps,
+        ResultRetryAlgorithm<?> resultRetryAlgorithm) {
       this.page = page;
       this.translator = translator;
+      this.deps = deps;
+      this.resultRetryAlgorithm = resultRetryAlgorithm;
     }
 
     @Override
@@ -987,43 +1056,84 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
     @Override
     public Page<ModelT> getNextPage() {
-      return new TransformingPageDecorator<>(page.getNextPage(), translator);
+      return new TransformingPageDecorator<>(
+          page.getNextPage(), translator, deps, resultRetryAlgorithm);
     }
 
+    @SuppressWarnings({"Convert2MethodRef"})
     @Override
     public Iterable<ModelT> iterateAll() {
-      return () -> {
-        final Iterator<ResourceT> iter = page.iterateAll().iterator();
-        return new TransformingIterator(iter);
-      };
+      // iterateAll on AbstractPage isn't very friendly to decoration, as getNextPage isn't actually
+      // ever called. This means we aren't able to apply our retry wrapping there.
+      // Instead, what we do is create a stream which will attempt to call getNextPage repeatedly
+      // until we meet some condition of exhaustion. At that point we can apply our retry logic.
+      return () ->
+          streamIterate(
+                  page,
+                  p -> p != null && p.hasNextPage(),
+                  prev -> {
+                    // explicitly define this callable rather than using the method reference to
+                    // prevent a javac 1.8 exception
+                    // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8056984
+                    Callable<PageT> c = () -> prev.getNextPage();
+                    return Retrying.run(deps, resultRetryAlgorithm, c, Decoder.identity());
+                  })
+              .filter(Objects::nonNull)
+              .flatMap(p -> StreamSupport.stream(p.getValues().spliterator(), false))
+              .map(translator::decode)
+              .iterator();
     }
 
     @Override
     public Iterable<ModelT> getValues() {
-      return () -> {
-        final Iterator<ResourceT> inter = page.getValues().iterator();
-        return new TransformingIterator(inter);
-      };
+      return () ->
+          StreamSupport.stream(page.getValues().spliterator(), false)
+              .map(translator::decode)
+              .iterator();
     }
 
-    private class TransformingIterator implements Iterator<ModelT> {
-
-      private final Iterator<ResourceT> iter;
-
-      public TransformingIterator(Iterator<ResourceT> iter) {
-        this.iter = iter;
-      }
-
-      @Override
-      public boolean hasNext() {
-        return iter.hasNext();
-      }
-
-      @Override
-      public ModelT next() {
-        ResourceT next = iter.next();
-        return translator.decode(next);
-      }
+    private static <T> Stream<T> streamIterate(
+        T seed, Predicate<? super T> shouldComputeNext, UnaryOperator<T> computeNext) {
+      requireNonNull(seed, "seed must be non null");
+      requireNonNull(shouldComputeNext, "shouldComputeNext must be non null");
+      requireNonNull(computeNext, "computeNext must be non null");
+      Spliterator<T> spliterator =
+          new AbstractSpliterator<T>(Long.MAX_VALUE, 0) {
+            T prev;
+            boolean started = false;
+            boolean done = false;
+
+            @Override
+            public boolean tryAdvance(Consumer<? super T> action) {
+              // if we haven't started, emit our seed and return
+              if (!started) {
+                started = true;
+                action.accept(seed);
+                prev = seed;
+                return true;
+              }
+              // if we've previously finished quickly return
+              if (done) {
+                return false;
+              }
+              // test whether we should try and compute the next value
+              if (shouldComputeNext.test(prev)) {
+                // compute the next value and figure out if we can use it
+                T next = computeNext.apply(prev);
+                if (next != null) {
+                  action.accept(next);
+                  prev = next;
+                  return true;
+                }
+              }
+
+              // fallthrough, if we haven't taken an action by now consider the stream done and
+              // return
+              done = true;
+              return false;
+            }
+          };
+      return StreamSupport.stream(spliterator, false);
     }
   }
 
@@ -1076,6 +1186,23 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     return opts.writeObjectRequest().apply(requestBuilder).build();
   }
 
+  private UnbufferedReadableByteChannelSession<Object> unbufferedReadSession(
+      BlobId blob, BlobSourceOption[] options) {
+    Opts<ObjectSourceOpt> opts = Opts.unwrap(options).resolveFrom(blob);
+    ReadObjectRequest readObjectRequest = getReadObjectRequest(blob, opts);
+    Set<StatusCode.Code> codes =
+        GrpcStorageImpl.resultRetryAlgorithmToCodes(
+            retryAlgorithmManager.getFor(readObjectRequest));
+    GrpcCallContext grpcCallContext = GrpcCallContext.createDefault().withRetryableCodes(codes);
+    return ResumableMedia.gapic()
+        .read()
+        .byteChannel(storageClient.readObjectCallable().withDefaultCallContext(grpcCallContext))
+        .setAutoGzipDecompression(!opts.autoGzipDecompression())
+        .unbuffered()
+        .setReadObjectRequest(readObjectRequest)
+        .build();
+  }
+
   private FieldMask fieldMaskGenerator(Message message) {
     return FieldMask.newBuilder()
         .addAllPaths(
@@ -1085,4 +1212,58 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
                 .collect(Collectors.toList()))
         .build();
   }
+
+  /**
+   * When using the retry features of the Gapic client, we are only allowed to provide a {@link
+   * Set}{@code <}{@link StatusCode.Code}{@code >}. Given {@link StatusCode.Code} is an enum, we can
+   * resolve the set of values from a given {@link ResultRetryAlgorithm} by evaluating each one as
+   * an {@link ApiException}.
+   */
+  static Set<StatusCode.Code> resultRetryAlgorithmToCodes(ResultRetryAlgorithm<?> alg) {
+    return CODE_API_EXCEPTIONS.stream()
+        .filter(e -> alg.shouldRetry(e, null))
+        .map(e -> e.getStatusCode().getCode())
+        .collect(Collectors.toSet());
+  }
+
+  private static GrpcStatusCode statusCodeFor(StatusCode.Code code) {
+    switch (code) {
+      case OK:
+        return GrpcStatusCode.of(Code.OK);
+      case CANCELLED:
+        return GrpcStatusCode.of(Code.CANCELLED);
+      case UNKNOWN:
+        return GrpcStatusCode.of(Code.UNKNOWN);
+      case INVALID_ARGUMENT:
+        return GrpcStatusCode.of(Code.INVALID_ARGUMENT);
+      case DEADLINE_EXCEEDED:
+        return GrpcStatusCode.of(Code.DEADLINE_EXCEEDED);
+      case NOT_FOUND:
+        return GrpcStatusCode.of(Code.NOT_FOUND);
+      case ALREADY_EXISTS:
+        return GrpcStatusCode.of(Code.ALREADY_EXISTS);
+      case PERMISSION_DENIED:
+        return GrpcStatusCode.of(Code.PERMISSION_DENIED);
+      case RESOURCE_EXHAUSTED:
+        return GrpcStatusCode.of(Code.RESOURCE_EXHAUSTED);
+      case FAILED_PRECONDITION:
+        return GrpcStatusCode.of(Code.FAILED_PRECONDITION);
+      case ABORTED:
+        return GrpcStatusCode.of(Code.ABORTED);
+      case OUT_OF_RANGE:
+        return GrpcStatusCode.of(Code.OUT_OF_RANGE);
+      case UNIMPLEMENTED:
+        return GrpcStatusCode.of(Code.UNIMPLEMENTED);
+      case INTERNAL:
+        return GrpcStatusCode.of(Code.INTERNAL);
+      case UNAVAILABLE:
+        return GrpcStatusCode.of(Code.UNAVAILABLE);
+      case DATA_LOSS:
+        return GrpcStatusCode.of(Code.DATA_LOSS);
+      case UNAUTHENTICATED:
+        return GrpcStatusCode.of(Code.UNAUTHENTICATED);
+      default:
+        throw new IllegalStateException("Unrecognized status code: " + code);
+    }
+  }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
index 8ceef950..875231e3 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
@@ -21,9 +21,11 @@ import static java.util.Objects.requireNonNull;
 import com.google.api.core.ApiClock;
 import com.google.api.core.BetaApi;
 import com.google.api.core.InternalApi;
+import com.google.api.gax.core.CredentialsProvider;
 import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.api.gax.core.NoCredentialsProvider;
 import com.google.api.gax.retrying.RetrySettings;
-import com.google.api.gax.rpc.ClientContext;
+import com.google.api.gax.retrying.StreamResumptionStrategy;
 import com.google.api.gax.rpc.HeaderProvider;
 import com.google.auth.Credentials;
 import com.google.cloud.NoCredentials;
@@ -36,15 +38,21 @@ import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.spi.StorageRpcFactory;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableSet;
+import com.google.storage.v2.ReadObjectRequest;
+import com.google.storage.v2.ReadObjectResponse;
+import com.google.storage.v2.StorageClient;
 import com.google.storage.v2.StorageSettings;
-import com.google.storage.v2.stub.GrpcStorageStub;
+import io.grpc.ManagedChannelBuilder;
 import java.io.IOException;
 import java.net.URI;
 import java.util.Set;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 @BetaApi
 @TransportCompatibility(Transport.GRPC)
-public final class GrpcStorageOptions extends StorageOptions {
+public final class GrpcStorageOptions extends StorageOptions
+    implements Retrying.RetryingDependencies {
 
   private static final long serialVersionUID = 4165732727259088956L;
   private static final String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control";
@@ -54,12 +62,12 @@ public final class GrpcStorageOptions extends StorageOptions {
   private final GrpcRetryAlgorithmManager retryAlgorithmManager;
 
   @BetaApi
-  public GrpcStorageOptions(Builder builder, StorageDefaults serviceDefaults) {
+  public GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) {
     super(builder, serviceDefaults);
     this.retryAlgorithmManager =
         new GrpcRetryAlgorithmManager(
             MoreObjects.firstNonNull(
-                builder.storageRetryStrategy, defaults().getStorageRetryStrategy()));
+                builder.storageRetryStrategy, serviceDefaults.getStorageRetryStrategy()));
   }
 
   @Override
@@ -67,10 +75,60 @@ public final class GrpcStorageOptions extends StorageOptions {
     return SCOPES;
   }
 
+  @InternalApi
   GrpcRetryAlgorithmManager getRetryAlgorithmManager() {
     return retryAlgorithmManager;
   }
 
+  @InternalApi
+  StorageSettings getStorageSettings() throws IOException {
+    URI uri = URI.create(getHost());
+    String scheme = uri.getScheme();
+    int port = uri.getPort() > 0 ? uri.getPort() : scheme.equals("http") ? 80 : 443;
+    String endpoint = String.format("%s:%d", uri.getHost(), port);
+
+    CredentialsProvider credentialsProvider;
+    if (credentials instanceof NoCredentials) {
+      credentialsProvider = NoCredentialsProvider.create();
+    } else {
+      credentialsProvider = FixedCredentialsProvider.create(credentials);
+    }
+
+    StorageSettings.Builder builder =
+        StorageSettings.newBuilder()
+            .setEndpoint(endpoint)
+            .setCredentialsProvider(credentialsProvider);
+
+    if (scheme.equals("http")) {
+      builder.setTransportChannelProvider(
+          com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.newBuilder()
+              .setEndpoint(endpoint)
+              .setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
+              .build());
+    }
+    RetrySettings retrySettings = getRetrySettings();
+    RetrySettings attemptOnce = retrySettings.toBuilder().setMaxAttempts(1).build();
+    // all retries for unary methods are handled at a different level
+    builder.applyToAllUnaryMethods(
+        input -> {
+          input.setRetrySettings(attemptOnce);
+          return null;
+        });
+    // for ReadObject we are configuring the server stream handling to do its own retries, so wire
+    // things through. Retryable codes will be controlled closer to the use site as idempotency
+    // considerations need to be made.
+    builder
+        .readObjectSettings()
+        .setRetrySettings(retrySettings)
+        // even though we might want to default to the empty set for retryable codes, don't ever
+        // actually do this. Doing so prevents any retry capability from being wired into the stream
+        // pipeline, ever.
+        // For our use, we will always set it one way or the other to ensure it's appropriate
+        // DO NOT: .setRetryableCodes(Collections.emptySet())
+        .setResumptionStrategy(new ReadObjectResumptionStrategy());
+    return builder.build();
+  }
+
   @BetaApi
   @Override
   public GrpcStorageOptions.Builder toBuilder() {
@@ -290,41 +348,8 @@ public final class GrpcStorageOptions extends StorageOptions {
       if (options instanceof GrpcStorageOptions) {
         GrpcStorageOptions grpcStorageOptions = (GrpcStorageOptions) options;
         try {
-          URI uri = URI.create(options.getHost());
-          String h = uri.getHost();
-          int p = uri.getPort() > 0 ? uri.getPort() : uri.getScheme().equals("http") ? 80 : 443;
-          String hp = String.format("%s:%d", h, p);
-          StorageSettings.Builder builder = StorageSettings.newBuilder().setEndpoint(hp);
-          builder.setCredentialsProvider(FixedCredentialsProvider.create(options.getCredentials()));
-          if (!"storage.googleapis.com:443".equals(hp)) { // TODO: make this more formal
-            options =
-                options
-                    .toBuilder()
-                    .setProjectId(
-                        options.getProjectId() == null ? "local-project" : options.getProjectId())
-                    .setCredentials(NoCredentials.getInstance())
-                    .build();
-            builder
-                .setCredentialsProvider(com.google.api.gax.core.NoCredentialsProvider.create())
-                .setTransportChannelProvider(
-                    com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.newBuilder()
-                        .setEndpoint(hp)
-                        .setChannelConfigurator(c -> c.usePlaintext())
-                        .build());
-          }
-          final StorageOptions finalOpts = options;
-          StorageSettings settings =
-              builder
-                  .applyToAllUnaryMethods(
-                      input -> {
-                        input.setRetrySettings(finalOpts.getRetrySettings());
-                        return null;
-                      })
-                  .build();
-          ClientContext clientContext = ClientContext.create(settings);
-          GrpcStorageStub grpcStorageStub = GrpcStorageStub.create(clientContext);
-
-          return new GrpcStorageImpl(grpcStorageOptions, grpcStorageStub);
+          StorageSettings storageSettings = grpcStorageOptions.getStorageSettings();
+          return new GrpcStorageImpl(grpcStorageOptions, StorageClient.create(storageSettings));
         } catch (IOException e) {
           throw new IllegalStateException(
               "Unable to instantiate gRPC com.google.cloud.storage.Storage client.", e);
@@ -370,4 +395,38 @@ public final class GrpcStorageOptions extends StorageOptions {
       throw new IllegalStateException("No supported for grpc");
     }
   }
+
+  // TODO: See if we can change gax to allow shifting this to callable.withContext so it doesn't
+  //   have to be set globally
+  private static class ReadObjectResumptionStrategy
+      implements StreamResumptionStrategy<ReadObjectRequest, ReadObjectResponse> {
+    private long readOffset = 0;
+
+    @NonNull
+    @Override
+    public StreamResumptionStrategy<ReadObjectRequest, ReadObjectResponse> createNew() {
+      return new ReadObjectResumptionStrategy();
+    }
+
+    @NonNull
+    @Override
+    public ReadObjectResponse processResponse(ReadObjectResponse response) {
+      readOffset += response.getChecksummedData().getContent().size();
+      return response;
+    }
+
+    @Nullable
+    @Override
+    public ReadObjectRequest getResumeRequest(ReadObjectRequest originalRequest) {
+      if (readOffset != 0) {
+        return originalRequest.toBuilder().setReadOffset(readOffset).build();
+      }
+      return originalRequest;
+    }
+
+    @Override
+    public boolean canResume() {
+      return true;
+    }
+  }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GzipReadableByteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GzipReadableByteChannel.java
new file mode 100644
index 00000000..6ef048b3
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GzipReadableByteChannel.java
@@ -0,0 +1,171 @@
+/*
+ * 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;
+
+import com.google.cloud.storage.UnbufferedReadableByteChannelSession.UnbufferedReadableByteChannel;
+import com.google.storage.v2.Object;
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.ScatteringByteChannel;
+import java.util.concurrent.ExecutionException;
+import java.util.zip.GZIPInputStream;
+
+final class GzipReadableByteChannel implements UnbufferedReadableByteChannel {
+  private final GapicUnbufferedReadableByteChannel source;
+
+  private boolean retEOF = false;
+  private ScatteringByteChannel delegate;
+
+  GzipReadableByteChannel(GapicUnbufferedReadableByteChannel source) {
+    this.source = source;
+  }
+
+  @Override
+  public int read(ByteBuffer dst) throws IOException {
+    return Math.toIntExact(read(new ByteBuffer[] {dst}));
+  }
+
+  @Override
+  public long read(ByteBuffer[] dsts) throws IOException {
+    return read(dsts, 0, dsts.length);
+  }
+
+  @Override
+  public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
+    if (retEOF) {
+      return -1;
+    }
+    long bytesRead = 0;
+    // if our delegate is null, that means this is the first read attempt
+    if (delegate == null) {
+      // try to determine if the underlying data coming out of `source` is gzip
+      byte[] first4 = new byte[4]; // 4 bytes = 32-bits
+      final ByteBuffer wrap = ByteBuffer.wrap(first4);
+      // Step 1: initiate a read of the first 4 bytes of the object
+      //   this will have minimal overhead as the messages coming from gcs are inherently windowed
+      //   if the object size is between 5 and 2MiB the remaining bytes will be held in the channel
+      //   for later read.
+      source.read(wrap);
+      try {
+        // Step 2: wait for the object metadata, this is populated in the first message from GCS
+        Object object = source.getResult().get();
+        // if the Content-Encoding is gzip, Step 3: wire gzip decompression into the byte path
+        //   this will have a copy impact as we are no longer controlling all the buffers
+        if ("gzip".equals(object.getContentEncoding())) {
+          // to wire gzip decompression into the byte path:
+          // Create an input stream of the first4 bytes we already read
+          ByteArrayInputStream first4again = new ByteArrayInputStream(first4);
+          // Create an InputStream facade of source
+          InputStream sourceInputStream = Channels.newInputStream(source);
+          // create a new InputStream with the first4 bytes prepended to source
+          SequenceInputStream first4AndSource =
+              new SequenceInputStream(first4again, sourceInputStream);
+          // add gzip decompression
+          GZIPInputStream decompress =
+              new GZIPInputStream(new OptimisticAvailabilityInputStream(first4AndSource));
+          // create a channel from our GZIPInputStream
+          ReadableByteChannel decompressedChannel = Channels.newChannel(decompress);
+          // turn our ReadableByteChannel into a ScatteringByteChannel
+          delegate = StorageByteChannels.readable().asScatteringByteChannel(decompressedChannel);
+        } else {
+          // if content encoding isn't gzip, copy the bytes we read into the dsts and set delegate
+          // to source
+          wrap.flip();
+          bytesRead += Buffers.copy(wrap, dsts, offset, length);
+          delegate = source;
+        }
+      } catch (InterruptedException | ExecutionException e) {
+        throw new IOException(e);
+      }
+    }
+
+    // Because we're pre-reading a few bytes of the object in order to determine if we need to
+    // plumb in gzip decompress, there is the possibility we will reach EOF while probing.
+    // In order to maintain correctness of EOF propagation, determine if we will need to signal EOF
+    // upon the next read.
+    long read = delegate.read(dsts, offset, length);
+    if (read == -1 && bytesRead == 0) {
+      return -1;
+    } else if (read == -1) {
+      retEOF = true;
+    } else {
+      bytesRead += read;
+    }
+
+    return bytesRead;
+  }
+
+  @Override
+  public boolean isOpen() {
+    return !retEOF && source.isOpen();
+  }
+
+  @Override
+  public void close() throws IOException {
+    // leverage try-with-resource to handle the dance closing these two resources
+    try (AutoCloseable ignored1 = source;
+        AutoCloseable ignored2 = delegate) {
+      delegate = null;
+    } catch (Exception e) {
+      throw new IOException("Error while attempting to close channel.", e);
+    }
+  }
+
+  /**
+   * There is an edge-case in the JDK's {@link GZIPInputStream} where it will prematurely terminate
+   * reading from the underlying InputStream.
+   *
+   * <p>This class decorates an InputStream to be optimistic about the number of available bytes
+   * when reading data to encourage GzipInputStream to consume the entire stream.
+   *
+   * <p>For a more in-depth write up see <a target="_blank" rel="noopener noreferrer"
+   * href="https://github.com/googleapis/google-http-java-client/pull/1608">google-http-java-client/pull/1608</a>
+   *
+   * <p><i>NOTE</i> This class is a copy of the private class from {@code
+   * com.google.api.client.http.GzipSupport}. This class should not be made public, as it is not
+   * general purpose and so is reproduced here.
+   */
+  private static final class OptimisticAvailabilityInputStream extends FilterInputStream {
+    private int lastRead = 0;
+
+    OptimisticAvailabilityInputStream(InputStream delegate) {
+      super(delegate);
+    }
+
+    @Override
+    public int available() throws IOException {
+      return lastRead > -1 ? Integer.MAX_VALUE : 0;
+    }
+
+    @Override
+    public int read() throws IOException {
+      return lastRead = super.read();
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+      return lastRead = super.read(b);
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+      return lastRead = super.read(b, off, len);
+    }
+  }
+}
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 2c617abd..06fca041 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
@@ -22,17 +22,23 @@ import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.function.Supplier;
 import javax.annotation.concurrent.Immutable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 interface Hasher {
 
+  @Nullable
   default Crc32cLengthKnown hash(Supplier<ByteBuffer> b) {
     return hash(b.get());
   }
 
+  @Nullable
   Crc32cLengthKnown hash(ByteBuffer b);
 
   void validate(Crc32cValue<?> expected, Supplier<ByteBuffer> b) throws IOException;
 
+  @Nullable
+  Crc32cLengthKnown nullSafeConcat(Crc32cLengthKnown r1, Crc32cLengthKnown r2);
+
   static Hasher noop() {
     return NoOpHasher.INSTANCE;
   }
@@ -54,6 +60,11 @@ interface Hasher {
 
     @Override
     public void validate(Crc32cValue<?> expected, Supplier<ByteBuffer> b) {}
+
+    @Override
+    public @Nullable Crc32cLengthKnown nullSafeConcat(Crc32cLengthKnown r1, Crc32cLengthKnown r2) {
+      return null;
+    }
   }
 
   @Immutable
@@ -68,12 +79,25 @@ interface Hasher {
       return Crc32cValue.of(Hashing.crc32c().hashBytes(b).asInt(), remaining);
     }
 
+    @SuppressWarnings("ConstantConditions")
     @Override
     public void validate(Crc32cValue<?> expected, Supplier<ByteBuffer> b) throws IOException {
       Crc32cLengthKnown actual = hash(b);
-      if (actual.getValue() != expected.getValue()) {
+      if (!actual.eqValue(expected)) {
         throw new IOException(
-            String.format("Mismatch checksum value. Expected %s actual %s", expected, actual));
+            String.format(
+                "Mismatch checksum value. Expected %s actual %s",
+                expected.debugString(), actual.debugString()));
+      }
+    }
+
+    @Override
+    @Nullable
+    public Crc32cLengthKnown nullSafeConcat(Crc32cLengthKnown r1, Crc32cLengthKnown r2) {
+      if (r1 == null) {
+        return r2;
+      } else {
+        return r1.concat(r2);
       }
     }
   }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableWrite.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableWrite.java
index 17e599e0..b804ba4c 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableWrite.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableWrite.java
@@ -35,12 +35,11 @@ final class ResumableWrite implements WriteObjectRequestBuilderFactory {
   public ResumableWrite(StartResumableWriteRequest req, StartResumableWriteResponse res) {
     this.req = req;
     this.res = res;
-    this.writeRequest =
-        WriteObjectRequest.newBuilder()
-            .setUploadId(res.getUploadId())
-            .setCommonObjectRequestParams(req.getCommonObjectRequestParams())
-            // REVIEW: how does kms come into play here?
-            .build();
+    WriteObjectRequest.Builder b = WriteObjectRequest.newBuilder().setUploadId(res.getUploadId());
+    if (req.hasCommonObjectRequestParams()) {
+      b.setCommonObjectRequestParams(req.getCommonObjectRequestParams());
+    }
+    this.writeRequest = b.build();
   }
 
   public StartResumableWriteRequest getReq() {
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Retrying.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Retrying.java
index b1811a33..e529ee9f 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Retrying.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Retrying.java
@@ -71,7 +71,7 @@ final class Retrying {
    *
    * @param <T> The result type of {@code c}
    * @param <U> The result type of any mapping that takes place via {@code f}
-   * @param options The {@link HttpStorageOptions} which {@link RetrySettings} and {@link ApiClock}
+   * @param deps The {@link RetryingDependencies} which {@link RetrySettings} and {@link ApiClock}
    *     will be resolved from.
    * @param algorithm The {@link ResultRetryAlgorithm} to use when determining if a retry is
    *     possible
@@ -83,15 +83,26 @@ final class Retrying {
    * @throws StorageException if {@code c} fails due to any retry exhaustion
    */
   static <T, U> U run(
-      GrpcStorageOptions options,
+      RetryingDependencies deps,
       ResultRetryAlgorithm<?> algorithm,
       Callable<T> c,
       Decoder<T, U> f) {
     try {
-      T result = runWithRetries(c, options.getRetrySettings(), algorithm, options.getClock());
+      T result = runWithRetries(c, deps.getRetrySettings(), algorithm, deps.getClock());
       return result == null ? null : f.decode(result);
     } catch (RetryHelperException e) {
       throw StorageException.coalesce(e.getCause());
     }
   }
+
+  /**
+   * Rather than requiring a full set of {@link StorageOptions} to be passed specify what we
+   * actually need and have StorageOptions implement this interface.
+   */
+  interface RetryingDependencies {
+
+    RetrySettings getRetrySettings();
+
+    ApiClock getClock();
+  }
 }
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 e8a7978f..8c22ba6e 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
@@ -3471,7 +3471,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    *
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   HmacKey createHmacKey(ServiceAccount serviceAccount, CreateHmacKeyOption... options);
 
   /**
@@ -3523,7 +3523,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    *
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   HmacKeyMetadata getHmacKey(String accessId, GetHmacKeyOption... options);
 
   /**
@@ -3560,7 +3560,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    *
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   HmacKeyMetadata updateHmacKeyState(
       final HmacKeyMetadata hmacKeyMetadata,
       final HmacKey.HmacKeyState state,
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageByteChannels.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageByteChannels.java
index cd4410db..48857cf2 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageByteChannels.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageByteChannels.java
@@ -22,6 +22,9 @@ import com.google.cloud.storage.UnbufferedReadableByteChannelSession.UnbufferedR
 import com.google.cloud.storage.UnbufferedWritableByteChannelSession.UnbufferedWritableByteChannel;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.ScatteringByteChannel;
 
 final class StorageByteChannels {
 
@@ -46,6 +49,10 @@ final class StorageByteChannels {
         UnbufferedReadableByteChannel delegate) {
       return new SynchronizedUnbufferedReadableByteChannel(delegate);
     }
+
+    public ScatteringByteChannel asScatteringByteChannel(ReadableByteChannel c) {
+      return new ScatteringByteChannelFacade(c);
+    }
   }
 
   static final class Writable {
@@ -187,4 +194,57 @@ final class StorageByteChannels {
       delegate.close();
     }
   }
+
+  private static final class ScatteringByteChannelFacade implements ScatteringByteChannel {
+    private final ReadableByteChannel c;
+
+    private ScatteringByteChannelFacade(final ReadableByteChannel c) {
+      this.c = c;
+    }
+
+    @Override
+    public int read(ByteBuffer dst) throws IOException {
+      return Math.toIntExact(read(new ByteBuffer[] {dst}, 0, 1));
+    }
+
+    @Override
+    public long read(ByteBuffer[] dsts) throws IOException {
+      return read(dsts, 0, dsts.length);
+    }
+
+    @Override
+    public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
+      if (!c.isOpen()) {
+        throw new ClosedChannelException();
+      }
+
+      long totalBytesRead = 0;
+      for (int i = offset; i < length; i++) {
+        ByteBuffer dst = dsts[i];
+        if (dst.hasRemaining()) {
+          int read = c.read(dst);
+          if (read == -1) {
+            if (totalBytesRead == 0) {
+              c.close();
+              return -1;
+            } else {
+              break;
+            }
+          }
+          totalBytesRead += read;
+        }
+      }
+      return totalBytesRead;
+    }
+
+    @Override
+    public boolean isOpen() {
+      return c.isOpen();
+    }
+
+    @Override
+    public void close() throws IOException {
+      c.close();
+    }
+  }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java
index 07207b07..708de195 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java
@@ -88,7 +88,11 @@ public final class StorageException extends BaseHttpServiceException {
   }
 
   private static StorageException getStorageException(Throwable t) {
-    return new StorageException(UNKNOWN_CODE, t.getMessage(), t.getCause());
+    // unwrap a RetryHelperException if that is what is being translated
+    if (t instanceof RetryHelperException) {
+      return new StorageException(UNKNOWN_CODE, t.getMessage(), t.getCause());
+    }
+    return new StorageException(UNKNOWN_CODE, t.getMessage(), t);
   }
 
   /**
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 32b2e42b..ddfae852 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
@@ -57,7 +57,6 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Locale;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
@@ -1104,7 +1103,7 @@ final class UnifiedOpts {
    */
   @Deprecated
   static final class ProjectId extends RpcOptVal<String>
-      implements HmacKeySourceOpt, HmacKeyTargetOpt, HmacKeyListOpt {
+      implements HmacKeySourceOpt, HmacKeyTargetOpt, HmacKeyListOpt, BucketListOpt {
     private static final long serialVersionUID = 1471462503030451598L;
 
     private ProjectId(String val) {
@@ -1122,9 +1121,14 @@ final class UnifiedOpts {
     }
 
     @Override
-    public Mapper<DeleteHmacKeyRequest.Builder> deleteHmacKey() {
+    public Mapper<CreateHmacKeyRequest.Builder> createHmacKey() {
       return b -> b.setProject(projectNameCodec.encode(val));
     }
+
+    @Override
+    public Mapper<ListBucketsRequest.Builder> listBuckets() {
+      return b -> b.setParent(projectNameCodec.encode(val));
+    }
   }
 
   static final class Projection extends RpcOptVal<String> implements BucketTargetOpt {
@@ -1224,6 +1228,7 @@ final class UnifiedOpts {
     }
   }
 
+  @Deprecated
   static final class ReturnRawInputStream extends RpcOptVal<@NonNull Boolean>
       implements ObjectSourceOpt {
     private static final long serialVersionUID = 505293506385742781L;
@@ -1958,9 +1963,16 @@ final class UnifiedOpts {
       return fuseMappers(ObjectTargetOpt.class, ObjectTargetOpt::blobInfo);
     }
 
+    /**
+     * Here for compatibility. This should NOT be an "Opt" instead an attribute of the channel
+     * builder. When {@link ReturnRawInputStream} is removed, this method should be removed as well.
+     *
+     * @see
+     *     GapicDownloadSessionBuilder.ReadableByteChannelSessionBuilder#setAutoGzipDecompression(boolean)
+     */
     @Deprecated
-    Optional<ProjectId> projectId() {
-      return filterTo(ProjectId.class).findFirst();
+    boolean autoGzipDecompression() {
+      return filterTo(ReturnRawInputStream.class).findFirst().map(r -> r.val).orElse(false);
     }
 
     private Mapper<ImmutableMap.Builder<StorageRpc.Option, Object>> rpcOptionMapper() {
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java
index 1104c18b..8dfe99f2 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java
@@ -49,24 +49,24 @@ final class WriteFlushStrategy {
    * Create a {@link Flusher} which will "fsync" every time {@link Flusher#flush(List)} is called
    * along with {@link Flusher#close(WriteObjectRequest)}.
    */
-  static Flusher fsyncEveryFlush(
-      ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write,
-      LongConsumer committedTotalBytesCallback,
-      Consumer<WriteObjectResponse> onSuccessCallback) {
-    return new FsyncEveryFlusher(write, committedTotalBytesCallback, onSuccessCallback);
+  static FlusherFactory fsyncEveryFlush(
+      ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write) {
+    return (LongConsumer committedTotalBytesCallback,
+        Consumer<WriteObjectResponse> onSuccessCallback) ->
+        new FsyncEveryFlusher(write, committedTotalBytesCallback, onSuccessCallback);
   }
 
   /**
    * Create a {@link Flusher} which will "fsync" only on {@link Flusher#close(WriteObjectRequest)}.
    * Calls to {@link Flusher#flush(List)} will be sent but not synced.
    *
-   * @see FlusherFactory#newFlusher(ClientStreamingCallable, LongConsumer, Consumer)
+   * @see FlusherFactory#newFlusher(LongConsumer, Consumer)
    */
-  static Flusher fsyncOnClose(
-      ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write,
-      LongConsumer committedTotalBytesCallback,
-      Consumer<WriteObjectResponse> onSuccessCallback) {
-    return new FsyncOnClose(write, committedTotalBytesCallback, onSuccessCallback);
+  static FlusherFactory fsyncOnClose(
+      ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write) {
+    return (LongConsumer committedTotalBytesCallback,
+        Consumer<WriteObjectResponse> onSuccessCallback) ->
+        new FsyncOnClose(write, committedTotalBytesCallback, onSuccessCallback);
   }
 
   @FunctionalInterface
@@ -77,9 +77,7 @@ final class WriteFlushStrategy {
      * @param onSuccessCallback Callback to signal success, and provide the final response.
      */
     Flusher newFlusher(
-        ClientStreamingCallable<WriteObjectRequest, WriteObjectResponse> write,
-        LongConsumer committedTotalBytesCallback,
-        Consumer<WriteObjectResponse> onSuccessCallback);
+        LongConsumer committedTotalBytesCallback, Consumer<WriteObjectResponse> onSuccessCallback);
   }
 
   interface Flusher {
@@ -201,9 +199,7 @@ final class WriteFlushStrategy {
       if (value.hasPersistedSize()) {
         sizeCallback.accept(value.getPersistedSize());
       } else if (value.hasResource()) {
-        // testbench_seems to return this even on finish
-      } else {
-        // serverState.confirmedBytes.set(serverState.totalSentBytes.get());
+        sizeCallback.accept(value.getResource().getSize());
       }
       last = value;
     }


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/AutoClosableFixture.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/AutoClosableFixture.java
new file mode 100644
index 00000000..01f0d425
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/AutoClosableFixture.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+import com.google.common.base.Preconditions;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+final class AutoClosableFixture<@NonNull T extends AutoCloseable> implements TestRule {
+  private final ThrowingSupplier<T> supplier;
+
+  @Nullable private T instance;
+
+  private AutoClosableFixture(ThrowingSupplier<T> supplier) {
+    this.supplier = supplier;
+  }
+
+  @NonNull
+  public T getInstance() {
+    Preconditions.checkState(instance != null, "getInstance() called outside active lifecycle.");
+    return instance;
+  }
+
+  @Override
+  public Statement apply(Statement base, Description description) {
+    return new Statement() {
+      @Override
+      public void evaluate() throws Throwable {
+        try (T t = supplier.get()) {
+          instance = t;
+          base.evaluate();
+        } finally {
+          instance = null;
+        }
+      }
+    };
+  }
+
+  static <@NonNull T extends AutoCloseable> AutoClosableFixture<T> of(
+      ThrowingSupplier<T> supplier) {
+    return new AutoClosableFixture<>(supplier);
+  }
+
+  @FunctionalInterface
+  interface ThrowingSupplier<@NonNull T> {
+    T get() throws Exception;
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ChunkSegmenterTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ChunkSegmenterTest.java
index 7da2f705..9a1a6a03 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ChunkSegmenterTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ChunkSegmenterTest.java
@@ -41,8 +41,8 @@ final class ChunkSegmenterTest {
     System.out.println("td = " + td);
 
     ChunkSegment[] data =
-        ChunkSegmenter.segmentBuffers(
-            td.buffers, Hasher.noop(), ByteStringStrategy.noCopy(), td.chunkSize);
+        new ChunkSegmenter(Hasher.noop(), ByteStringStrategy.noCopy(), td.chunkSize)
+            .segmentBuffers(td.buffers);
 
     long dataTotalSize = Arrays.stream(data).mapToLong(d -> d.getB().size()).sum();
     Optional<Crc32cLengthKnown> reduce =
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/FakeServer.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/FakeServer.java
new file mode 100644
index 00000000..d2778342
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/FakeServer.java
@@ -0,0 +1,64 @@
+/*
+ * 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;
+
+import com.google.cloud.NoCredentials;
+import com.google.storage.v2.StorageGrpc;
+import com.google.storage.v2.StorageSettings;
+import io.grpc.Server;
+import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.TimeUnit;
+
+final class FakeServer implements AutoCloseable {
+
+  private final Server server;
+  private final GrpcStorageOptions grpcStorageOptions;
+
+  FakeServer(Server server, GrpcStorageOptions grpcStorageOptions) {
+    this.server = server;
+    this.grpcStorageOptions = grpcStorageOptions;
+  }
+
+  GrpcStorageOptions getGrpcStorageOptions() {
+    return grpcStorageOptions;
+  }
+
+  StorageSettings storageSettings() throws IOException {
+    return grpcStorageOptions.getStorageSettings();
+  }
+
+  @Override
+  public void close() throws InterruptedException {
+    server.shutdownNow().awaitTermination(10, TimeUnit.SECONDS);
+  }
+
+  static FakeServer of(StorageGrpc.StorageImplBase service) throws IOException {
+    InetSocketAddress address = new InetSocketAddress("localhost", 0);
+    Server server = NettyServerBuilder.forAddress(address).addService(service).build();
+    server.start();
+    String endpoint = String.format("%s:%d", address.getHostString(), server.getPort());
+    GrpcStorageOptions grpcStorageOptions =
+        StorageOptions.grpc()
+            .setHost("http://" + endpoint)
+            .setProjectId("test-proj")
+            .setCredentials(NoCredentials.getInstance())
+            .build();
+    return new FakeServer(server, grpcStorageOptions);
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/GapicUnbufferedReadableByteChannelTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/GapicUnbufferedReadableByteChannelTest.java
new file mode 100644
index 00000000..27603447
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/GapicUnbufferedReadableByteChannelTest.java
@@ -0,0 +1,354 @@
+/*
+ * 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;
+
+import static com.google.cloud.storage.TestUtils.apiException;
+import static com.google.cloud.storage.TestUtils.contextWithRetryForCodes;
+import static com.google.cloud.storage.TestUtils.getChecksummedData;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.api.core.ApiFutures;
+import com.google.api.gax.rpc.StatusCode;
+import com.google.cloud.storage.ChannelSession.UnbufferedReadSession;
+import com.google.cloud.storage.UnbufferedReadableByteChannelSession.UnbufferedReadableByteChannel;
+import com.google.common.collect.ImmutableList;
+import com.google.common.hash.Hashing;
+import com.google.protobuf.ByteString;
+import com.google.storage.v2.ContentRange;
+import com.google.storage.v2.Object;
+import com.google.storage.v2.ObjectChecksums;
+import com.google.storage.v2.ReadObjectRequest;
+import com.google.storage.v2.ReadObjectResponse;
+import com.google.storage.v2.StorageClient;
+import com.google.storage.v2.StorageGrpc;
+import io.grpc.Status.Code;
+import io.grpc.stub.StreamObserver;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Test;
+
+public final class GapicUnbufferedReadableByteChannelTest {
+  private final byte[] bytes = DataGenerator.base64Characters().genBytes(40);
+  private final ByteString data1 = ByteString.copyFrom(bytes, 0, 10);
+  private final ByteString data2 = ByteString.copyFrom(bytes, 10, 10);
+  private final ByteString data3 = ByteString.copyFrom(bytes, 20, 10);
+  private final ByteString data4 = ByteString.copyFrom(bytes, 30, 10);
+
+  private final String objectName = "name";
+  private final Object expectedResult =
+      Object.newBuilder()
+          .setName(objectName)
+          .setGeneration(3L)
+          .setContentType("application/octet-stream")
+          .build();
+
+  private final ReadObjectRequest req1 =
+      ReadObjectRequest.newBuilder().setObject(objectName).setReadOffset(0).build();
+  private final ReadObjectRequest req2 = req1.toBuilder().setReadOffset(20).build();
+  private final ReadObjectResponse resp1 =
+      ReadObjectResponse.newBuilder()
+          .setMetadata(expectedResult)
+          .setContentRange(ContentRange.newBuilder().setStart(0).build())
+          .setObjectChecksums(
+              ObjectChecksums.newBuilder().setCrc32C(Hashing.crc32c().hashBytes(bytes).asInt()))
+          .setChecksummedData(getChecksummedData(data1, Hasher.enabled()))
+          .build();
+  private final ReadObjectResponse resp2 =
+      ReadObjectResponse.newBuilder()
+          .setChecksummedData(getChecksummedData(data2, Hasher.enabled()))
+          .build();
+  private final ReadObjectResponse resp3 =
+      ReadObjectResponse.newBuilder()
+          .setMetadata(expectedResult)
+          .setContentRange(ContentRange.newBuilder().setStart(20).build())
+          .setObjectChecksums(
+              ObjectChecksums.newBuilder().setCrc32C(Hashing.crc32c().hashBytes(bytes).asInt()))
+          .setChecksummedData(getChecksummedData(data3, Hasher.enabled()))
+          .build();
+  private final ReadObjectResponse resp4 =
+      ReadObjectResponse.newBuilder()
+          .setChecksummedData(getChecksummedData(data4, Hasher.enabled()))
+          .build();
+
+  /** Define a Storage service that will always return an error during the first readObject */
+  private final StorageGrpc.StorageImplBase fakeStorage =
+      new StorageGrpc.StorageImplBase() {
+        @Override
+        public void readObject(
+            ReadObjectRequest request, StreamObserver<ReadObjectResponse> responseObserver) {
+          if (request.equals(req1)) {
+            responseObserver.onNext(resp1);
+            responseObserver.onNext(resp2);
+            responseObserver.onError(apiException(Code.DATA_LOSS));
+          } else if (request.equals(req2)) {
+            responseObserver.onNext(resp3);
+            responseObserver.onNext(resp4);
+            responseObserver.onCompleted();
+          } else {
+            responseObserver.onError(apiException(Code.PERMISSION_DENIED));
+          }
+        }
+      };
+
+  @Test
+  public void readRetriesAreProperlyOrdered_readLargerThanMessageSize()
+      throws IOException, ExecutionException, InterruptedException, TimeoutException {
+    try (FakeServer server = FakeServer.of(fakeStorage);
+        StorageClient storageClient = StorageClient.create(server.storageSettings())) {
+
+      UnbufferedReadableByteChannelSession<Object> session =
+          new UnbufferedReadSession<>(
+              ApiFutures.immediateFuture(req1),
+              (start, resultFuture) ->
+                  new GapicUnbufferedReadableByteChannel(
+                      resultFuture,
+                      storageClient
+                          .readObjectCallable()
+                          .withDefaultCallContext(
+                              contextWithRetryForCodes(StatusCode.Code.DATA_LOSS)),
+                      start,
+                      Hasher.noop()));
+      byte[] actualBytes = new byte[40];
+      try (UnbufferedReadableByteChannel c = session.open()) {
+        c.read(ByteBuffer.wrap(actualBytes));
+      }
+      Object actualResult = session.getResult().get(1000, TimeUnit.MILLISECONDS);
+      assertThat(actualResult).isEqualTo(expectedResult);
+      assertThat(actualBytes).isEqualTo(bytes);
+    }
+  }
+
+  @Test
+  public void readRetriesAreProperlyOrdered_readSmallerThanMessageSize()
+      throws IOException, ExecutionException, InterruptedException, TimeoutException {
+    try (FakeServer server = FakeServer.of(fakeStorage);
+        StorageClient storageClient = StorageClient.create(server.storageSettings())) {
+
+      UnbufferedReadableByteChannelSession<Object> session =
+          new UnbufferedReadSession<>(
+              ApiFutures.immediateFuture(req1),
+              (start, resultFuture) ->
+                  new GapicUnbufferedReadableByteChannel(
+                      resultFuture,
+                      storageClient
+                          .readObjectCallable()
+                          .withDefaultCallContext(
+                              contextWithRetryForCodes(StatusCode.Code.DATA_LOSS)),
+                      start,
+                      Hasher.noop()));
+      byte[] actualBytes = new byte[40];
+      ImmutableList<ByteBuffer> buffers = TestUtils.subDivide(actualBytes, 2);
+      try (UnbufferedReadableByteChannel c = session.open()) {
+        for (ByteBuffer buf : buffers) {
+          c.read(buf);
+        }
+      }
+      Object actualResult = session.getResult().get(1000, TimeUnit.MILLISECONDS);
+      assertThat(actualResult).isEqualTo(expectedResult);
+      assertThat(actualBytes).isEqualTo(bytes);
+    }
+  }
+
+  @Test
+  public void ioException_if_generation_changes() throws IOException, InterruptedException {
+    StorageGrpc.StorageImplBase fakeStorage =
+        new StorageGrpc.StorageImplBase() {
+
+          final AtomicInteger invocationCount = new AtomicInteger(0);
+
+          @Override
+          public void readObject(
+              ReadObjectRequest request, StreamObserver<ReadObjectResponse> responseObserver) {
+            int count = invocationCount.getAndIncrement();
+            if (request.equals(req1)) {
+              if (count == 0) {
+                responseObserver.onNext(resp1);
+                responseObserver.onNext(resp2);
+                responseObserver.onError(apiException(Code.DATA_LOSS));
+              }
+            } else if (request.equals(req2)) {
+              ReadObjectResponse.Builder builder = resp3.toBuilder();
+              // increment the generation, as if it had been updated between initial read and retry
+              builder.getMetadataBuilder().setGeneration(expectedResult.getGeneration() + 1);
+              responseObserver.onNext(builder.build());
+              responseObserver.onNext(resp4);
+              responseObserver.onCompleted();
+            } else {
+              responseObserver.onError(apiException(Code.PERMISSION_DENIED));
+            }
+          }
+        };
+
+    try (FakeServer server = FakeServer.of(fakeStorage);
+        StorageClient storageClient = StorageClient.create(server.storageSettings())) {
+
+      UnbufferedReadableByteChannelSession<Object> session =
+          new UnbufferedReadSession<>(
+              ApiFutures.immediateFuture(req1),
+              (start, resultFuture) ->
+                  new GapicUnbufferedReadableByteChannel(
+                      resultFuture,
+                      storageClient
+                          .readObjectCallable()
+                          .withDefaultCallContext(
+                              contextWithRetryForCodes(StatusCode.Code.DATA_LOSS)),
+                      start,
+                      Hasher.noop()));
+      byte[] actualBytes = new byte[40];
+      try (UnbufferedReadableByteChannel c = session.open()) {
+        IOException ioException =
+            assertThrows(IOException.class, () -> c.read(ByteBuffer.wrap(actualBytes)));
+
+        assertThat(ioException).hasMessageThat().containsMatch(".*Generation.*3.*4.*");
+      }
+    }
+  }
+
+  @Test
+  public void ioException_if_crc32c_mismatch_individual_message()
+      throws IOException, InterruptedException {
+    StorageGrpc.StorageImplBase fakeStorage =
+        new StorageGrpc.StorageImplBase() {
+          @Override
+          public void readObject(
+              ReadObjectRequest request, StreamObserver<ReadObjectResponse> responseObserver) {
+            if (request.equals(req1)) {
+              responseObserver.onNext(resp1);
+              ReadObjectResponse.Builder b = resp2.toBuilder();
+              // set a bad checksum value
+              b.getChecksummedDataBuilder().setCrc32C(1);
+              responseObserver.onNext(b.build());
+              responseObserver.onNext(resp3);
+              responseObserver.onNext(resp4);
+              responseObserver.onCompleted();
+            } else {
+              responseObserver.onError(apiException(Code.PERMISSION_DENIED));
+            }
+          }
+        };
+    try (FakeServer server = FakeServer.of(fakeStorage);
+        StorageClient storageClient = StorageClient.create(server.storageSettings())) {
+
+      UnbufferedReadableByteChannelSession<Object> session =
+          new UnbufferedReadSession<>(
+              ApiFutures.immediateFuture(req1),
+              (start, resultFuture) ->
+                  new GapicUnbufferedReadableByteChannel(
+                      resultFuture,
+                      storageClient
+                          .readObjectCallable()
+                          .withDefaultCallContext(
+                              contextWithRetryForCodes(StatusCode.Code.DATA_LOSS)),
+                      start,
+                      Hasher.enabled()));
+      byte[] actualBytes = new byte[40];
+      try (UnbufferedReadableByteChannel c = session.open()) {
+        IOException ioException =
+            assertThrows(IOException.class, () -> c.read(ByteBuffer.wrap(actualBytes)));
+
+        assertThat(ioException).hasMessageThat().contains("Mismatch checksum");
+      }
+    }
+  }
+
+  @Test
+  public void ioException_if_crc32c_mismatch_cumulative() throws IOException, InterruptedException {
+    StorageGrpc.StorageImplBase fakeStorage =
+        new StorageGrpc.StorageImplBase() {
+          @Override
+          public void readObject(
+              ReadObjectRequest request, StreamObserver<ReadObjectResponse> responseObserver) {
+            if (request.equals(req1)) {
+              responseObserver.onNext(resp1);
+              responseObserver.onCompleted();
+            } else {
+              responseObserver.onError(apiException(Code.PERMISSION_DENIED));
+            }
+          }
+        };
+    try (FakeServer server = FakeServer.of(fakeStorage);
+        StorageClient storageClient = StorageClient.create(server.storageSettings())) {
+
+      UnbufferedReadableByteChannelSession<Object> session =
+          new UnbufferedReadSession<>(
+              ApiFutures.immediateFuture(req1),
+              (start, resultFuture) ->
+                  new GapicUnbufferedReadableByteChannel(
+                      resultFuture,
+                      storageClient
+                          .readObjectCallable()
+                          .withDefaultCallContext(
+                              contextWithRetryForCodes(StatusCode.Code.DATA_LOSS)),
+                      start,
+                      Hasher.enabled()));
+      byte[] actualBytes = new byte[40];
+      //noinspection resource
+      UnbufferedReadableByteChannel c = session.open();
+      c.read(ByteBuffer.wrap(actualBytes));
+
+      IOException ioException = assertThrows(IOException.class, c::close);
+      assertThat(ioException).hasMessageThat().contains("Mismatch checksum");
+    }
+  }
+
+  @Test
+  public void overRead_handledProperly() throws IOException, InterruptedException {
+    StorageGrpc.StorageImplBase fakeStorage =
+        new StorageGrpc.StorageImplBase() {
+          @Override
+          public void readObject(
+              ReadObjectRequest request, StreamObserver<ReadObjectResponse> responseObserver) {
+            responseObserver.onNext(resp1);
+            responseObserver.onNext(resp2);
+            responseObserver.onNext(resp3);
+            responseObserver.onNext(resp4);
+            responseObserver.onCompleted();
+          }
+        };
+    try (FakeServer server = FakeServer.of(fakeStorage);
+        StorageClient storageClient = StorageClient.create(server.storageSettings())) {
+
+      UnbufferedReadableByteChannelSession<Object> session =
+          new UnbufferedReadSession<>(
+              ApiFutures.immediateFuture(req1),
+              (start, resultFuture) ->
+                  new GapicUnbufferedReadableByteChannel(
+                      resultFuture,
+                      storageClient
+                          .readObjectCallable()
+                          .withDefaultCallContext(
+                              contextWithRetryForCodes(StatusCode.Code.DATA_LOSS)),
+                      start,
+                      Hasher.enabled()));
+      byte[] actualBytes = new byte[41];
+      //noinspection resource
+      UnbufferedReadableByteChannel c = session.open();
+      ByteBuffer buf = ByteBuffer.wrap(actualBytes);
+      int read1 = c.read(buf);
+      assertThat(read1).isAtLeast(1);
+      int read2 = c.read(buf);
+      assertThat(read2).isEqualTo(-1);
+      assertThrows(ClosedChannelException.class, () -> c.read(buf));
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannelTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannelTest.java
new file mode 100644
index 00000000..bc32ccea
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannelTest.java
@@ -0,0 +1,237 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.api.core.SettableApiFuture;
+import com.google.cloud.storage.WriteCtx.WriteObjectRequestBuilderFactory;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.ByteString;
+import com.google.storage.v2.Object;
+import com.google.storage.v2.StartResumableWriteRequest;
+import com.google.storage.v2.StartResumableWriteResponse;
+import com.google.storage.v2.StorageClient;
+import com.google.storage.v2.StorageGrpc.StorageImplBase;
+import com.google.storage.v2.WriteObjectRequest;
+import com.google.storage.v2.WriteObjectResponse;
+import com.google.storage.v2.WriteObjectSpec;
+import io.grpc.Status.Code;
+import io.grpc.stub.CallStreamObserver;
+import io.grpc.stub.StreamObserver;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+
+public final class GapicUnbufferedWritableByteChannelTest {
+
+  ChunkSegmenter segmenter = new ChunkSegmenter(Hasher.noop(), ByteStringStrategy.copy(), 10, 5);
+
+  @Test
+  public void directUpload() throws IOException, InterruptedException, ExecutionException {
+    Object obj = Object.newBuilder().setBucket("buck").setName("obj").build();
+    WriteObjectSpec spec = WriteObjectSpec.newBuilder().setResource(obj).build();
+
+    byte[] bytes = DataGenerator.base64Characters().genBytes(40);
+    WriteObjectRequest req1 =
+        WriteObjectRequest.newBuilder()
+            .setWriteObjectSpec(spec)
+            .setChecksummedData(TestUtils.getChecksummedData(ByteString.copyFrom(bytes, 0, 10)))
+            .build();
+    WriteObjectRequest req2 =
+        WriteObjectRequest.newBuilder()
+            .setWriteOffset(10)
+            .setChecksummedData(TestUtils.getChecksummedData(ByteString.copyFrom(bytes, 10, 10)))
+            .build();
+    WriteObjectRequest req3 =
+        WriteObjectRequest.newBuilder()
+            .setWriteOffset(20)
+            .setChecksummedData(TestUtils.getChecksummedData(ByteString.copyFrom(bytes, 20, 10)))
+            .build();
+    WriteObjectRequest req4 =
+        WriteObjectRequest.newBuilder()
+            .setWriteOffset(30)
+            .setChecksummedData(TestUtils.getChecksummedData(ByteString.copyFrom(bytes, 30, 10)))
+            .build();
+    WriteObjectRequest req5 =
+        WriteObjectRequest.newBuilder().setWriteOffset(40).setFinishWrite(true).build();
+
+    WriteObjectResponse resp =
+        WriteObjectResponse.newBuilder().setResource(obj.toBuilder().setSize(40)).build();
+
+    WriteObjectRequest base = WriteObjectRequest.newBuilder().setWriteObjectSpec(spec).build();
+    WriteObjectRequestBuilderFactory reqFactory = base::toBuilder;
+
+    StorageImplBase service =
+        new DirectWriteService(
+            ImmutableMap.of(ImmutableList.of(req1, req2, req3, req4, req5), resp));
+    try (FakeServer fake = FakeServer.of(service);
+        StorageClient sc = StorageClient.create(fake.storageSettings())) {
+      SettableApiFuture<WriteObjectResponse> result = SettableApiFuture.create();
+      try (GapicUnbufferedWritableByteChannel<?> c =
+          new GapicUnbufferedWritableByteChannel<>(
+              result,
+              segmenter,
+              reqFactory,
+              WriteFlushStrategy.fsyncOnClose(sc.writeObjectCallable()))) {
+        c.write(ByteBuffer.wrap(bytes));
+      }
+      assertThat(result.get()).isEqualTo(resp);
+    }
+  }
+
+  @Test
+  public void resumableUpload() throws IOException, InterruptedException, ExecutionException {
+    String uploadId = "upload-id";
+
+    Object obj = Object.newBuilder().setBucket("buck").setName("obj").build();
+    WriteObjectSpec spec = WriteObjectSpec.newBuilder().setResource(obj).build();
+
+    StartResumableWriteRequest startReq =
+        StartResumableWriteRequest.newBuilder().setWriteObjectSpec(spec).build();
+    StartResumableWriteResponse startResp =
+        StartResumableWriteResponse.newBuilder().setUploadId(uploadId).build();
+
+    byte[] bytes = DataGenerator.base64Characters().genBytes(40);
+    WriteObjectRequest req1 =
+        WriteObjectRequest.newBuilder()
+            .setUploadId(uploadId)
+            .setChecksummedData(TestUtils.getChecksummedData(ByteString.copyFrom(bytes, 0, 10)))
+            .build();
+    WriteObjectRequest req2 =
+        WriteObjectRequest.newBuilder()
+            .setUploadId(uploadId)
+            .setWriteOffset(10)
+            .setChecksummedData(TestUtils.getChecksummedData(ByteString.copyFrom(bytes, 10, 10)))
+            .build();
+    WriteObjectRequest req3 =
+        WriteObjectRequest.newBuilder()
+            .setUploadId(uploadId)
+            .setWriteOffset(20)
+            .setChecksummedData(TestUtils.getChecksummedData(ByteString.copyFrom(bytes, 20, 10)))
+            .build();
+    WriteObjectRequest req4 =
+        WriteObjectRequest.newBuilder()
+            .setUploadId(uploadId)
+            .setWriteOffset(30)
+            .setChecksummedData(TestUtils.getChecksummedData(ByteString.copyFrom(bytes, 30, 10)))
+            .build();
+    WriteObjectRequest req5 =
+        WriteObjectRequest.newBuilder()
+            .setUploadId(uploadId)
+            .setWriteOffset(40)
+            .setFinishWrite(true)
+            .build();
+
+    WriteObjectResponse resp1 = WriteObjectResponse.newBuilder().setPersistedSize(10).build();
+    WriteObjectResponse resp2 = WriteObjectResponse.newBuilder().setPersistedSize(20).build();
+    WriteObjectResponse resp3 = WriteObjectResponse.newBuilder().setPersistedSize(30).build();
+    WriteObjectResponse resp4 = WriteObjectResponse.newBuilder().setPersistedSize(40).build();
+    WriteObjectResponse resp5 =
+        WriteObjectResponse.newBuilder().setResource(obj.toBuilder().setSize(40)).build();
+
+    WriteObjectRequestBuilderFactory reqFactory = new ResumableWrite(startReq, startResp);
+
+    ImmutableMap<List<WriteObjectRequest>, WriteObjectResponse> writes =
+        ImmutableMap.<List<WriteObjectRequest>, WriteObjectResponse>builder()
+            .put(ImmutableList.of(req1), resp1)
+            .put(ImmutableList.of(req2), resp2)
+            .put(ImmutableList.of(req3), resp3)
+            .put(ImmutableList.of(req4), resp4)
+            .put(ImmutableList.of(req5), resp5)
+            .build();
+    StorageImplBase service = new DirectWriteService(writes);
+    try (FakeServer fake = FakeServer.of(service);
+        StorageClient sc = StorageClient.create(fake.storageSettings())) {
+      SettableApiFuture<WriteObjectResponse> result = SettableApiFuture.create();
+      try (GapicUnbufferedWritableByteChannel<?> c =
+          new GapicUnbufferedWritableByteChannel<>(
+              result,
+              segmenter,
+              reqFactory,
+              WriteFlushStrategy.fsyncEveryFlush(sc.writeObjectCallable()))) {
+        ImmutableList<ByteBuffer> buffers = TestUtils.subDivide(bytes, 10);
+        for (ByteBuffer buf : buffers) {
+          c.write(buf);
+        }
+      }
+      assertThat(result.get()).isEqualTo(resp5);
+    }
+  }
+
+  private static class DirectWriteService extends StorageImplBase {
+    private final ImmutableMap<List<WriteObjectRequest>, WriteObjectResponse> writes;
+
+    private ImmutableList.Builder<WriteObjectRequest> requests;
+
+    private DirectWriteService(ImmutableMap<List<WriteObjectRequest>, WriteObjectResponse> writes) {
+      this.writes = writes;
+      this.requests = new ImmutableList.Builder<>();
+    }
+
+    @Override
+    public StreamObserver<WriteObjectRequest> writeObject(StreamObserver<WriteObjectResponse> obs) {
+      return new Adapter() {
+        @Override
+        public void onNext(WriteObjectRequest value) {
+          requests.add(value);
+        }
+
+        @Override
+        public void onError(Throwable t) {}
+
+        @Override
+        public void onCompleted() {
+          ImmutableList<WriteObjectRequest> build = requests.build();
+          if (writes.containsKey(build)) {
+            requests = new ImmutableList.Builder<>();
+            obs.onNext(writes.get(build));
+            obs.onCompleted();
+          } else {
+            obs.onError(
+                TestUtils.apiException(Code.PERMISSION_DENIED, "Unexpected request chain."));
+          }
+        }
+      };
+    }
+  }
+
+  private abstract static class Adapter extends CallStreamObserver<WriteObjectRequest> {
+
+    private Adapter() {}
+
+    @Override
+    public boolean isReady() {
+      return true;
+    }
+
+    @Override
+    public void setOnReadyHandler(Runnable onReadyHandler) {}
+
+    @Override
+    public void disableAutoInboundFlowControl() {}
+
+    @Override
+    public void request(int count) {}
+
+    @Override
+    public void setMessageCompression(boolean enable) {}
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcTransformPageDecoratorTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcTransformPageDecoratorTest.java
index 56f50015..187348a9 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcTransformPageDecoratorTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcTransformPageDecoratorTest.java
@@ -18,111 +18,298 @@ package com.google.cloud.storage;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.api.core.ApiClock;
 import com.google.api.core.ApiFuture;
 import com.google.api.core.ApiFutures;
+import com.google.api.core.NanoClock;
 import com.google.api.gax.paging.AbstractPage;
+import com.google.api.gax.retrying.ResultRetryAlgorithm;
+import com.google.api.gax.retrying.RetrySettings;
+import com.google.api.gax.retrying.TimedAttemptSettings;
 import com.google.api.gax.rpc.ApiCallContext;
 import com.google.api.gax.rpc.PageContext;
 import com.google.api.gax.rpc.PagedListDescriptor;
 import com.google.api.gax.rpc.UnaryCallable;
 import com.google.cloud.storage.GrpcStorageImpl.TransformingPageDecorator;
+import com.google.cloud.storage.Retrying.RetryingDependencies;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Streams;
-import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
 import org.junit.Test;
 import org.mockito.Mockito;
 
 public class GrpcTransformPageDecoratorTest {
 
+  // define some common data used across our tests
+  private final ApiCallContext apiCallContext = Mockito.mock(ApiCallContext.class);
+
+  // Initial values for the first page
+  private final ImmutableList<String> page1 = ImmutableList.of("string1", "string2", "string3");
+  // Request which will be appended to the second page
+  private final ImmutableList<String> page2 = ImmutableList.of("string4");
+  // Request which will be appended to the second page
+  private final ImmutableList<String> page3 = ImmutableList.of("string5");
+
+  // Expected values after the translation
+  private final List<String> expectedValues =
+      Streams.concat(page1.stream(), page2.stream(), page3.stream())
+          .map(String::toUpperCase)
+          .collect(ImmutableList.toImmutableList());
+
+  private final Req req1 = new Req("req1");
+  private final Resp resp1 = new Resp("req2", page1);
+  private final Req req2 = new Req("req2");
+  private final Resp resp2 = new Resp("req3", page2);
+  private final Req req3 = new Req("req3");
+  private final Resp resp3 = new Resp(null, page3);
+  private final ImmutableMap<Req, Resp> data =
+      ImmutableMap.of(
+          req1, resp1,
+          req2, resp2,
+          req3, resp3);
+
   @Test
   public void valueTranslationTest() {
-    // Initial values for the first page
-    List<String> initialValues = ImmutableList.of("string1", "string2", "string3");
-    // Request which will be appended to the second page
-    String request = "string4";
-
-    // Expected values after the translation
-    List<String> expectedValuesPageOne = ImmutableList.of("STRING1", "STRING2", "STRING3");
-    List<String> expectedValuesPageTwo = ImmutableList.of("STRING4");
-    List<String> expectedPagesValuesMerged =
-        Streams.concat(expectedValuesPageOne.stream(), expectedValuesPageTwo.stream())
-            .collect(ImmutableList.toImmutableList());
-
-    StringPagedListDescriptor descriptor = new StringPagedListDescriptor();
-    UnaryCallable<String, List<String>> callable = new StringPagedCallable();
-    PageContext<String, List<String>, String> context =
-        PageContext.create(callable, descriptor, request, Mockito.mock(ApiCallContext.class));
-    ListStringPage page = new ListStringPage(context, initialValues);
-    TransformingPageDecorator<String, List<String>, String, ListStringPage, String> decorator =
-        new TransformingPageDecorator<>(page, String::toUpperCase);
+    UnaryCallable<Req, Resp> callable = new DataDrivenCallable(data);
+    ReqRespDescriptor descriptor = new ReqRespDescriptor();
+    PageContext<Req, Resp, String> context =
+        PageContext.create(callable, descriptor, req1, apiCallContext);
+    ReqRespPage page = new ReqRespPage(context, resp1);
+    TransformingPageDecorator<Req, Resp, String, ReqRespPage, String> decorator =
+        new TransformingPageDecorator<>(
+            page,
+            String::toUpperCase,
+            retryingDeps(),
+            StorageRetryStrategy.getUniformStorageRetryStrategy().getIdempotentHandler());
 
     assertThat(ImmutableList.copyOf(decorator.getValues().iterator()))
-        .containsExactlyElementsIn(expectedValuesPageOne);
+        .containsExactlyElementsIn(
+            page1.stream().map(String::toUpperCase).collect(Collectors.toList()));
     assertThat(ImmutableList.copyOf(decorator.iterateAll().iterator()))
-        .containsExactlyElementsIn(expectedPagesValuesMerged);
+        .containsExactlyElementsIn(expectedValues);
   }
 
-  private static class ListStringPage
-      extends AbstractPage<String, List<String>, String, ListStringPage> {
+  @Test
+  public void retryWorks() {
+    FailureInducingCallable callable = new FailureInducingCallable(new DataDrivenCallable(data));
+
+    ShouldRetryExceptionAlgorithm alg = new ShouldRetryExceptionAlgorithm();
+
+    ReqRespDescriptor descriptor = new ReqRespDescriptor();
+    PageContext<Req, Resp, String> context =
+        PageContext.create(callable, descriptor, req1, apiCallContext);
+    ReqRespPage page = new ReqRespPage(context, resp1);
+    TransformingPageDecorator<Req, Resp, String, ReqRespPage, String> decorator =
+        new TransformingPageDecorator<>(page, String::toUpperCase, retryingDeps(), alg);
 
-    public ListStringPage(
-        PageContext<String, List<String>, String> context, List<String> response) {
+    ImmutableList<String> actual = ImmutableList.copyOf(decorator.iterateAll().iterator());
+    assertThat(actual).containsExactlyElementsIn(expectedValues);
+    // sometimes shouldRetry is called multiple times, not totally sure why
+    assertThat(alg.shouldRetryCallCount.get()).isAtLeast(1);
+    // we expect to attempt the RPC twice, first attempt fails, latter calls succeed
+    assertThat(callable.callableCallCount.get()).isEqualTo(3);
+  }
+
+  static RetryingDependencies retryingDeps() {
+    return new RetryingDependencies() {
+      @Override
+      public RetrySettings getRetrySettings() {
+        return StorageOptions.getDefaultRetrySettings();
+      }
+
+      @Override
+      public ApiClock getClock() {
+        return NanoClock.getDefaultClock();
+      }
+    };
+  }
+
+  private static class ReqRespPage extends AbstractPage<Req, Resp, String, ReqRespPage> {
+
+    private final Resp response;
+
+    public ReqRespPage(PageContext<Req, Resp, String> context, Resp response) {
       super(context, response);
+      this.response = response;
+    }
+
+    @Override
+    public ReqRespPage createPage(PageContext<Req, Resp, String> context, Resp response) {
+      return new ReqRespPage(context, response);
     }
 
     @Override
-    public ListStringPage createPage(
-        PageContext<String, List<String>, String> context, List<String> response) {
-      return new ListStringPage(context, response);
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("response", response).toString();
     }
   }
 
-  private static class StringPagedListDescriptor
-      implements PagedListDescriptor<String, List<String>, String> {
+  private static class ReqRespDescriptor implements PagedListDescriptor<Req, Resp, String> {
+
     @Override
     public String emptyToken() {
       return "";
     }
 
     @Override
-    public String injectToken(String payload, String token) {
-      return payload;
+    public Req injectToken(Req payload, String token) {
+      return payload.withToken(token);
     }
 
     @Override
-    public String injectPageSize(String payload, int pageSize) {
-      return null;
+    public Req injectPageSize(Req payload, int pageSize) {
+      return payload.withPageSize(pageSize);
     }
 
     @Override
-    public Integer extractPageSize(String payload) {
-      return null;
+    public Integer extractPageSize(Req payload) {
+      return payload.pageSize;
     }
 
     @Override
-    public String extractNextToken(List<String> payload) {
-      return payload.size() > 0 ? payload.get(0) : emptyToken();
+    public String extractNextToken(Resp payload) {
+      return payload.nextToken != null ? payload.nextToken : emptyToken();
+    }
+
+    @Override
+    public Iterable<String> extractResources(Resp payload) {
+      return payload.resources;
+    }
+  }
+
+  /**
+   * Provide an immutable map of {@link Req} to {@link Resp}, when {@link #futureCall(Req,
+   * ApiCallContext)} is invoked attempt to retrieve the Resp from the provided map, if it's present
+   * return it, if not return an exception.
+   */
+  private static class DataDrivenCallable extends UnaryCallable<Req, Resp> {
+    private final ImmutableMap<Req, Resp> data;
+
+    private DataDrivenCallable(ImmutableMap<Req, Resp> data) {
+      this.data = data;
     }
 
     @Override
-    public Iterable<String> extractResources(List<String> payload) {
-      return payload;
+    public ApiFuture<Resp> futureCall(Req request, ApiCallContext context) {
+      if (data.containsKey(request)) {
+        return ApiFutures.immediateFuture(data.get(request));
+      }
+      return ApiFutures.immediateFailedFuture(
+          new IllegalStateException("No matching response for request: " + request));
     }
   }
 
-  private static class StringPagedCallable extends UnaryCallable<String, List<String>> {
-    // We only want to add one additional page with the same value as the request,
-    // this is kind of hacky, but I wanted to validate we are performing iterate all
-    // properly.
-    private int numberOfPages = 1;
+  private static final class Req {
+    private final String token;
+    private final int pageSize;
+
+    Req(String token) {
+      this(token, 5);
+    }
+
+    Req(String token, int pageSize) {
+      this.token = token;
+      this.pageSize = pageSize;
+    }
+
+    Req withToken(String t) {
+      return new Req(t, pageSize);
+    }
+
+    Req withPageSize(int i) {
+      return new Req(token, i);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof Req)) {
+        return false;
+      }
+      Req req = (Req) o;
+      return pageSize == req.pageSize && Objects.equals(token, req.token);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(token, pageSize);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("token", token)
+          .add("pageSize", pageSize)
+          .toString();
+    }
+  }
+
+  private static final class Resp {
+    private final String nextToken;
+    private final ImmutableList<String> resources;
+
+    public Resp(String nextToken, ImmutableList<String> resources) {
+      this.resources = resources;
+      this.nextToken = nextToken;
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("nextToken", nextToken)
+          .add("resources", resources)
+          .toString();
+    }
+  }
+
+  private static final class ShouldRetryException extends RuntimeException {}
+
+  private static final class ShouldRetryExceptionAlgorithm implements ResultRetryAlgorithm<Object> {
+    private final AtomicInteger shouldRetryCallCount;
+
+    public ShouldRetryExceptionAlgorithm() {
+      shouldRetryCallCount = new AtomicInteger(0);
+    }
+
+    @Override
+    public TimedAttemptSettings createNextAttempt(
+        Throwable prevThrowable, Object prevResponse, TimedAttemptSettings prevSettings) {
+      return null;
+    }
+
+    @Override
+    public boolean shouldRetry(Throwable prevThrowable, Object prevResponse)
+        throws CancellationException {
+      shouldRetryCallCount.incrementAndGet();
+      return prevThrowable instanceof ShouldRetryException;
+    }
+  }
+
+  private static final class FailureInducingCallable extends UnaryCallable<Req, Resp> {
+
+    private final AtomicInteger callableCallCount;
+    private final UnaryCallable<Req, Resp> delegate;
+
+    FailureInducingCallable(UnaryCallable<Req, Resp> delegate) {
+      this.delegate = delegate;
+      this.callableCallCount = new AtomicInteger(0);
+    }
 
     @Override
-    public ApiFuture<List<String>> futureCall(String request, ApiCallContext context) {
-      if (numberOfPages-- > 0) {
-        return ApiFutures.immediateFuture(ImmutableList.of(request));
+    public ApiFuture<Resp> futureCall(Req request, ApiCallContext context) {
+      int callCount = callableCallCount.getAndIncrement();
+      if (callCount == 0) {
+        throw new ShouldRetryException();
       }
-      return ApiFutures.immediateFuture(Collections.emptyList());
+      return delegate.futureCall(request, context);
     }
   }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/GzipReadableByteChannelTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/GzipReadableByteChannelTest.java
new file mode 100644
index 00000000..27809c7b
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/GzipReadableByteChannelTest.java
@@ -0,0 +1,317 @@
+/*
+ * 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;
+
+import static com.google.cloud.storage.TestUtils.getChecksummedData;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.cloud.ReadChannel;
+import com.google.cloud.storage.Storage.BlobSourceOption;
+import com.google.cloud.storage.UnbufferedReadableByteChannelSession.UnbufferedReadableByteChannel;
+import com.google.common.io.ByteStreams;
+import com.google.protobuf.ByteString;
+import com.google.storage.v2.Object;
+import com.google.storage.v2.ReadObjectRequest;
+import com.google.storage.v2.ReadObjectResponse;
+import com.google.storage.v2.StorageClient;
+import com.google.storage.v2.StorageGrpc;
+import io.grpc.Status;
+import io.grpc.stub.StreamObserver;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.security.SecureRandom;
+import java.util.concurrent.ExecutionException;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+
+@RunWith(Enclosed.class)
+public class GzipReadableByteChannelTest {
+
+  @SuppressWarnings("PointlessArithmeticExpression")
+  private static final int _1KiB = 1 * 1024;
+
+  private static final int _2KiB = 2 * 1024;
+  private static final int _3KiB = _1KiB + _2KiB;
+  private static final SecureRandom rand = new SecureRandom();
+
+  private static final byte[] dataUncompressed = DataGenerator.rand(rand).genBytes(_3KiB);
+  private static final byte[] dataCompressed = TestUtils.gzipBytes(dataUncompressed);
+  private static final ByteString contentUncompressed1 =
+      ByteString.copyFrom(dataUncompressed, 0, _2KiB);
+  private static final ByteString contentUncompressed2 =
+      ByteString.copyFrom(dataUncompressed, _2KiB, _1KiB);
+  private static final ByteString contentCompressed1 =
+      ByteString.copyFrom(dataCompressed, 0, _2KiB);
+  private static final ByteString contentCompressed2 =
+      ByteString.copyFrom(dataCompressed, _2KiB, dataCompressed.length - _2KiB);
+  private static final ReadObjectRequest reqUncompressed =
+      ReadObjectRequest.newBuilder()
+          .setBucket("projects/_/buckets/buck")
+          .setObject("obj-uncompressed")
+          .build();
+  private static final ReadObjectRequest reqCompressed =
+      ReadObjectRequest.newBuilder()
+          .setBucket("projects/_/buckets/buck")
+          .setObject("obj-compressed")
+          .build();
+
+  private static final ReadObjectResponse respUncompressed1 =
+      ReadObjectResponse.newBuilder()
+          .setMetadata(Object.newBuilder().setContentEncoding("identity").build())
+          .setChecksummedData(getChecksummedData(contentUncompressed1))
+          .build();
+  private static final ReadObjectResponse respUncompressed2 =
+      ReadObjectResponse.newBuilder()
+          .setChecksummedData(getChecksummedData(contentUncompressed2))
+          .build();
+
+  private static final ReadObjectResponse respCompressed1 =
+      ReadObjectResponse.newBuilder()
+          .setMetadata(Object.newBuilder().setContentEncoding("gzip").build())
+          .setChecksummedData(getChecksummedData(contentCompressed1))
+          .build();
+  private static final ReadObjectResponse respCompressed2 =
+      ReadObjectResponse.newBuilder()
+          .setChecksummedData(getChecksummedData(contentCompressed2))
+          .build();
+
+  public static final class Uncompressed {
+    private static final StorageGrpc.StorageImplBase fakeStorage =
+        new StorageGrpc.StorageImplBase() {
+          @Override
+          public void readObject(
+              ReadObjectRequest request, StreamObserver<ReadObjectResponse> responseObserver) {
+            if (request.equals(reqUncompressed)) {
+              responseObserver.onNext(respUncompressed1);
+              responseObserver.onNext(respUncompressed2);
+              responseObserver.onCompleted();
+            } else {
+              responseObserver.onError(TestUtils.apiException(Status.Code.UNIMPLEMENTED));
+            }
+          }
+        };
+
+    @ClassRule(order = 1)
+    public static final AutoClosableFixture<FakeServer> fakeServer =
+        AutoClosableFixture.of(() -> FakeServer.of(fakeStorage));
+
+    @ClassRule(order = 2)
+    public static final AutoClosableFixture<StorageClient> storageClient =
+        AutoClosableFixture.of(
+            () -> StorageClient.create(fakeServer.getInstance().storageSettings()));
+
+    @Test
+    public void autoGzipDecompress_true() throws IOException {
+      UnbufferedReadableByteChannelSession<Object> session =
+          ResumableMedia.gapic()
+              .read()
+              .byteChannel(storageClient.getInstance().readObjectCallable())
+              .setHasher(Hasher.noop())
+              .setAutoGzipDecompression(true)
+              .unbuffered()
+              .setReadObjectRequest(reqUncompressed)
+              .build();
+
+      byte[] actualBytes = new byte[dataUncompressed.length];
+      try (UnbufferedReadableByteChannel c = session.open()) {
+        c.read(ByteBuffer.wrap(actualBytes));
+      }
+      assertThat(actualBytes).isEqualTo(dataUncompressed);
+    }
+
+    @Test
+    public void autoGzipDecompress_false() throws IOException {
+      UnbufferedReadableByteChannelSession<Object> session =
+          ResumableMedia.gapic()
+              .read()
+              .byteChannel(storageClient.getInstance().readObjectCallable())
+              .setHasher(Hasher.noop())
+              .setAutoGzipDecompression(false)
+              .unbuffered()
+              .setReadObjectRequest(reqUncompressed)
+              .build();
+
+      byte[] actualBytes = new byte[dataUncompressed.length];
+      try (UnbufferedReadableByteChannel c = session.open()) {
+        c.read(ByteBuffer.wrap(actualBytes));
+      }
+      assertThat(actualBytes).isEqualTo(dataUncompressed);
+    }
+  }
+
+  public static final class Compressed {
+
+    private static final StorageGrpc.StorageImplBase fakeStorage =
+        new StorageGrpc.StorageImplBase() {
+          @Override
+          public void readObject(
+              ReadObjectRequest request, StreamObserver<ReadObjectResponse> responseObserver) {
+            if (request.equals(reqCompressed)) {
+              responseObserver.onNext(respCompressed1);
+              responseObserver.onNext(respCompressed2);
+              responseObserver.onCompleted();
+            } else {
+              responseObserver.onError(TestUtils.apiException(Status.Code.UNIMPLEMENTED));
+            }
+          }
+        };
+
+    @ClassRule(order = 1)
+    public static final AutoClosableFixture<FakeServer> fakeServer =
+        AutoClosableFixture.of(() -> FakeServer.of(fakeStorage));
+
+    @ClassRule(order = 2)
+    public static final AutoClosableFixture<StorageClient> storageClient =
+        AutoClosableFixture.of(
+            () -> StorageClient.create(fakeServer.getInstance().storageSettings()));
+
+    @ClassRule(order = 3)
+    public static final StorageFixture storageFixture =
+        StorageFixture.of(() -> fakeServer.getInstance().getGrpcStorageOptions().getService());
+
+    @Test
+    public void autoGzipDecompress_true() throws IOException {
+      UnbufferedReadableByteChannelSession<Object> session =
+          ResumableMedia.gapic()
+              .read()
+              .byteChannel(storageClient.getInstance().readObjectCallable())
+              .setHasher(Hasher.noop())
+              .setAutoGzipDecompression(true)
+              .unbuffered()
+              .setReadObjectRequest(reqCompressed)
+              .build();
+
+      byte[] actualBytes = new byte[dataUncompressed.length];
+      try (UnbufferedReadableByteChannel c = session.open()) {
+        c.read(ByteBuffer.wrap(actualBytes));
+      }
+      assertThat(actualBytes).isEqualTo(dataUncompressed);
+    }
+
+    @Test
+    public void autoGzipDecompress_false() throws IOException {
+      UnbufferedReadableByteChannelSession<Object> session =
+          ResumableMedia.gapic()
+              .read()
+              .byteChannel(storageClient.getInstance().readObjectCallable())
+              .setHasher(Hasher.noop())
+              .setAutoGzipDecompression(false)
+              .unbuffered()
+              .setReadObjectRequest(reqCompressed)
+              .build();
+
+      byte[] actualBytes = new byte[dataCompressed.length];
+      try (UnbufferedReadableByteChannel c = session.open()) {
+        c.read(ByteBuffer.wrap(actualBytes));
+      }
+      assertThat(actualBytes).isEqualTo(dataCompressed);
+    }
+
+    @Test
+    public void storage_readAllBytes() {
+      Storage s = storageFixture.getInstance();
+      byte[] actual = s.readAllBytes(BlobId.of("buck", "obj-compressed"));
+      assertThat(actual).isEqualTo(dataUncompressed);
+    }
+
+    @Test
+    public void storage_readAllBytes_returnRawInputStream() {
+      Storage s = storageFixture.getInstance();
+      byte[] actual =
+          s.readAllBytes(
+              BlobId.of("buck", "obj-compressed"),
+              BlobSourceOption.shouldReturnRawInputStream(true));
+      assertThat(actual).isEqualTo(dataCompressed);
+    }
+
+    @Test
+    public void storage_reader() throws Exception {
+      Storage s = storageFixture.getInstance();
+      byte[] actual = new byte[dataUncompressed.length];
+      try (ReadChannel c = s.reader(BlobId.of("buck", "obj-compressed"))) {
+        c.read(ByteBuffer.wrap(actual));
+      }
+      assertThat(actual).isEqualTo(dataUncompressed);
+    }
+
+    @Test
+    public void storage_reader_returnRawInputStream() throws Exception {
+      Storage s = storageFixture.getInstance();
+      byte[] actual = new byte[dataCompressed.length];
+      try (ReadChannel c =
+          s.reader(
+              BlobId.of("buck", "obj-compressed"),
+              BlobSourceOption.shouldReturnRawInputStream(true))) {
+        c.read(ByteBuffer.wrap(actual));
+      }
+      assertThat(actual).isEqualTo(dataCompressed);
+    }
+  }
+
+  public static final class Behavior {
+
+    @Test
+    public void properlyTracksEOF() throws IOException, InterruptedException, ExecutionException {
+      final StorageGrpc.StorageImplBase fakeStorage =
+          new StorageGrpc.StorageImplBase() {
+            int count = 0;
+
+            @Override
+            public void readObject(
+                ReadObjectRequest request, StreamObserver<ReadObjectResponse> responseObserver) {
+              if (count++ == 0) {
+                responseObserver.onNext(
+                    ReadObjectResponse.newBuilder()
+                        .setMetadata(Object.newBuilder().setSize(1).build())
+                        .setChecksummedData(getChecksummedData(ByteString.copyFromUtf8("a")))
+                        .build());
+                responseObserver.onCompleted();
+              } else {
+                responseObserver.onError(TestUtils.apiException(Status.Code.UNIMPLEMENTED));
+              }
+            }
+          };
+
+      try (FakeServer fakeServer = FakeServer.of(fakeStorage);
+          StorageClient sc = StorageClient.create(fakeServer.storageSettings())) {
+        ReadableByteChannelSession<?, Object> session =
+            ResumableMedia.gapic()
+                .read()
+                .byteChannel(sc.readObjectCallable())
+                .setHasher(Hasher.noop())
+                .setAutoGzipDecompression(true)
+                .unbuffered()
+                .setReadObjectRequest(reqUncompressed)
+                .build();
+
+        byte[] expected = new byte[] {(byte) 'a'};
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (ReadableByteChannel c = session.open()) {
+          ByteStreams.copy(c, Channels.newChannel(baos));
+        }
+        byte[] actual = baos.toByteArray();
+        assertThat(actual).isEqualTo(expected);
+        assertThat(session.getResult().get()).isNotNull();
+      }
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ResultRetryAlgorithmCompatibilityTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ResultRetryAlgorithmCompatibilityTest.java
new file mode 100644
index 00000000..717500d7
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ResultRetryAlgorithmCompatibilityTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.api.gax.retrying.BasicResultRetryAlgorithm;
+import com.google.api.gax.rpc.DataLossException;
+import com.google.api.gax.rpc.StatusCode.Code;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import org.junit.Test;
+
+public final class ResultRetryAlgorithmCompatibilityTest {
+  @Test
+  public void validateConversion() {
+    BasicResultRetryAlgorithm<Object> alg =
+        new BasicResultRetryAlgorithm<java.lang.Object>() {
+          @Override
+          public boolean shouldRetry(
+              Throwable previousThrowable, java.lang.Object previousResponse) {
+            return previousThrowable instanceof DataLossException;
+          }
+        };
+    Set<Code> codes = GrpcStorageImpl.resultRetryAlgorithmToCodes(alg);
+
+    assertThat(codes).isEqualTo(ImmutableSet.of(Code.DATA_LOSS));
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java
new file mode 100644
index 00000000..573c2781
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java
@@ -0,0 +1,111 @@
+/*
+ * 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;
+
+import com.google.api.gax.grpc.GrpcCallContext;
+import com.google.api.gax.grpc.GrpcStatusCode;
+import com.google.api.gax.rpc.ApiException;
+import com.google.api.gax.rpc.ApiExceptionFactory;
+import com.google.api.gax.rpc.ErrorDetails;
+import com.google.api.gax.rpc.StatusCode;
+import com.google.cloud.storage.Crc32cValue.Crc32cLengthKnown;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.protobuf.Any;
+import com.google.protobuf.ByteString;
+import com.google.rpc.DebugInfo;
+import com.google.storage.v2.ChecksummedData;
+import io.grpc.Status.Code;
+import io.grpc.StatusRuntimeException;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import java.util.zip.GZIPOutputStream;
+
+public final class TestUtils {
+
+  private TestUtils() {}
+
+  public static byte[] gzipBytes(byte[] bytes) {
+    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+    try (OutputStream out = new GZIPOutputStream(byteArrayOutputStream)) {
+      out.write(bytes);
+    } catch (IOException ignore) {
+      // GZIPOutputStream will only throw if the underlying stream throws.
+      // ByteArrayOutputStream does not throw on write
+    }
+
+    return byteArrayOutputStream.toByteArray();
+  }
+
+  public static ChecksummedData getChecksummedData(ByteString content) {
+    return ChecksummedData.newBuilder().setContent(content).build();
+  }
+
+  public static ChecksummedData getChecksummedData(ByteString content, Hasher hasher) {
+    ChecksummedData.Builder b = ChecksummedData.newBuilder().setContent(content);
+    Crc32cLengthKnown hash = hasher.hash(content.asReadOnlyByteBuffer());
+    if (hash != null) {
+      int crc32c = hash.getValue();
+      b.setCrc32C(crc32c);
+    }
+    return b.build();
+  }
+
+  public static ApiException apiException(Code code) {
+    return apiException(code, "");
+  }
+
+  public static ApiException apiException(Code code, String message) {
+    StatusRuntimeException statusRuntimeException = code.toStatus().asRuntimeException();
+    DebugInfo debugInfo =
+        DebugInfo.newBuilder().setDetail("forced failure |~| " + code.name() + message).build();
+    ErrorDetails errorDetails =
+        ErrorDetails.builder().setRawErrorMessages(ImmutableList.of(Any.pack(debugInfo))).build();
+    return ApiExceptionFactory.createException(
+        statusRuntimeException, GrpcStatusCode.of(code), true, errorDetails);
+  }
+
+  public static GrpcCallContext contextWithRetryForCodes(StatusCode.Code... code) {
+    return GrpcCallContext.createDefault().withRetryableCodes(ImmutableSet.copyOf(code));
+  }
+
+  public static ImmutableList<ByteBuffer> subDivide(byte[] bytes, int division) {
+    int length = bytes.length;
+    int fullDivisions = length / division;
+    int x = division * fullDivisions;
+    int remaining = length - x;
+
+    if ((fullDivisions == 1 && remaining == 0) || (fullDivisions == 0 && remaining == 1)) {
+      return ImmutableList.of(ByteBuffer.wrap(bytes));
+    } else {
+      return Stream.of(
+              IntStream.iterate(0, i -> i + division)
+                  .limit(fullDivisions)
+                  .mapToObj(i -> ByteBuffer.wrap(bytes, i, division)),
+              Stream.of(ByteBuffer.wrap(bytes, x, remaining)))
+          .flatMap(Function.identity())
+          .filter(Buffer::hasRemaining)
+          .collect(ImmutableList.toImmutableList());
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java
index e2042b01..9b346aca 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java
@@ -31,12 +31,13 @@ public final class TransportCompatibilityTest {
 
   @Test
   public void verifyUnsupportedMethodsGenerateMeaningfulException() {
-    Storage s =
+    GrpcStorageOptions options =
         StorageOptions.grpc()
             .setProjectId("blank")
             .setCredentials(NoCredentials.getInstance())
-            .build()
-            .getService();
+            .build();
+    @SuppressWarnings("resource")
+    Storage s = new GrpcStorageImpl(options, null);
     ImmutableList<String> messages =
         Stream.<Supplier<?>>of(
                 s::batch,
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/UnifiedOptsGrpcTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/UnifiedOptsGrpcTest.java
index 8099fe36..6e384bb3 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/UnifiedOptsGrpcTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/UnifiedOptsGrpcTest.java
@@ -34,8 +34,8 @@ import com.google.protobuf.ByteString;
 import com.google.storage.v2.CommonObjectRequestParams;
 import com.google.storage.v2.ComposeObjectRequest;
 import com.google.storage.v2.CreateBucketRequest;
+import com.google.storage.v2.CreateHmacKeyRequest;
 import com.google.storage.v2.DeleteBucketRequest;
-import com.google.storage.v2.DeleteHmacKeyRequest;
 import com.google.storage.v2.DeleteObjectRequest;
 import com.google.storage.v2.GetBucketRequest;
 import com.google.storage.v2.GetHmacKeyRequest;
@@ -828,14 +828,14 @@ public final class UnifiedOptsGrpcTest {
       }
 
       @Test
-      public void deleteHmacKey_nonResource() {
-        DeleteHmacKeyRequest expected =
-            DeleteHmacKeyRequest.newBuilder().setProject("projects/proj").build();
+      public void createHmacKey_nonResource() {
+        CreateHmacKeyRequest expected =
+            CreateHmacKeyRequest.newBuilder().setProject("projects/proj").build();
 
-        DeleteHmacKeyRequest actual =
+        CreateHmacKeyRequest actual =
             UnifiedOpts.projectId("proj")
-                .deleteHmacKey()
-                .apply(DeleteHmacKeyRequest.newBuilder())
+                .createHmacKey()
+                .apply(CreateHmacKeyRequest.newBuilder())
                 .build();
         assertThat(actual).isEqualTo(expected);
       }
@@ -867,14 +867,40 @@ public final class UnifiedOptsGrpcTest {
       }
 
       @Test
-      public void deleteHmacKey_resource() {
-        DeleteHmacKeyRequest expected =
-            DeleteHmacKeyRequest.newBuilder().setProject("projects/proj").build();
+      public void createHmacKey_resource() {
+        CreateHmacKeyRequest expected =
+            CreateHmacKeyRequest.newBuilder().setProject("projects/proj").build();
 
-        DeleteHmacKeyRequest actual =
+        CreateHmacKeyRequest actual =
             UnifiedOpts.projectId("projects/proj")
-                .deleteHmacKey()
-                .apply(DeleteHmacKeyRequest.newBuilder())
+                .createHmacKey()
+                .apply(CreateHmacKeyRequest.newBuilder())
+                .build();
+        assertThat(actual).isEqualTo(expected);
+      }
+
+      @Test
+      public void listBuckets_nonResource() {
+        ListBucketsRequest expected =
+            ListBucketsRequest.newBuilder().setParent("projects/proj").build();
+
+        ListBucketsRequest actual =
+            UnifiedOpts.projectId("proj")
+                .listBuckets()
+                .apply(ListBucketsRequest.newBuilder())
+                .build();
+        assertThat(actual).isEqualTo(expected);
+      }
+
+      @Test
+      public void listBuckets_resource() {
+        ListBucketsRequest expected =
+            ListBucketsRequest.newBuilder().setParent("projects/proj").build();
+
+        ListBucketsRequest actual =
+            UnifiedOpts.projectId("projects/proj")
+                .listBuckets()
+                .apply(ListBucketsRequest.newBuilder())
                 .build();
         assertThat(actual).isEqualTo(expected);
       }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java
new file mode 100644
index 00000000..f7f6c234
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java
@@ -0,0 +1,785 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.cloud.Condition;
+import com.google.cloud.Identity;
+import com.google.cloud.Policy;
+import com.google.cloud.storage.Acl;
+import com.google.cloud.storage.Acl.Role;
+import com.google.cloud.storage.Acl.User;
+import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.BucketFixture;
+import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.Storage.BucketField;
+import com.google.cloud.storage.StorageException;
+import com.google.cloud.storage.StorageFixture;
+import com.google.cloud.storage.StorageRoles;
+import com.google.cloud.storage.testing.RemoteStorageHelper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+public class ITAccessTest {
+  @ClassRule(order = 1)
+  public static final StorageFixture storageFixture = StorageFixture.defaultHttp();
+
+  @ClassRule(order = 2)
+  public static final BucketFixture bucketFixture =
+      BucketFixture.newBuilder().setHandle(storageFixture::getInstance).build();
+
+  @ClassRule(order = 3)
+  public static final BucketFixture requesterPaysFixture =
+      BucketFixture.newBuilder().setHandle(storageFixture::getInstance).build();
+
+  private static final Long RETENTION_PERIOD = 5L;
+
+  private static Storage storage;
+  private static String bucketName;
+  private static String requesterPaysBucketName;
+
+  @BeforeClass
+  public static void setup() {
+    storage = storageFixture.getInstance();
+    bucketName = bucketFixture.getBucketInfo().getName();
+    requesterPaysBucketName = requesterPaysFixture.getBucketInfo().getName();
+  }
+
+  @Test
+  public void testBucketAcl() {
+    unsetRequesterPays();
+    testBucketAclRequesterPays(true);
+    testBucketAclRequesterPays(false);
+  }
+
+  private void testBucketAclRequesterPays(boolean requesterPays) {
+    if (requesterPays) {
+      Bucket remoteBucket =
+          storage.get(
+              requesterPaysBucketName,
+              Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING));
+      assertTrue(remoteBucket.requesterPays() == null || !remoteBucket.requesterPays());
+      remoteBucket = remoteBucket.toBuilder().setRequesterPays(true).build();
+      Bucket updatedBucket = storage.update(remoteBucket);
+      assertTrue(updatedBucket.requesterPays());
+    }
+
+    String projectId = storage.getOptions().getProjectId();
+
+    Storage.BucketSourceOption[] bucketOptions =
+        requesterPays
+            ? new Storage.BucketSourceOption[] {Storage.BucketSourceOption.userProject(projectId)}
+            : new Storage.BucketSourceOption[] {};
+
+    assertNull(
+        storage.getAcl(requesterPaysBucketName, User.ofAllAuthenticatedUsers(), bucketOptions));
+    assertFalse(
+        storage.deleteAcl(requesterPaysBucketName, User.ofAllAuthenticatedUsers(), bucketOptions));
+    Acl acl = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER);
+    assertNotNull(storage.createAcl(requesterPaysBucketName, acl, bucketOptions));
+    Acl updatedAcl =
+        storage.updateAcl(
+            requesterPaysBucketName, acl.toBuilder().setRole(Role.WRITER).build(), bucketOptions);
+    assertEquals(Role.WRITER, updatedAcl.getRole());
+    Set<Acl> acls = new HashSet<>();
+    acls.addAll(storage.listAcls(requesterPaysBucketName, bucketOptions));
+    assertTrue(acls.contains(updatedAcl));
+    assertTrue(
+        storage.deleteAcl(requesterPaysBucketName, User.ofAllAuthenticatedUsers(), bucketOptions));
+    assertNull(
+        storage.getAcl(requesterPaysBucketName, User.ofAllAuthenticatedUsers(), bucketOptions));
+    if (requesterPays) {
+      Bucket remoteBucket =
+          storage.get(
+              requesterPaysBucketName,
+              Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING),
+              Storage.BucketGetOption.userProject(projectId));
+      assertTrue(remoteBucket.requesterPays());
+      remoteBucket = remoteBucket.toBuilder().setRequesterPays(false).build();
+      Bucket updatedBucket =
+          storage.update(remoteBucket, Storage.BucketTargetOption.userProject(projectId));
+      assertFalse(updatedBucket.requesterPays());
+    }
+  }
+
+  @Test
+  public void testBucketDefaultAcl() {
+    assertNull(storage.getDefaultAcl(bucketName, User.ofAllAuthenticatedUsers()));
+    assertFalse(storage.deleteDefaultAcl(bucketName, User.ofAllAuthenticatedUsers()));
+    Acl acl = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER);
+    assertNotNull(storage.createDefaultAcl(bucketName, acl));
+    Acl updatedAcl =
+        storage.updateDefaultAcl(bucketName, acl.toBuilder().setRole(Role.OWNER).build());
+    assertEquals(Role.OWNER, updatedAcl.getRole());
+    Set<Acl> acls = new HashSet<>();
+    acls.addAll(storage.listDefaultAcls(bucketName));
+    assertTrue(acls.contains(updatedAcl));
+    assertTrue(storage.deleteDefaultAcl(bucketName, User.ofAllAuthenticatedUsers()));
+    assertNull(storage.getDefaultAcl(bucketName, User.ofAllAuthenticatedUsers()));
+  }
+
+  @Test
+  public void testBucketPolicyV1RequesterPays() {
+    unsetRequesterPays();
+    Bucket bucketDefault =
+        storage.get(
+            requesterPaysBucketName,
+            Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING));
+    assertTrue(bucketDefault.requesterPays() == null || !bucketDefault.requesterPays());
+
+    Bucket bucketTrue = storage.update(bucketDefault.toBuilder().setRequesterPays(true).build());
+    assertTrue(bucketTrue.requesterPays());
+
+    String projectId = storage.getOptions().getProjectId();
+
+    Storage.BucketSourceOption[] bucketOptions =
+        new Storage.BucketSourceOption[] {Storage.BucketSourceOption.userProject(projectId)};
+    Identity projectOwner = Identity.projectOwner(projectId);
+    Identity projectEditor = Identity.projectEditor(projectId);
+    Identity projectViewer = Identity.projectViewer(projectId);
+    Map<com.google.cloud.Role, Set<Identity>> bindingsWithoutPublicRead =
+        ImmutableMap.of(
+            StorageRoles.legacyBucketOwner(),
+            new HashSet<>(Arrays.asList(projectOwner, projectEditor)),
+            StorageRoles.legacyBucketReader(),
+            (Set<Identity>) new HashSet<>(Collections.singleton(projectViewer)));
+    Map<com.google.cloud.Role, Set<Identity>> bindingsWithPublicRead =
+        ImmutableMap.of(
+            StorageRoles.legacyBucketOwner(),
+            new HashSet<>(Arrays.asList(projectOwner, projectEditor)),
+            StorageRoles.legacyBucketReader(),
+            new HashSet<>(Collections.singleton(projectViewer)),
+            StorageRoles.legacyObjectReader(),
+            (Set<Identity>) new HashSet<>(Collections.singleton(Identity.allUsers())));
+
+    // Validate getting policy.
+    Policy currentPolicy = storage.getIamPolicy(requesterPaysBucketName, bucketOptions);
+    assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindings());
+
+    // Validate updating policy.
+    Policy updatedPolicy =
+        storage.setIamPolicy(
+            requesterPaysBucketName,
+            currentPolicy
+                .toBuilder()
+                .addIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers())
+                .build(),
+            bucketOptions);
+    assertEquals(bindingsWithPublicRead, updatedPolicy.getBindings());
+    Policy revertedPolicy =
+        storage.setIamPolicy(
+            requesterPaysBucketName,
+            updatedPolicy
+                .toBuilder()
+                .removeIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers())
+                .build(),
+            bucketOptions);
+    assertEquals(bindingsWithoutPublicRead, revertedPolicy.getBindings());
+
+    // Validate testing permissions.
+    List<Boolean> expectedPermissions = ImmutableList.of(true, true);
+    assertEquals(
+        expectedPermissions,
+        storage.testIamPermissions(
+            requesterPaysBucketName,
+            ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"),
+            bucketOptions));
+    Bucket bucketFalse =
+        storage.update(
+            bucketTrue.toBuilder().setRequesterPays(false).build(),
+            Storage.BucketTargetOption.userProject(projectId));
+    assertFalse(bucketFalse.requesterPays());
+  }
+
+  @Test
+  public void testBucketPolicyV1() {
+    String projectId = storage.getOptions().getProjectId();
+
+    Storage.BucketSourceOption[] bucketOptions = new Storage.BucketSourceOption[] {};
+    Identity projectOwner = Identity.projectOwner(projectId);
+    Identity projectEditor = Identity.projectEditor(projectId);
+    Identity projectViewer = Identity.projectViewer(projectId);
+    Map<com.google.cloud.Role, Set<Identity>> bindingsWithoutPublicRead =
+        ImmutableMap.of(
+            StorageRoles.legacyBucketOwner(),
+            new HashSet<>(Arrays.asList(projectOwner, projectEditor)),
+            StorageRoles.legacyBucketReader(),
+            (Set<Identity>) new HashSet<>(Collections.singleton(projectViewer)));
+    Map<com.google.cloud.Role, Set<Identity>> bindingsWithPublicRead =
+        ImmutableMap.of(
+            StorageRoles.legacyBucketOwner(),
+            new HashSet<>(Arrays.asList(projectOwner, projectEditor)),
+            StorageRoles.legacyBucketReader(),
+            new HashSet<>(Collections.singleton(projectViewer)),
+            StorageRoles.legacyObjectReader(),
+            (Set<Identity>) new HashSet<>(Collections.singleton(Identity.allUsers())));
+
+    // Validate getting policy.
+    Policy currentPolicy = storage.getIamPolicy(bucketName, bucketOptions);
+    assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindings());
+
+    // Validate updating policy.
+    Policy updatedPolicy =
+        storage.setIamPolicy(
+            bucketName,
+            currentPolicy
+                .toBuilder()
+                .addIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers())
+                .build(),
+            bucketOptions);
+    assertEquals(bindingsWithPublicRead, updatedPolicy.getBindings());
+    Policy revertedPolicy =
+        storage.setIamPolicy(
+            bucketName,
+            updatedPolicy
+                .toBuilder()
+                .removeIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers())
+                .build(),
+            bucketOptions);
+    assertEquals(bindingsWithoutPublicRead, revertedPolicy.getBindings());
+
+    // Validate testing permissions.
+    List<Boolean> expectedPermissions = ImmutableList.of(true, true);
+    assertEquals(
+        expectedPermissions,
+        storage.testIamPermissions(
+            bucketName,
+            ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"),
+            bucketOptions));
+  }
+
+  @Test
+  public void testBucketPolicyV3() {
+    // Enable Uniform Bucket-Level Access
+    storage.update(
+        BucketInfo.newBuilder(bucketName)
+            .setIamConfiguration(
+                BucketInfo.IamConfiguration.newBuilder()
+                    .setIsUniformBucketLevelAccessEnabled(true)
+                    .build())
+            .build());
+    String projectId = storage.getOptions().getProjectId();
+
+    Storage.BucketSourceOption[] bucketOptions =
+        new Storage.BucketSourceOption[] {Storage.BucketSourceOption.requestedPolicyVersion(3)};
+    Identity projectOwner = Identity.projectOwner(projectId);
+    Identity projectEditor = Identity.projectEditor(projectId);
+    Identity projectViewer = Identity.projectViewer(projectId);
+    List<com.google.cloud.Binding> bindingsWithoutPublicRead =
+        ImmutableList.of(
+            com.google.cloud.Binding.newBuilder()
+                .setRole(StorageRoles.legacyBucketOwner().toString())
+                .setMembers(ImmutableList.of(projectEditor.strValue(), projectOwner.strValue()))
+                .build(),
+            com.google.cloud.Binding.newBuilder()
+                .setRole(StorageRoles.legacyBucketReader().toString())
+                .setMembers(ImmutableList.of(projectViewer.strValue()))
+                .build());
+    List<com.google.cloud.Binding> bindingsWithPublicRead =
+        ImmutableList.of(
+            com.google.cloud.Binding.newBuilder()
+                .setRole(StorageRoles.legacyBucketReader().toString())
+                .setMembers(ImmutableList.of(projectViewer.strValue()))
+                .build(),
+            com.google.cloud.Binding.newBuilder()
+                .setRole(StorageRoles.legacyBucketOwner().toString())
+                .setMembers(ImmutableList.of(projectEditor.strValue(), projectOwner.strValue()))
+                .build(),
+            com.google.cloud.Binding.newBuilder()
+                .setRole(StorageRoles.legacyObjectReader().toString())
+                .setMembers(ImmutableList.of("allUsers"))
+                .build());
+
+    List<com.google.cloud.Binding> bindingsWithConditionalPolicy =
+        ImmutableList.of(
+            com.google.cloud.Binding.newBuilder()
+                .setRole(StorageRoles.legacyBucketReader().toString())
+                .setMembers(ImmutableList.of(projectViewer.strValue()))
+                .build(),
+            com.google.cloud.Binding.newBuilder()
+                .setRole(StorageRoles.legacyBucketOwner().toString())
+                .setMembers(ImmutableList.of(projectEditor.strValue(), projectOwner.strValue()))
+                .build(),
+            com.google.cloud.Binding.newBuilder()
+                .setRole(StorageRoles.legacyObjectReader().toString())
+                .setMembers(
+                    ImmutableList.of(
+                        "serviceAccount:storage-python@spec-test-ruby-samples.iam.gserviceaccount.com"))
+                .setCondition(
+                    Condition.newBuilder()
+                        .setTitle("Title")
+                        .setDescription("Description")
+                        .setExpression(
+                            "resource.name.startsWith(\"projects/_/buckets/bucket-name/objects/prefix-a-\")")
+                        .build())
+                .build());
+
+    // Validate getting policy.
+    Policy currentPolicy = storage.getIamPolicy(bucketName, bucketOptions);
+    assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindingsList());
+
+    // Validate updating policy.
+    List<com.google.cloud.Binding> currentBindings = new ArrayList(currentPolicy.getBindingsList());
+    currentBindings.add(
+        com.google.cloud.Binding.newBuilder()
+            .setRole(StorageRoles.legacyObjectReader().getValue())
+            .addMembers(Identity.allUsers().strValue())
+            .build());
+    Policy updatedPolicy =
+        storage.setIamPolicy(
+            bucketName,
+            currentPolicy.toBuilder().setBindings(currentBindings).build(),
+            bucketOptions);
+    assertTrue(
+        bindingsWithPublicRead.size() == updatedPolicy.getBindingsList().size()
+            && bindingsWithPublicRead.containsAll(updatedPolicy.getBindingsList()));
+
+    // Remove a member
+    List<com.google.cloud.Binding> updatedBindings = new ArrayList(updatedPolicy.getBindingsList());
+    for (int i = 0; i < updatedBindings.size(); i++) {
+      com.google.cloud.Binding binding = updatedBindings.get(i);
+      if (binding.getRole().equals(StorageRoles.legacyObjectReader().toString())) {
+        List<String> members = new ArrayList(binding.getMembers());
+        members.remove(Identity.allUsers().strValue());
+        updatedBindings.set(i, binding.toBuilder().setMembers(members).build());
+        break;
+      }
+    }
+
+    Policy revertedPolicy =
+        storage.setIamPolicy(
+            bucketName,
+            updatedPolicy.toBuilder().setBindings(updatedBindings).build(),
+            bucketOptions);
+
+    assertEquals(bindingsWithoutPublicRead, revertedPolicy.getBindingsList());
+    assertTrue(
+        bindingsWithoutPublicRead.size() == revertedPolicy.getBindingsList().size()
+            && bindingsWithoutPublicRead.containsAll(revertedPolicy.getBindingsList()));
+
+    // Add Conditional Policy
+    List<com.google.cloud.Binding> conditionalBindings =
+        new ArrayList(revertedPolicy.getBindingsList());
+    conditionalBindings.add(
+        com.google.cloud.Binding.newBuilder()
+            .setRole(StorageRoles.legacyObjectReader().toString())
+            .addMembers(
+                "serviceAccount:storage-python@spec-test-ruby-samples.iam.gserviceaccount.com")
+            .setCondition(
+                Condition.newBuilder()
+                    .setTitle("Title")
+                    .setDescription("Description")
+                    .setExpression(
+                        "resource.name.startsWith(\"projects/_/buckets/bucket-name/objects/prefix-a-\")")
+                    .build())
+            .build());
+    Policy conditionalPolicy =
+        storage.setIamPolicy(
+            bucketName,
+            revertedPolicy.toBuilder().setBindings(conditionalBindings).setVersion(3).build(),
+            bucketOptions);
+    assertTrue(
+        bindingsWithConditionalPolicy.size() == conditionalPolicy.getBindingsList().size()
+            && bindingsWithConditionalPolicy.containsAll(conditionalPolicy.getBindingsList()));
+
+    // Remove Conditional Policy
+    conditionalPolicy =
+        storage.setIamPolicy(
+            bucketName,
+            conditionalPolicy.toBuilder().setBindings(updatedBindings).setVersion(3).build(),
+            bucketOptions);
+
+    // Validate testing permissions.
+    List<Boolean> expectedPermissions = ImmutableList.of(true, true);
+    assertEquals(
+        expectedPermissions,
+        storage.testIamPermissions(
+            bucketName,
+            ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"),
+            bucketOptions));
+
+    // Disable Uniform Bucket-Level Access
+    storage.update(
+        BucketInfo.newBuilder(bucketName)
+            .setIamConfiguration(
+                BucketInfo.IamConfiguration.newBuilder()
+                    .setIsUniformBucketLevelAccessEnabled(false)
+                    .build())
+            .build());
+  }
+
+  @Test
+  @SuppressWarnings({"unchecked", "deprecation"})
+  public void testBucketWithBucketPolicyOnlyEnabled() throws Exception {
+    String bucket = RemoteStorageHelper.generateBucketName();
+    try {
+      storage.create(
+          Bucket.newBuilder(bucket)
+              .setIamConfiguration(
+                  BucketInfo.IamConfiguration.newBuilder()
+                      .setIsBucketPolicyOnlyEnabled(true)
+                      .build())
+              .build());
+
+      Bucket remoteBucket =
+          storage.get(bucket, Storage.BucketGetOption.fields(BucketField.IAMCONFIGURATION));
+
+      assertTrue(remoteBucket.getIamConfiguration().isBucketPolicyOnlyEnabled());
+      assertNotNull(remoteBucket.getIamConfiguration().getBucketPolicyOnlyLockedTime());
+
+      try {
+        remoteBucket.listAcls();
+        fail("StorageException was expected.");
+      } catch (StorageException e) {
+        // Expected: Listing legacy ACLs should fail on a BPO enabled bucket
+      }
+      try {
+        remoteBucket.listDefaultAcls();
+        fail("StorageException was expected");
+      } catch (StorageException e) {
+        // Expected: Listing legacy ACLs should fail on a BPO enabled bucket
+      }
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, bucket, 1, TimeUnit.MINUTES);
+    }
+  }
+
+  @Test
+  public void testBucketWithUniformBucketLevelAccessEnabled() throws Exception {
+    String bucket = RemoteStorageHelper.generateBucketName();
+    try {
+      storage.create(
+          Bucket.newBuilder(bucket)
+              .setIamConfiguration(
+                  BucketInfo.IamConfiguration.newBuilder()
+                      .setIsUniformBucketLevelAccessEnabled(true)
+                      .build())
+              .build());
+
+      Bucket remoteBucket =
+          storage.get(bucket, Storage.BucketGetOption.fields(BucketField.IAMCONFIGURATION));
+
+      assertTrue(remoteBucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
+      assertNotNull(remoteBucket.getIamConfiguration().getUniformBucketLevelAccessLockedTime());
+      try {
+        remoteBucket.listAcls();
+        fail("StorageException was expected.");
+      } catch (StorageException e) {
+        // Expected: Listing legacy ACLs should fail on a BPO enabled bucket
+      }
+      try {
+        remoteBucket.listDefaultAcls();
+        fail("StorageException was expected");
+      } catch (StorageException e) {
+        // Expected: Listing legacy ACLs should fail on a BPO enabled bucket
+      }
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, bucket, 1, TimeUnit.MINUTES);
+    }
+  }
+
+  @Test
+  public void testEnableAndDisableUniformBucketLevelAccessOnExistingBucket() throws Exception {
+    String bpoBucket = RemoteStorageHelper.generateBucketName();
+    try {
+      BucketInfo.IamConfiguration ublaDisabledIamConfiguration =
+          BucketInfo.IamConfiguration.newBuilder()
+              .setIsUniformBucketLevelAccessEnabled(false)
+              .build();
+      Bucket bucket =
+          storage.create(
+              Bucket.newBuilder(bpoBucket)
+                  .setIamConfiguration(ublaDisabledIamConfiguration)
+                  .setAcl(ImmutableList.of(Acl.of(User.ofAllAuthenticatedUsers(), Role.READER)))
+                  .setDefaultAcl(
+                      ImmutableList.of(Acl.of(User.ofAllAuthenticatedUsers(), Role.READER)))
+                  .build());
+
+      bucket
+          .toBuilder()
+          .setAcl(null)
+          .setDefaultAcl(null)
+          .setIamConfiguration(
+              ublaDisabledIamConfiguration
+                  .toBuilder()
+                  .setIsUniformBucketLevelAccessEnabled(true)
+                  .build())
+          .build()
+          .update();
+
+      Bucket remoteBucket =
+          storage.get(bpoBucket, Storage.BucketGetOption.fields(BucketField.IAMCONFIGURATION));
+
+      assertTrue(remoteBucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
+      assertNotNull(remoteBucket.getIamConfiguration().getUniformBucketLevelAccessLockedTime());
+
+      remoteBucket.toBuilder().setIamConfiguration(ublaDisabledIamConfiguration).build().update();
+
+      remoteBucket =
+          storage.get(
+              bpoBucket,
+              Storage.BucketGetOption.fields(
+                  BucketField.IAMCONFIGURATION, BucketField.ACL, BucketField.DEFAULT_OBJECT_ACL));
+
+      assertFalse(remoteBucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
+      assertEquals(User.ofAllAuthenticatedUsers(), remoteBucket.getDefaultAcl().get(0).getEntity());
+      assertEquals(Role.READER, remoteBucket.getDefaultAcl().get(0).getRole());
+      assertEquals(User.ofAllAuthenticatedUsers(), remoteBucket.getAcl().get(0).getEntity());
+      assertEquals(Role.READER, remoteBucket.getAcl().get(0).getRole());
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, bpoBucket, 1, TimeUnit.MINUTES);
+    }
+  }
+
+  @Test
+  public void testEnforcedPublicAccessPreventionOnBucket() throws Exception {
+    String papBucket = RemoteStorageHelper.generateBucketName();
+    try {
+      Bucket bucket = generatePublicAccessPreventionBucket(papBucket, true);
+      // Making bucket public should fail.
+      try {
+        storage.setIamPolicy(
+            papBucket,
+            Policy.newBuilder()
+                .setVersion(3)
+                .setBindings(
+                    ImmutableList.<com.google.cloud.Binding>of(
+                        com.google.cloud.Binding.newBuilder()
+                            .setRole("roles/storage.objectViewer")
+                            .addMembers("allUsers")
+                            .build()))
+                .build());
+        fail("pap: expected adding allUsers policy to bucket should fail");
+      } catch (StorageException storageException) {
+        // Creating a bucket with roles/storage.objectViewer is not
+        // allowed when publicAccessPrevention is enabled.
+        assertEquals(storageException.getCode(), 412);
+      }
+
+      // Making object public via ACL should fail.
+      try {
+        // Create a public object
+        bucket.create(
+            "pap-test-object",
+            "".getBytes(),
+            Bucket.BlobTargetOption.predefinedAcl(Storage.PredefinedAcl.PUBLIC_READ));
+        fail("pap: expected adding allUsers ACL to object should fail");
+      } catch (StorageException storageException) {
+        // Creating an object with allUsers roles/storage.viewer permission
+        // is not allowed. When Public Access Prevention is enabled.
+        assertEquals(storageException.getCode(), 412);
+      }
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, papBucket, 1, TimeUnit.MINUTES);
+    }
+  }
+
+  @Test
+  public void testUnspecifiedPublicAccessPreventionOnBucket() throws Exception {
+    String papBucket = RemoteStorageHelper.generateBucketName();
+    try {
+      Bucket bucket = generatePublicAccessPreventionBucket(papBucket, false);
+
+      // Now, making object public or making bucket public should succeed.
+      try {
+        // Create a public object
+        bucket.create(
+            "pap-test-object",
+            "".getBytes(),
+            Bucket.BlobTargetOption.predefinedAcl(Storage.PredefinedAcl.PUBLIC_READ));
+      } catch (StorageException storageException) {
+        fail("pap: expected adding allUsers ACL to object to succeed");
+      }
+
+      // Now, making bucket public should succeed.
+      try {
+        storage.setIamPolicy(
+            papBucket,
+            Policy.newBuilder()
+                .setVersion(3)
+                .setBindings(
+                    ImmutableList.<com.google.cloud.Binding>of(
+                        com.google.cloud.Binding.newBuilder()
+                            .setRole("roles/storage.objectViewer")
+                            .addMembers("allUsers")
+                            .build()))
+                .build());
+      } catch (StorageException storageException) {
+        fail("pap: expected adding allUsers policy to bucket to succeed");
+      }
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, papBucket, 1, TimeUnit.MINUTES);
+    }
+  }
+
+  @Test
+  public void testUBLAWithPublicAccessPreventionOnBucket() throws Exception {
+    String papBucket = RemoteStorageHelper.generateBucketName();
+    try {
+      Bucket bucket = generatePublicAccessPreventionBucket(papBucket, false);
+      assertEquals(
+          bucket.getIamConfiguration().getPublicAccessPrevention(),
+          BucketInfo.PublicAccessPrevention.INHERITED);
+      assertFalse(bucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
+      assertFalse(bucket.getIamConfiguration().isBucketPolicyOnlyEnabled());
+
+      // Update PAP setting to ENFORCED and should not affect UBLA setting.
+      bucket
+          .toBuilder()
+          .setIamConfiguration(
+              bucket
+                  .getIamConfiguration()
+                  .toBuilder()
+                  .setPublicAccessPrevention(BucketInfo.PublicAccessPrevention.ENFORCED)
+                  .build())
+          .build()
+          .update();
+      bucket = storage.get(papBucket, Storage.BucketGetOption.fields(BucketField.IAMCONFIGURATION));
+      assertEquals(
+          bucket.getIamConfiguration().getPublicAccessPrevention(),
+          BucketInfo.PublicAccessPrevention.ENFORCED);
+      assertFalse(bucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
+      assertFalse(bucket.getIamConfiguration().isBucketPolicyOnlyEnabled());
+
+      // Updating UBLA should not affect PAP setting.
+      bucket =
+          bucket
+              .toBuilder()
+              .setIamConfiguration(
+                  bucket
+                      .getIamConfiguration()
+                      .toBuilder()
+                      .setIsUniformBucketLevelAccessEnabled(true)
+                      .build())
+              .build()
+              .update();
+      assertTrue(bucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
+      assertTrue(bucket.getIamConfiguration().isBucketPolicyOnlyEnabled());
+      assertEquals(
+          bucket.getIamConfiguration().getPublicAccessPrevention(),
+          BucketInfo.PublicAccessPrevention.ENFORCED);
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, papBucket, 1, TimeUnit.MINUTES);
+    }
+  }
+
+  @Test
+  public void testListBucketRequesterPaysFails() throws InterruptedException {
+    String projectId = storage.getOptions().getProjectId();
+    Iterator<Bucket> bucketIterator =
+        storage
+            .list(
+                Storage.BucketListOption.prefix(bucketName),
+                Storage.BucketListOption.fields(),
+                Storage.BucketListOption.userProject(projectId))
+            .iterateAll()
+            .iterator();
+    while (!bucketIterator.hasNext()) {
+      Thread.sleep(500);
+      bucketIterator =
+          storage
+              .list(Storage.BucketListOption.prefix(bucketName), Storage.BucketListOption.fields())
+              .iterateAll()
+              .iterator();
+    }
+    while (bucketIterator.hasNext()) {
+      Bucket remoteBucket = bucketIterator.next();
+      assertTrue(remoteBucket.getName().startsWith(bucketName));
+      assertNull(remoteBucket.getCreateTime());
+      assertNull(remoteBucket.getSelfLink());
+    }
+  }
+
+  @Test
+  public void testRetentionPolicyNoLock() throws ExecutionException, InterruptedException {
+    String bucketName = RemoteStorageHelper.generateBucketName();
+    Bucket remoteBucket =
+        storage.create(
+            BucketInfo.newBuilder(bucketName).setRetentionPeriod(RETENTION_PERIOD).build());
+    try {
+      assertEquals(RETENTION_PERIOD, remoteBucket.getRetentionPeriod());
+      assertNotNull(remoteBucket.getRetentionEffectiveTime());
+      assertNull(remoteBucket.retentionPolicyIsLocked());
+      remoteBucket =
+          storage.get(bucketName, Storage.BucketGetOption.fields(BucketField.RETENTION_POLICY));
+      assertEquals(RETENTION_PERIOD, remoteBucket.getRetentionPeriod());
+      assertNotNull(remoteBucket.getRetentionEffectiveTime());
+      assertNull(remoteBucket.retentionPolicyIsLocked());
+      String blobName = "test-create-with-retention-policy-hold";
+      BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, blobName).build();
+      Blob remoteBlob = storage.create(blobInfo);
+      assertNotNull(remoteBlob.getRetentionExpirationTime());
+      remoteBucket = remoteBucket.toBuilder().setRetentionPeriod(null).build().update();
+      assertNull(remoteBucket.getRetentionPeriod());
+      remoteBucket = remoteBucket.toBuilder().setRetentionPeriod(null).build().update();
+      assertNull(remoteBucket.getRetentionPeriod());
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
+    }
+  }
+
+  private static void unsetRequesterPays() {
+    Bucket remoteBucket =
+        storage.get(
+            requesterPaysBucketName,
+            Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING),
+            Storage.BucketGetOption.userProject(storage.getOptions().getProjectId()));
+    // Disable requester pays in case a test fails to clean up.
+    if (remoteBucket.requesterPays() != null && remoteBucket.requesterPays() == true) {
+      remoteBucket
+          .toBuilder()
+          .setRequesterPays(false)
+          .build()
+          .update(Storage.BucketTargetOption.userProject(storage.getOptions().getProjectId()));
+    }
+  }
+
+  private Bucket generatePublicAccessPreventionBucket(String bucketName, boolean enforced) {
+    return storage.create(
+        Bucket.newBuilder(bucketName)
+            .setIamConfiguration(
+                BucketInfo.IamConfiguration.newBuilder()
+                    .setPublicAccessPrevention(
+                        enforced
+                            ? BucketInfo.PublicAccessPrevention.ENFORCED
+                            : BucketInfo.PublicAccessPrevention.INHERITED)
+                    .build())
+            .build());
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBatchTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBatchTest.java
new file mode 100644
index 00000000..711840b2
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBatchTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.BlobId;
+import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.BucketFixture;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageBatch;
+import com.google.cloud.storage.StorageBatchResult;
+import com.google.cloud.storage.StorageException;
+import com.google.cloud.storage.StorageFixture;
+import com.google.common.collect.Lists;
+import java.util.List;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+public class ITBatchTest {
+  private static final int MAX_BATCH_SIZE = 100;
+  private static final String CONTENT_TYPE = "text/plain";
+
+  private static Storage storage;
+  private static String bucketName;
+
+  @ClassRule(order = 1)
+  public static final StorageFixture storageFixture = StorageFixture.defaultHttp();
+
+  @ClassRule(order = 2)
+  public static final BucketFixture bucketFixture =
+      BucketFixture.newBuilder().setHandle(storageFixture::getInstance).build();
+
+  @BeforeClass
+  public static void setUp() {
+    storage = storageFixture.getInstance();
+    bucketName = bucketFixture.getBucketInfo().getName();
+  }
+
+  @Test
+  public void testBatchRequest() {
+    String sourceBlobName1 = "test-batch-request-blob-1";
+    String sourceBlobName2 = "test-batch-request-blob-2";
+    BlobInfo sourceBlob1 = BlobInfo.newBuilder(bucketName, sourceBlobName1).build();
+    BlobInfo sourceBlob2 = BlobInfo.newBuilder(bucketName, sourceBlobName2).build();
+    assertNotNull(storage.create(sourceBlob1));
+    assertNotNull(storage.create(sourceBlob2));
+
+    // Batch update request
+    BlobInfo updatedBlob1 = sourceBlob1.toBuilder().setContentType(CONTENT_TYPE).build();
+    BlobInfo updatedBlob2 = sourceBlob2.toBuilder().setContentType(CONTENT_TYPE).build();
+    StorageBatch updateBatch = storage.batch();
+    StorageBatchResult<Blob> updateResult1 = updateBatch.update(updatedBlob1);
+    StorageBatchResult<Blob> updateResult2 = updateBatch.update(updatedBlob2);
+    updateBatch.submit();
+    Blob remoteUpdatedBlob1 = updateResult1.get();
+    Blob remoteUpdatedBlob2 = updateResult2.get();
+    assertEquals(sourceBlob1.getBucket(), remoteUpdatedBlob1.getBucket());
+    assertEquals(sourceBlob1.getName(), remoteUpdatedBlob1.getName());
+    assertEquals(sourceBlob2.getBucket(), remoteUpdatedBlob2.getBucket());
+    assertEquals(sourceBlob2.getName(), remoteUpdatedBlob2.getName());
+    assertEquals(updatedBlob1.getContentType(), remoteUpdatedBlob1.getContentType());
+    assertEquals(updatedBlob2.getContentType(), remoteUpdatedBlob2.getContentType());
+
+    // Batch get request
+    StorageBatch getBatch = storage.batch();
+    StorageBatchResult<Blob> getResult1 = getBatch.get(bucketName, sourceBlobName1);
+    StorageBatchResult<Blob> getResult2 = getBatch.get(bucketName, sourceBlobName2);
+    getBatch.submit();
+    Blob remoteBlob1 = getResult1.get();
+    Blob remoteBlob2 = getResult2.get();
+    assertEquals(remoteUpdatedBlob1, remoteBlob1);
+    assertEquals(remoteUpdatedBlob2, remoteBlob2);
+
+    // Batch delete request
+    StorageBatch deleteBatch = storage.batch();
+    StorageBatchResult<Boolean> deleteResult1 = deleteBatch.delete(bucketName, sourceBlobName1);
+    StorageBatchResult<Boolean> deleteResult2 = deleteBatch.delete(bucketName, sourceBlobName2);
+    deleteBatch.submit();
+    assertTrue(deleteResult1.get());
+    assertTrue(deleteResult2.get());
+  }
+
+  @Test
+  public void testBatchRequestManyOperations() {
+    List<StorageBatchResult<Boolean>> deleteResults =
+        Lists.newArrayListWithCapacity(MAX_BATCH_SIZE);
+    List<StorageBatchResult<Blob>> getResults = Lists.newArrayListWithCapacity(MAX_BATCH_SIZE / 2);
+    List<StorageBatchResult<Blob>> updateResults =
+        Lists.newArrayListWithCapacity(MAX_BATCH_SIZE / 2);
+    StorageBatch batch = storage.batch();
+    for (int i = 0; i < MAX_BATCH_SIZE; i++) {
+      BlobId blobId = BlobId.of(bucketName, "test-batch-request-many-operations-blob-" + i);
+      deleteResults.add(batch.delete(blobId));
+    }
+    for (int i = 0; i < MAX_BATCH_SIZE / 2; i++) {
+      BlobId blobId = BlobId.of(bucketName, "test-batch-request-many-operations-blob-" + i);
+      getResults.add(batch.get(blobId));
+    }
+    for (int i = 0; i < MAX_BATCH_SIZE / 2; i++) {
+      BlobInfo blob =
+          BlobInfo.newBuilder(BlobId.of(bucketName, "test-batch-request-many-operations-blob-" + i))
+              .build();
+      updateResults.add(batch.update(blob));
+    }
+
+    String sourceBlobName1 = "test-batch-request-many-operations-source-blob-1";
+    String sourceBlobName2 = "test-batch-request-many-operations-source-blob-2";
+    BlobInfo sourceBlob1 = BlobInfo.newBuilder(bucketName, sourceBlobName1).build();
+    BlobInfo sourceBlob2 = BlobInfo.newBuilder(bucketName, sourceBlobName2).build();
+    assertNotNull(storage.create(sourceBlob1));
+    assertNotNull(storage.create(sourceBlob2));
+    BlobInfo updatedBlob2 = sourceBlob2.toBuilder().setContentType(CONTENT_TYPE).build();
+
+    StorageBatchResult<Blob> getResult = batch.get(bucketName, sourceBlobName1);
+    StorageBatchResult<Blob> updateResult = batch.update(updatedBlob2);
+
+    batch.submit();
+
+    // Check deletes
+    for (StorageBatchResult<Boolean> failedDeleteResult : deleteResults) {
+      assertFalse(failedDeleteResult.get());
+    }
+
+    // Check gets
+    for (StorageBatchResult<Blob> failedGetResult : getResults) {
+      assertNull(failedGetResult.get());
+    }
+    Blob remoteBlob1 = getResult.get();
+    assertEquals(sourceBlob1.getBucket(), remoteBlob1.getBucket());
+    assertEquals(sourceBlob1.getName(), remoteBlob1.getName());
+
+    // Check updates
+    for (StorageBatchResult<Blob> failedUpdateResult : updateResults) {
+      try {
+        failedUpdateResult.get();
+        fail("Expected StorageException");
+      } catch (StorageException ex) {
+        // expected
+      }
+    }
+    Blob remoteUpdatedBlob2 = updateResult.get();
+    assertEquals(sourceBlob2.getBucket(), remoteUpdatedBlob2.getBucket());
+    assertEquals(sourceBlob2.getName(), remoteUpdatedBlob2.getName());
+    assertEquals(updatedBlob2.getContentType(), remoteUpdatedBlob2.getContentType());
+  }
+
+  @Test
+  public void testBatchRequestFail() {
+    String blobName = "test-batch-request-blob-fail";
+    BlobInfo blob = BlobInfo.newBuilder(bucketName, blobName).build();
+    Blob remoteBlob = storage.create(blob);
+    assertNotNull(remoteBlob);
+    BlobInfo updatedBlob = BlobInfo.newBuilder(bucketName, blobName, -1L).build();
+    StorageBatch batch = storage.batch();
+    StorageBatchResult<Blob> updateResult =
+        batch.update(updatedBlob, Storage.BlobTargetOption.generationMatch());
+    StorageBatchResult<Boolean> deleteResult1 =
+        batch.delete(bucketName, blobName, Storage.BlobSourceOption.generationMatch(-1L));
+    StorageBatchResult<Boolean> deleteResult2 = batch.delete(BlobId.of(bucketName, blobName, -1L));
+    StorageBatchResult<Blob> getResult1 =
+        batch.get(bucketName, blobName, Storage.BlobGetOption.generationMatch(-1L));
+    StorageBatchResult<Blob> getResult2 = batch.get(BlobId.of(bucketName, blobName, -1L));
+    batch.submit();
+    try {
+      updateResult.get();
+      fail("Expected StorageException");
+    } catch (StorageException ex) {
+      // expected
+    }
+    try {
+      deleteResult1.get();
+      fail("Expected StorageException");
+    } catch (StorageException ex) {
+      // expected
+    }
+    try {
+      deleteResult2.get();
+      fail("Expected an 'Invalid argument' exception");
+    } catch (StorageException e) {
+      assertThat(e.getMessage()).contains("Invalid argument");
+    }
+    try {
+      getResult1.get();
+      fail("Expected StorageException");
+    } catch (StorageException ex) {
+      // expected
+    }
+    try {
+      getResult2.get();
+      fail("Expected an 'Invalid argument' exception");
+    } catch (StorageException e) {
+      assertThat(e.getMessage()).contains("Invalid argument");
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketLifecycleTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketLifecycleTest.java
new file mode 100644
index 00000000..00d886af
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketLifecycleTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.BucketInfo.LifecycleRule;
+import com.google.cloud.storage.BucketInfo.LifecycleRule.AbortIncompleteMPUAction;
+import com.google.cloud.storage.BucketInfo.LifecycleRule.LifecycleAction;
+import com.google.cloud.storage.BucketInfo.LifecycleRule.LifecycleCondition;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.Storage.BucketField;
+import com.google.cloud.storage.StorageClass;
+import com.google.cloud.storage.StorageFixture;
+import com.google.cloud.storage.testing.RemoteStorageHelper;
+import com.google.common.collect.ImmutableList;
+import java.time.OffsetDateTime;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+public class ITBucketLifecycleTest {
+
+  @ClassRule public static final StorageFixture storageFixture = StorageFixture.defaultHttp();
+
+  private static final LifecycleRule LIFECYCLE_RULE_1 =
+      new LifecycleRule(
+          LifecycleAction.newSetStorageClassAction(StorageClass.COLDLINE),
+          LifecycleCondition.newBuilder()
+              .setAge(1)
+              .setNumberOfNewerVersions(3)
+              .setIsLive(false)
+              .setMatchesStorageClass(ImmutableList.of(StorageClass.COLDLINE))
+              .build());
+  private static final LifecycleRule LIFECYCLE_RULE_2 =
+      new LifecycleRule(
+          LifecycleAction.newDeleteAction(), LifecycleCondition.newBuilder().setAge(1).build());
+  private static final ImmutableList<LifecycleRule> LIFECYCLE_RULES =
+      ImmutableList.of(LIFECYCLE_RULE_1, LIFECYCLE_RULE_2);
+
+  private static Storage storage;
+
+  @BeforeClass
+  public static void setup() {
+    storage = storageFixture.getInstance();
+  }
+
+  @Test
+  public void testGetBucketLifecycleRules() {
+    String lifecycleTestBucketName = RemoteStorageHelper.generateBucketName();
+    storage.create(
+        BucketInfo.newBuilder(lifecycleTestBucketName)
+            .setLocation("us")
+            .setLifecycleRules(
+                ImmutableList.of(
+                    new LifecycleRule(
+                        LifecycleAction.newSetStorageClassAction(StorageClass.COLDLINE),
+                        LifecycleCondition.newBuilder()
+                            .setAge(1)
+                            .setNumberOfNewerVersions(3)
+                            .setIsLive(false)
+                            .setCreatedBeforeOffsetDateTime(OffsetDateTime.now())
+                            .setMatchesStorageClass(ImmutableList.of(StorageClass.COLDLINE))
+                            .setDaysSinceNoncurrentTime(30)
+                            .setNoncurrentTimeBeforeOffsetDateTime(OffsetDateTime.now())
+                            .setCustomTimeBeforeOffsetDateTime(OffsetDateTime.now())
+                            .setDaysSinceCustomTime(30)
+                            .build())))
+            .build());
+    Bucket remoteBucket =
+        storage.get(lifecycleTestBucketName, Storage.BucketGetOption.fields(BucketField.LIFECYCLE));
+    LifecycleRule lifecycleRule = remoteBucket.getLifecycleRules().get(0);
+    try {
+      assertTrue(
+          lifecycleRule
+              .getAction()
+              .getActionType()
+              .equals(LifecycleRule.SetStorageClassLifecycleAction.TYPE));
+      assertEquals(3, lifecycleRule.getCondition().getNumberOfNewerVersions().intValue());
+      assertNotNull(lifecycleRule.getCondition().getCreatedBeforeOffsetDateTime());
+      assertFalse(lifecycleRule.getCondition().getIsLive());
+      assertEquals(1, lifecycleRule.getCondition().getAge().intValue());
+      assertEquals(1, lifecycleRule.getCondition().getMatchesStorageClass().size());
+      assertEquals(30, lifecycleRule.getCondition().getDaysSinceNoncurrentTime().intValue());
+      assertNotNull(lifecycleRule.getCondition().getNoncurrentTimeBeforeOffsetDateTime());
+      assertEquals(30, lifecycleRule.getCondition().getDaysSinceCustomTime().intValue());
+      assertNotNull(lifecycleRule.getCondition().getCustomTimeBeforeOffsetDateTime());
+    } finally {
+      storage.delete(lifecycleTestBucketName);
+    }
+  }
+
+  @Test
+  public void testGetBucketAbortMPULifecycle() {
+    String lifecycleTestBucketName = RemoteStorageHelper.generateBucketName();
+    storage.create(
+        BucketInfo.newBuilder(lifecycleTestBucketName)
+            .setLocation("us")
+            .setLifecycleRules(
+                ImmutableList.of(
+                    new LifecycleRule(
+                        LifecycleAction.newAbortIncompleteMPUploadAction(),
+                        LifecycleCondition.newBuilder().setAge(1).build())))
+            .build());
+    Bucket remoteBucket =
+        storage.get(lifecycleTestBucketName, Storage.BucketGetOption.fields(BucketField.LIFECYCLE));
+    LifecycleRule lifecycleRule = remoteBucket.getLifecycleRules().get(0);
+    try {
+      assertEquals(AbortIncompleteMPUAction.TYPE, lifecycleRule.getAction().getActionType());
+      assertEquals(1, lifecycleRule.getCondition().getAge().intValue());
+    } finally {
+      storage.delete(lifecycleTestBucketName);
+    }
+  }
+
+  @Test
+  public void testDeleteLifecycleRules() throws ExecutionException, InterruptedException {
+    String bucketName = RemoteStorageHelper.generateBucketName();
+    Bucket bucket =
+        storage.create(
+            BucketInfo.newBuilder(bucketName)
+                .setLocation("us")
+                .setLifecycleRules(LIFECYCLE_RULES)
+                .build());
+    assertThat(bucket.getLifecycleRules()).isNotNull();
+    assertThat(bucket.getLifecycleRules()).hasSize(2);
+    try {
+      Bucket updatedBucket = bucket.toBuilder().deleteLifecycleRules().build().update();
+      assertThat(updatedBucket.getLifecycleRules()).hasSize(0);
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketTest.java
new file mode 100644
index 00000000..1b6a98f7
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketTest.java
@@ -0,0 +1,379 @@
+/*
+ * 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 static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.cloud.Policy;
+import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.BucketFixture;
+import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.BucketInfo.CustomPlacementConfig;
+import com.google.cloud.storage.Cors;
+import com.google.cloud.storage.HttpMethod;
+import com.google.cloud.storage.Rpo;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.Storage.BlobField;
+import com.google.cloud.storage.Storage.BucketField;
+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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+public class ITBucketTest {
+
+  @ClassRule(order = 1)
+  public static final StorageFixture storageFixture = StorageFixture.defaultHttp();
+
+  @ClassRule(order = 2)
+  public static final BucketFixture bucketFixture =
+      BucketFixture.newBuilder().setHandle(storageFixture::getInstance).build();
+
+  @ClassRule(order = 3)
+  public static final BucketFixture requesterPaysFixture =
+      BucketFixture.newBuilder().setHandle(storageFixture::getInstance).build();
+
+  private static Storage storage;
+  private static String bucketName;
+  private static String requesterPaysBucketName;
+
+  private static final byte[] BLOB_BYTE_CONTENT = {0xD, 0xE, 0xA, 0xD};
+  private static final Map<String, String> BUCKET_LABELS = ImmutableMap.of("label1", "value1");
+  private static final Long RETENTION_PERIOD = 5L;
+
+  @BeforeClass
+  public static void setup() {
+    storage = storageFixture.getInstance();
+    bucketName = bucketFixture.getBucketInfo().getName();
+    requesterPaysBucketName = requesterPaysFixture.getBucketInfo().getName();
+  }
+
+  @AfterClass
+  public static void cleanup() {
+    unsetRequesterPays();
+  }
+
+  @Test(timeout = 5000)
+  public void testListBuckets() throws InterruptedException {
+    Iterator<Bucket> bucketIterator =
+        storage
+            .list(Storage.BucketListOption.prefix(bucketName), Storage.BucketListOption.fields())
+            .iterateAll()
+            .iterator();
+    while (!bucketIterator.hasNext()) {
+      Thread.sleep(500);
+      bucketIterator =
+          storage
+              .list(Storage.BucketListOption.prefix(bucketName), Storage.BucketListOption.fields())
+              .iterateAll()
+              .iterator();
+    }
+    while (bucketIterator.hasNext()) {
+      Bucket remoteBucket = bucketIterator.next();
+      assertTrue(remoteBucket.getName().startsWith(bucketName));
+      assertNull(remoteBucket.getCreateTime());
+      assertNull(remoteBucket.getSelfLink());
+    }
+  }
+
+  @Test
+  public void testGetBucketSelectedFields() {
+    Bucket remoteBucket = storage.get(bucketName, Storage.BucketGetOption.fields(BucketField.ID));
+    assertEquals(bucketName, remoteBucket.getName());
+    assertNull(remoteBucket.getCreateTime());
+    assertNotNull(remoteBucket.getGeneratedId());
+  }
+
+  @Test
+  public void testGetBucketAllSelectedFields() {
+    Bucket remoteBucket =
+        storage.get(bucketName, Storage.BucketGetOption.fields(BucketField.values()));
+    assertEquals(bucketName, remoteBucket.getName());
+    assertNotNull(remoteBucket.getCreateTime());
+    assertNotNull(remoteBucket.getSelfLink());
+  }
+
+  @Test
+  public void testBucketLocationType() {
+    String bucketName = RemoteStorageHelper.generateBucketName();
+    Bucket bucket = storage.create(BucketInfo.newBuilder(bucketName).setLocation("us").build());
+    assertEquals("multi-region", bucket.getLocationType());
+  }
+
+  @Test
+  public void testBucketCustomPlacmentConfigDualRegion() {
+    String bucketName = RemoteStorageHelper.generateBucketName();
+    List<String> locations = new ArrayList<>();
+    locations.add("US-EAST1");
+    locations.add("US-WEST1");
+    CustomPlacementConfig customPlacementConfig =
+        CustomPlacementConfig.newBuilder().setDataLocations(locations).build();
+    Bucket bucket =
+        storage.create(
+            BucketInfo.newBuilder(bucketName)
+                .setCustomPlacementConfig(customPlacementConfig)
+                .setLocation("us")
+                .build());
+    assertTrue(bucket.getCustomPlacementConfig().getDataLocations().contains("US-EAST1"));
+    assertTrue(bucket.getCustomPlacementConfig().getDataLocations().contains("US-WEST1"));
+    assertTrue(bucket.getLocation().equalsIgnoreCase("us"));
+  }
+
+  @Test
+  public void testBucketLogging() throws ExecutionException, InterruptedException {
+    String logsBucket = RemoteStorageHelper.generateBucketName();
+    String loggingBucket = RemoteStorageHelper.generateBucketName();
+    try {
+      assertNotNull(storage.create(BucketInfo.newBuilder(logsBucket).setLocation("us").build()));
+      Policy policy = storage.getIamPolicy(logsBucket);
+      assertNotNull(policy);
+      BucketInfo.Logging logging =
+          BucketInfo.Logging.newBuilder()
+              .setLogBucket(logsBucket)
+              .setLogObjectPrefix("test-logs")
+              .build();
+      Bucket bucket =
+          storage.create(
+              BucketInfo.newBuilder(loggingBucket).setLocation("us").setLogging(logging).build());
+      assertEquals(logsBucket, bucket.getLogging().getLogBucket());
+      assertEquals("test-logs", bucket.getLogging().getLogObjectPrefix());
+
+      // Disable bucket logging.
+      Bucket updatedBucket = bucket.toBuilder().setLogging(null).build().update();
+      assertNull(updatedBucket.getLogging());
+
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, logsBucket, 5, TimeUnit.SECONDS);
+      RemoteStorageHelper.forceDelete(storage, loggingBucket, 5, TimeUnit.SECONDS);
+    }
+  }
+
+  @Test
+  public void testRemoveBucketCORS() throws ExecutionException, InterruptedException {
+    String bucketName = RemoteStorageHelper.generateBucketName();
+    List<Cors.Origin> origins = ImmutableList.of(Cors.Origin.of("http://cloud.google.com"));
+    List<HttpMethod> httpMethods = ImmutableList.of(HttpMethod.GET);
+    List<String> responseHeaders = ImmutableList.of("Content-Type");
+    try {
+      Cors cors =
+          Cors.newBuilder()
+              .setOrigins(origins)
+              .setMethods(httpMethods)
+              .setResponseHeaders(responseHeaders)
+              .setMaxAgeSeconds(100)
+              .build();
+      storage.create(BucketInfo.newBuilder(bucketName).setCors(ImmutableList.of(cors)).build());
+
+      // case-1 : Cors are set and field selector is selected then returns not-null.
+      Bucket remoteBucket =
+          storage.get(bucketName, Storage.BucketGetOption.fields(BucketField.CORS));
+      assertThat(remoteBucket.getCors()).isNotNull();
+      assertThat(remoteBucket.getCors().get(0).getMaxAgeSeconds()).isEqualTo(100);
+      assertThat(remoteBucket.getCors().get(0).getMethods()).isEqualTo(httpMethods);
+      assertThat(remoteBucket.getCors().get(0).getOrigins()).isEqualTo(origins);
+      assertThat(remoteBucket.getCors().get(0).getResponseHeaders()).isEqualTo(responseHeaders);
+
+      // case-2 : Cors are set but field selector isn't selected then returns not-null.
+      remoteBucket = storage.get(bucketName);
+      assertThat(remoteBucket.getCors()).isNotNull();
+
+      // Remove CORS configuration from the bucket.
+      Bucket updatedBucket = remoteBucket.toBuilder().setCors(null).build().update();
+      assertThat(updatedBucket.getCors()).isNull();
+
+      // case-3 : Cors are not set and field selector is selected then returns null.
+      updatedBucket = storage.get(bucketName, Storage.BucketGetOption.fields(BucketField.CORS));
+      assertThat(updatedBucket.getCors()).isNull();
+
+      // case-4 : Cors are not set and field selector isn't selected then returns null.
+      updatedBucket = storage.get(bucketName);
+      assertThat(updatedBucket.getCors()).isNull();
+
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
+    }
+  }
+
+  @Test
+  public void testRpoConfig() {
+    String rpoBucket = RemoteStorageHelper.generateBucketName();
+    try {
+      Bucket bucket =
+          storage.create(
+              BucketInfo.newBuilder(rpoBucket).setLocation("NAM4").setRpo(Rpo.ASYNC_TURBO).build());
+      assertEquals("ASYNC_TURBO", bucket.getRpo().toString());
+
+      bucket.toBuilder().setRpo(Rpo.DEFAULT).build().update();
+
+      assertEquals("DEFAULT", storage.get(rpoBucket).getRpo().toString());
+    } finally {
+      storage.delete(rpoBucket);
+    }
+  }
+
+  @Test
+  public void testRetentionPolicyLock() throws ExecutionException, InterruptedException {
+    retentionPolicyLockRequesterPays(true);
+    retentionPolicyLockRequesterPays(false);
+  }
+
+  private void retentionPolicyLockRequesterPays(boolean requesterPays)
+      throws ExecutionException, InterruptedException {
+    String projectId = storage.getOptions().getProjectId();
+    String bucketName = RemoteStorageHelper.generateBucketName();
+    BucketInfo bucketInfo;
+    if (requesterPays) {
+      bucketInfo =
+          BucketInfo.newBuilder(bucketName)
+              .setRetentionPeriod(RETENTION_PERIOD)
+              .setRequesterPays(true)
+              .build();
+    } else {
+      bucketInfo = BucketInfo.newBuilder(bucketName).setRetentionPeriod(RETENTION_PERIOD).build();
+    }
+    Bucket remoteBucket = storage.create(bucketInfo);
+    try {
+      assertNull(remoteBucket.retentionPolicyIsLocked());
+      assertNotNull(remoteBucket.getRetentionEffectiveTime());
+      assertNotNull(remoteBucket.getMetageneration());
+      if (requesterPays) {
+        remoteBucket =
+            storage.lockRetentionPolicy(
+                remoteBucket,
+                Storage.BucketTargetOption.metagenerationMatch(),
+                Storage.BucketTargetOption.userProject(projectId));
+      } else {
+        remoteBucket =
+            storage.lockRetentionPolicy(
+                remoteBucket, Storage.BucketTargetOption.metagenerationMatch());
+      }
+      assertTrue(remoteBucket.retentionPolicyIsLocked());
+      assertNotNull(remoteBucket.getRetentionEffectiveTime());
+    } finally {
+      if (requesterPays) {
+        bucketInfo = bucketInfo.toBuilder().setRequesterPays(false).build();
+        Bucket updateBucket =
+            storage.update(bucketInfo, Storage.BucketTargetOption.userProject(projectId));
+        assertFalse(updateBucket.requesterPays());
+      }
+      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS, projectId);
+    }
+  }
+
+  @Test
+  public void testUpdateBucketLabel() {
+    Bucket remoteBucket =
+        storage.get(
+            bucketName, Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING));
+    assertNull(remoteBucket.getLabels());
+    remoteBucket = remoteBucket.toBuilder().setLabels(BUCKET_LABELS).build();
+    Bucket updatedBucket = storage.update(remoteBucket);
+    assertEquals(BUCKET_LABELS, updatedBucket.getLabels());
+    remoteBucket.toBuilder().setLabels(Collections.emptyMap()).build().update();
+    assertNull(storage.get(bucketName).getLabels());
+  }
+
+  @Test
+  public void testUpdateBucketRequesterPays() {
+    unsetRequesterPays();
+    Bucket remoteBucket =
+        storage.get(
+            requesterPaysBucketName,
+            Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING));
+    assertTrue(remoteBucket.requesterPays() == null || !remoteBucket.requesterPays());
+    remoteBucket = remoteBucket.toBuilder().setRequesterPays(true).build();
+    Bucket updatedBucket = storage.update(remoteBucket);
+    assertTrue(updatedBucket.requesterPays());
+
+    String projectId = storage.getOptions().getProjectId();
+    Bucket.BlobTargetOption option = Bucket.BlobTargetOption.userProject(projectId);
+    String blobName = "test-create-empty-blob-requester-pays";
+    Blob remoteBlob = updatedBucket.create(blobName, BLOB_BYTE_CONTENT, option);
+    assertNotNull(remoteBlob);
+    byte[] readBytes =
+        storage.readAllBytes(
+            requesterPaysBucketName, blobName, Storage.BlobSourceOption.userProject(projectId));
+    assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
+    remoteBucket = remoteBucket.toBuilder().setRequesterPays(false).build();
+    updatedBucket = storage.update(remoteBucket, Storage.BucketTargetOption.userProject(projectId));
+    assertFalse(updatedBucket.requesterPays());
+  }
+
+  @Test
+  public void testEnableDisableBucketDefaultEventBasedHold()
+      throws ExecutionException, InterruptedException {
+    String bucketName = RemoteStorageHelper.generateBucketName();
+    Bucket remoteBucket =
+        storage.create(BucketInfo.newBuilder(bucketName).setDefaultEventBasedHold(true).build());
+    try {
+      assertTrue(remoteBucket.getDefaultEventBasedHold());
+      remoteBucket =
+          storage.get(
+              bucketName, Storage.BucketGetOption.fields(BucketField.DEFAULT_EVENT_BASED_HOLD));
+      assertTrue(remoteBucket.getDefaultEventBasedHold());
+      String blobName = "test-create-with-event-based-hold";
+      BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, blobName).build();
+      Blob remoteBlob = storage.create(blobInfo);
+      assertTrue(remoteBlob.getEventBasedHold());
+      remoteBlob =
+          storage.get(
+              blobInfo.getBlobId(), Storage.BlobGetOption.fields(BlobField.EVENT_BASED_HOLD));
+      assertTrue(remoteBlob.getEventBasedHold());
+      remoteBlob = remoteBlob.toBuilder().setEventBasedHold(false).build().update();
+      assertFalse(remoteBlob.getEventBasedHold());
+      remoteBucket = remoteBucket.toBuilder().setDefaultEventBasedHold(false).build().update();
+      assertFalse(remoteBucket.getDefaultEventBasedHold());
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
+    }
+  }
+
+  private static void unsetRequesterPays() {
+    Bucket remoteBucket =
+        storage.get(
+            requesterPaysBucketName,
+            Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING),
+            Storage.BucketGetOption.userProject(storage.getOptions().getProjectId()));
+    // Disable requester pays in case a test fails to clean up.
+    if (remoteBucket.requesterPays() != null && remoteBucket.requesterPays() == true) {
+      remoteBucket
+          .toBuilder()
+          .setRequesterPays(false)
+          .build()
+          .update(Storage.BucketTargetOption.userProject(storage.getOptions().getProjectId()));
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITDownloadToTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITDownloadToTest.java
index 0a1a37c7..0dd743f6 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITDownloadToTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITDownloadToTest.java
@@ -24,14 +24,12 @@ import com.google.cloud.storage.BlobInfo;
 import com.google.cloud.storage.BucketFixture;
 import com.google.cloud.storage.Storage;
 import com.google.cloud.storage.StorageFixture;
-import java.io.ByteArrayOutputStream;
+import com.google.cloud.storage.TestUtils;
 import java.io.File;
 import java.io.IOException;
-import java.io.OutputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Arrays;
-import java.util.zip.GZIPOutputStream;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Test;
@@ -46,7 +44,7 @@ public final class ITDownloadToTest {
       BucketFixture.newBuilder().setHandle(storageFixture::getInstance).build();
 
   private static final byte[] helloWorldTextBytes = "hello world".getBytes();
-  private static final byte[] helloWorldGzipBytes = gzipBytes(helloWorldTextBytes);
+  private static final byte[] helloWorldGzipBytes = TestUtils.gzipBytes(helloWorldTextBytes);
 
   private static Storage storage;
   private static BlobId blobId;
@@ -82,14 +80,4 @@ public final class ITDownloadToTest {
     byte[] actualTxtBytes = Files.readAllBytes(helloWorldTxt);
     assertThat(actualTxtBytes).isEqualTo(helloWorldTextBytes);
   }
-
-  private static byte[] gzipBytes(byte[] bytes) {
-    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-    try (OutputStream out = new GZIPOutputStream(byteArrayOutputStream)) {
-      out.write(bytes);
-    } catch (IOException ignore) {
-    }
-
-    return byteArrayOutputStream.toByteArray();
-  }
 }
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
index 4147f68b..cca6645d 100644
--- 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
@@ -24,12 +24,19 @@ import com.google.cloud.storage.Blob;
 import com.google.cloud.storage.Bucket;
 import com.google.cloud.storage.BucketFixture;
 import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.HmacKey;
+import com.google.cloud.storage.HmacKey.HmacKeyMetadata;
+import com.google.cloud.storage.HmacKey.HmacKeyState;
+import com.google.cloud.storage.ServiceAccount;
+import com.google.cloud.storage.Storage.CreateHmacKeyOption;
+import com.google.cloud.storage.Storage.ListHmacKeysOption;
 import com.google.cloud.storage.StorageFixture;
 import com.google.cloud.storage.StorageOptions;
 import com.google.cloud.storage.conformance.retry.CleanupStrategy;
 import com.google.cloud.storage.conformance.retry.TestBench;
 import com.google.cloud.storage.testing.RemoteStorageHelper;
 import com.google.common.collect.ImmutableList;
+import java.util.stream.IntStream;
 import java.util.stream.StreamSupport;
 import org.junit.ClassRule;
 import org.junit.Test;
@@ -86,4 +93,67 @@ public final class ITGrpcTest {
 
     assertThat(bucketNames).contains(bucketFixture.getBucketInfo().getName());
   }
+
+  @Test
+  public void createHmacKey() {
+    ServiceAccount serviceAccount = ServiceAccount.of("x@y.z");
+    HmacKey hmacKey = storageFixture.getInstance().createHmacKey(serviceAccount);
+    assertThat(hmacKey).isNotNull();
+    assertThat(hmacKey.getSecretKey()).isNotNull();
+    assertThat(hmacKey.getMetadata().getServiceAccount()).isEqualTo(serviceAccount);
+  }
+
+  @Test
+  public void getHmacKey() {
+    ServiceAccount serviceAccount = ServiceAccount.of("x@y.z");
+    HmacKey hmacKey = storageFixture.getInstance().createHmacKey(serviceAccount);
+    HmacKeyMetadata actual =
+        storageFixture.getInstance().getHmacKey(hmacKey.getMetadata().getAccessId());
+    assertThat(actual).isEqualTo(hmacKey.getMetadata());
+  }
+
+  @Test
+  public void listHmacKeys() {
+    ImmutableList<HmacKey> keys =
+        IntStream.rangeClosed(1, 4)
+            .mapToObj(i -> ServiceAccount.of(String.format("x-%d@y.z", i)))
+            .map(
+                sa ->
+                    storageFixture
+                        .getInstance()
+                        .createHmacKey(sa, CreateHmacKeyOption.projectId("proj")))
+            .collect(ImmutableList.toImmutableList());
+
+    ImmutableList<HmacKeyMetadata> expected =
+        keys.stream().map(HmacKey::getMetadata).collect(ImmutableList.toImmutableList());
+
+    Page<HmacKeyMetadata> page =
+        storageFixture.getInstance().listHmacKeys(ListHmacKeysOption.projectId("proj"));
+
+    ImmutableList<HmacKeyMetadata> actual =
+        StreamSupport.stream(page.iterateAll().spliterator(), false)
+            .collect(ImmutableList.toImmutableList());
+
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void updateHmacKey() {
+    ServiceAccount serviceAccount = ServiceAccount.of("x@y.z");
+    HmacKey hmacKey = storageFixture.getInstance().createHmacKey(serviceAccount);
+    HmacKeyMetadata updated =
+        storageFixture
+            .getInstance()
+            .updateHmacKeyState(hmacKey.getMetadata(), HmacKeyState.INACTIVE);
+    assertThat(updated.getServiceAccount()).isEqualTo(serviceAccount);
+    assertThat(updated.getState()).isEqualTo(HmacKeyState.INACTIVE);
+  }
+
+  @Test
+  public void deleteHmacKey() {
+    ServiceAccount serviceAccount = ServiceAccount.of("x@y.z");
+    HmacKey hmacKey = storageFixture.getInstance().createHmacKey(serviceAccount);
+    storageFixture.getInstance().updateHmacKeyState(hmacKey.getMetadata(), HmacKeyState.INACTIVE);
+    storageFixture.getInstance().deleteHmacKey(hmacKey.getMetadata());
+  }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITNotificationTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITNotificationTest.java
new file mode 100644
index 00000000..097ca9ae
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITNotificationTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.ServiceOptions;
+import com.google.cloud.pubsub.v1.TopicAdminClient;
+import com.google.cloud.storage.BucketFixture;
+import com.google.cloud.storage.Notification;
+import com.google.cloud.storage.NotificationInfo;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageFixture;
+import com.google.cloud.storage.testing.RemoteStorageHelper;
+import com.google.common.collect.ImmutableMap;
+import com.google.iam.v1.Binding;
+import com.google.iam.v1.GetIamPolicyRequest;
+import com.google.iam.v1.SetIamPolicyRequest;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+public class ITNotificationTest {
+  @ClassRule(order = 1)
+  public static final StorageFixture storageFixture = StorageFixture.defaultHttp();
+
+  @ClassRule(order = 2)
+  public static final BucketFixture bucketFixture =
+      BucketFixture.newBuilder().setHandle(storageFixture::getInstance).build();
+
+  private static final String PROJECT = ServiceOptions.getDefaultProjectId();
+  private static final String ID = UUID.randomUUID().toString().substring(0, 8);
+  private static final String TOPIC =
+      String.format("projects/%s/topics/test_topic_foo_%s", PROJECT, ID).trim();
+  private static final Notification.PayloadFormat PAYLOAD_FORMAT =
+      Notification.PayloadFormat.JSON_API_V1.JSON_API_V1;
+  private static final Map<String, String> CUSTOM_ATTRIBUTES = ImmutableMap.of("label1", "value1");
+  private static final Logger log = Logger.getLogger(ITNotificationTest.class.getName());
+
+  private static Storage storage;
+  private static String bucketName;
+  private static TopicAdminClient topicAdminClient;
+
+  @BeforeClass
+  public static void setup() throws IOException {
+    storage = storageFixture.getInstance();
+    bucketName = bucketFixture.getBucketInfo().getName();
+    // Configure topic admin client for notification.
+    topicAdminClient = configureTopicAdminClient();
+  }
+
+  @AfterClass
+  public static void cleanup() {
+    /* Delete the Pub/Sub topic */
+    if (topicAdminClient != null) {
+      try {
+        topicAdminClient.deleteTopic(TOPIC);
+        topicAdminClient.close();
+      } catch (Exception e) {
+        log.log(Level.WARNING, "Error while trying to delete topic and shutdown topic client", e);
+      }
+      topicAdminClient = null;
+    }
+  }
+
+  private static TopicAdminClient configureTopicAdminClient() throws IOException {
+    TopicAdminClient topicAdminClient = TopicAdminClient.create();
+    topicAdminClient.createTopic(TOPIC);
+    GetIamPolicyRequest getIamPolicyRequest =
+        GetIamPolicyRequest.newBuilder().setResource(TOPIC).build();
+    com.google.iam.v1.Policy policy = topicAdminClient.getIamPolicy(getIamPolicyRequest);
+    Binding binding =
+        Binding.newBuilder().setRole("roles/owner").addMembers("allAuthenticatedUsers").build();
+    SetIamPolicyRequest setIamPolicyRequest =
+        SetIamPolicyRequest.newBuilder()
+            .setResource(TOPIC)
+            .setPolicy(policy.toBuilder().addBindings(binding).build())
+            .build();
+    topicAdminClient.setIamPolicy(setIamPolicyRequest);
+    return topicAdminClient;
+  }
+
+  @Test
+  public void testNotification() throws InterruptedException, ExecutionException {
+    NotificationInfo notificationInfo =
+        NotificationInfo.newBuilder(TOPIC)
+            .setCustomAttributes(CUSTOM_ATTRIBUTES)
+            .setPayloadFormat(PAYLOAD_FORMAT)
+            .build();
+    try {
+      assertThat(storage.listNotifications(bucketName)).isEmpty();
+      Notification notification = storage.createNotification(bucketName, notificationInfo);
+      assertThat(notification.getNotificationId()).isNotNull();
+      assertThat(CUSTOM_ATTRIBUTES).isEqualTo(notification.getCustomAttributes());
+      assertThat(PAYLOAD_FORMAT.name()).isEqualTo(notification.getPayloadFormat().name());
+      assertThat(notification.getTopic().contains(TOPIC)).isTrue();
+
+      // Gets the notification with the specified id.
+      Notification actualNotification =
+          storage.getNotification(bucketName, notification.getNotificationId());
+      assertThat(actualNotification.getNotificationId())
+          .isEqualTo(notification.getNotificationId());
+      assertThat(actualNotification.getTopic().trim()).isEqualTo(notification.getTopic().trim());
+      assertThat(actualNotification.getEtag()).isEqualTo(notification.getEtag());
+      assertThat(actualNotification.getEventTypes()).isEqualTo(notification.getEventTypes());
+      assertThat(actualNotification.getPayloadFormat()).isEqualTo(notification.getPayloadFormat());
+      assertThat(actualNotification.getSelfLink()).isEqualTo(notification.getSelfLink());
+      assertThat(actualNotification.getCustomAttributes())
+          .isEqualTo(notification.getCustomAttributes());
+
+      // Retrieves the list of notifications associated with the bucket.
+      List<Notification> notifications = storage.listNotifications(bucketName);
+      assertThat(notifications.size()).isEqualTo(1);
+      assertThat(notifications.get(0).getNotificationId())
+          .isEqualTo(actualNotification.getNotificationId());
+
+      // Deletes the notification with the specified id.
+      assertThat(storage.deleteNotification(bucketName, notification.getNotificationId())).isTrue();
+      assertThat(storage.deleteNotification(bucketName, notification.getNotificationId()))
+          .isFalse();
+      assertThat(storage.getNotification(bucketName, notification.getNotificationId())).isNull();
+      assertThat(storage.listNotifications(bucketName)).isEmpty();
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
index 60249afb..f974938c 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
@@ -33,16 +33,11 @@ import com.google.api.client.http.HttpTransport;
 import com.google.api.client.http.apache.ApacheHttpTransport;
 import com.google.api.gax.paging.Page;
 import com.google.auth.http.HttpTransportFactory;
-import com.google.cloud.Condition;
-import com.google.cloud.Identity;
-import com.google.cloud.Policy;
 import com.google.cloud.ReadChannel;
 import com.google.cloud.RestorableState;
-import com.google.cloud.ServiceOptions;
 import com.google.cloud.TransportOptions;
 import com.google.cloud.WriteChannel;
 import com.google.cloud.http.HttpTransportOptions;
-import com.google.cloud.pubsub.v1.TopicAdminClient;
 import com.google.cloud.storage.Acl;
 import com.google.cloud.storage.Acl.Role;
 import com.google.cloud.storage.Acl.User;
@@ -51,32 +46,19 @@ import com.google.cloud.storage.BlobId;
 import com.google.cloud.storage.BlobInfo;
 import com.google.cloud.storage.Bucket;
 import com.google.cloud.storage.BucketInfo;
-import com.google.cloud.storage.BucketInfo.CustomPlacementConfig;
-import com.google.cloud.storage.BucketInfo.LifecycleRule;
-import com.google.cloud.storage.BucketInfo.LifecycleRule.AbortIncompleteMPUAction;
-import com.google.cloud.storage.BucketInfo.LifecycleRule.LifecycleAction;
-import com.google.cloud.storage.BucketInfo.LifecycleRule.LifecycleCondition;
 import com.google.cloud.storage.CopyWriter;
-import com.google.cloud.storage.Cors;
 import com.google.cloud.storage.DataGeneration;
 import com.google.cloud.storage.HmacKey;
 import com.google.cloud.storage.HmacKey.HmacKeyState;
-import com.google.cloud.storage.HttpMethod;
-import com.google.cloud.storage.Notification;
-import com.google.cloud.storage.NotificationInfo;
-import com.google.cloud.storage.Rpo;
 import com.google.cloud.storage.ServiceAccount;
 import com.google.cloud.storage.Storage;
 import com.google.cloud.storage.Storage.BlobField;
 import com.google.cloud.storage.Storage.BlobWriteOption;
 import com.google.cloud.storage.Storage.BucketField;
-import com.google.cloud.storage.StorageBatch;
-import com.google.cloud.storage.StorageBatchResult;
 import com.google.cloud.storage.StorageClass;
 import com.google.cloud.storage.StorageException;
 import com.google.cloud.storage.StorageFixture;
 import com.google.cloud.storage.StorageOptions;
-import com.google.cloud.storage.StorageRoles;
 import com.google.cloud.storage.testing.RemoteStorageHelper;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -85,19 +67,13 @@ import com.google.common.collect.Iterators;
 import com.google.common.collect.Lists;
 import com.google.common.io.BaseEncoding;
 import com.google.common.io.ByteStreams;
-import com.google.iam.v1.Binding;
-import com.google.iam.v1.GetIamPolicyRequest;
-import com.google.iam.v1.SetIamPolicyRequest;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
 import java.security.Key;
-import java.time.OffsetDateTime;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -105,7 +81,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Random;
 import java.util.Set;
-import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
@@ -127,14 +102,12 @@ import org.threeten.bp.Instant;
 public class ITStorageTest {
 
   private static Storage storage;
-  private static TopicAdminClient topicAdminClient;
   private static final Logger log = Logger.getLogger(ITStorageTest.class.getName());
   private static final String BUCKET = RemoteStorageHelper.generateBucketName();
   private static final String BUCKET_REQUESTER_PAYS = RemoteStorageHelper.generateBucketName();
   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 int MAX_BATCH_SIZE = 100;
   private static final String BASE64_KEY = "JVzfVl8NLD9FjedFuStegjRfES5ll5zc59CIXw572OA=";
   private static final String OTHER_BASE64_KEY = "IcOIQGlliNr5pr3vJb63l+XMqc7NjXqjfw/deBoNxPA=";
   private static final Key KEY =
@@ -142,7 +115,6 @@ public class ITStorageTest {
   private static final byte[] COMPRESSED_CONTENT =
       BaseEncoding.base64()
           .decode("H4sIAAAAAAAAAPNIzcnJV3DPz0/PSVVwzskvTVEILskvSkxPVQQA/LySchsAAAA=");
-  private static final Map<String, String> BUCKET_LABELS = ImmutableMap.of("label1", "value1");
   private static final Map<String, String> REMOVE_BUCKET_LABELS;
 
   static {
@@ -157,22 +129,6 @@ public class ITStorageTest {
   private static final boolean IS_VPC_TEST =
       System.getenv("GOOGLE_CLOUD_TESTS_IN_VPCSC") != null
           && System.getenv("GOOGLE_CLOUD_TESTS_IN_VPCSC").equalsIgnoreCase("true");
-  private static final List<String> LOCATION_TYPES =
-      ImmutableList.of("multi-region", "region", "dual-region");
-  private static final LifecycleRule LIFECYCLE_RULE_1 =
-      new LifecycleRule(
-          LifecycleAction.newSetStorageClassAction(StorageClass.COLDLINE),
-          LifecycleCondition.newBuilder()
-              .setAge(1)
-              .setNumberOfNewerVersions(3)
-              .setIsLive(false)
-              .setMatchesStorageClass(ImmutableList.of(StorageClass.COLDLINE))
-              .build());
-  private static final LifecycleRule LIFECYCLE_RULE_2 =
-      new LifecycleRule(
-          LifecycleAction.newDeleteAction(), LifecycleCondition.newBuilder().setAge(1).build());
-  private static final ImmutableList<LifecycleRule> LIFECYCLE_RULES =
-      ImmutableList.of(LIFECYCLE_RULE_1, LIFECYCLE_RULE_2);
 
   @ClassRule
   public static final StorageFixture storageFixture =
@@ -180,32 +136,14 @@ public class ITStorageTest {
 
   @Rule public final TestName testName = new TestName();
   @Rule public final DataGeneration dataGeneration = new DataGeneration(new Random(1234567890));
-  private static final String PROJECT = ServiceOptions.getDefaultProjectId();
-  private static final String ID = UUID.randomUUID().toString().substring(0, 8);
-  private static final String TOPIC =
-      String.format("projects/%s/topics/test_topic_foo_%s", PROJECT, ID).trim();
-  private static final Notification.PayloadFormat PAYLOAD_FORMAT =
-      Notification.PayloadFormat.JSON_API_V1.JSON_API_V1;
-  private static final Map<String, String> CUSTOM_ATTRIBUTES = ImmutableMap.of("label1", "value1");
 
   @BeforeClass
   public static void beforeClass() throws IOException {
     storage = storageFixture.getInstance();
 
-    storage.create(
-        BucketInfo.newBuilder(BUCKET)
-            .setLocation("us")
-            .setLifecycleRules(
-                ImmutableList.of(
-                    new LifecycleRule(
-                        LifecycleAction.newDeleteAction(),
-                        LifecycleCondition.newBuilder().setAge(1).build())))
-            .build());
+    storage.create(BucketInfo.newBuilder(BUCKET).setLocation("us").build());
 
     storage.create(BucketInfo.newBuilder(BUCKET_REQUESTER_PAYS).build());
-
-    // Configure topic admin client for notification.
-    topicAdminClient = configureTopicAdminClient();
   }
 
   private static void unsetRequesterPays() {
@@ -227,17 +165,6 @@ public class ITStorageTest {
   @AfterClass
   public static void afterClass() throws ExecutionException, InterruptedException {
 
-    /* Delete the Pub/Sub topic */
-    if (topicAdminClient != null) {
-      try {
-        topicAdminClient.deleteTopic(TOPIC);
-        topicAdminClient.close();
-      } catch (Exception e) {
-        log.log(Level.WARNING, "Error while trying to delete topic and shutdown topic client", e);
-      }
-      topicAdminClient = null;
-    }
-
     if (storage != null) {
       // In beforeClass, we make buckets auto-delete blobs older than a day old.
       // Here, delete all buckets older than 2 days. They should already be empty and easy.
@@ -264,130 +191,6 @@ public class ITStorageTest {
     }
   }
 
-  private static TopicAdminClient configureTopicAdminClient() throws IOException {
-    TopicAdminClient topicAdminClient = TopicAdminClient.create();
-    topicAdminClient.createTopic(TOPIC);
-    GetIamPolicyRequest getIamPolicyRequest =
-        GetIamPolicyRequest.newBuilder().setResource(TOPIC).build();
-    com.google.iam.v1.Policy policy = topicAdminClient.getIamPolicy(getIamPolicyRequest);
-    Binding binding =
-        Binding.newBuilder().setRole("roles/owner").addMembers("allAuthenticatedUsers").build();
-    SetIamPolicyRequest setIamPolicyRequest =
-        SetIamPolicyRequest.newBuilder()
-            .setResource(TOPIC)
-            .setPolicy(policy.toBuilder().addBindings(binding).build())
-            .build();
-    topicAdminClient.setIamPolicy(setIamPolicyRequest);
-    return topicAdminClient;
-  }
-
-  @Test(timeout = 5000)
-  public void testListBuckets() throws InterruptedException {
-    Iterator<Bucket> bucketIterator =
-        storage
-            .list(Storage.BucketListOption.prefix(BUCKET), Storage.BucketListOption.fields())
-            .iterateAll()
-            .iterator();
-    while (!bucketIterator.hasNext()) {
-      Thread.sleep(500);
-      bucketIterator =
-          storage
-              .list(Storage.BucketListOption.prefix(BUCKET), Storage.BucketListOption.fields())
-              .iterateAll()
-              .iterator();
-    }
-    while (bucketIterator.hasNext()) {
-      Bucket remoteBucket = bucketIterator.next();
-      assertTrue(remoteBucket.getName().startsWith(BUCKET));
-      assertNull(remoteBucket.getCreateTime());
-      assertNull(remoteBucket.getSelfLink());
-    }
-  }
-
-  @Test
-  public void testGetBucketSelectedFields() {
-    Bucket remoteBucket = storage.get(BUCKET, Storage.BucketGetOption.fields(BucketField.ID));
-    assertEquals(BUCKET, remoteBucket.getName());
-    assertNull(remoteBucket.getCreateTime());
-    assertNotNull(remoteBucket.getGeneratedId());
-  }
-
-  @Test
-  public void testGetBucketAllSelectedFields() {
-    Bucket remoteBucket = storage.get(BUCKET, Storage.BucketGetOption.fields(BucketField.values()));
-    assertEquals(BUCKET, remoteBucket.getName());
-    assertNotNull(remoteBucket.getCreateTime());
-    assertNotNull(remoteBucket.getSelfLink());
-  }
-
-  @Test
-  public void testGetBucketLifecycleRules() {
-    String lifecycleTestBucketName = RemoteStorageHelper.generateBucketName();
-    storage.create(
-        BucketInfo.newBuilder(lifecycleTestBucketName)
-            .setLocation("us")
-            .setLifecycleRules(
-                ImmutableList.of(
-                    new LifecycleRule(
-                        LifecycleAction.newSetStorageClassAction(StorageClass.COLDLINE),
-                        LifecycleCondition.newBuilder()
-                            .setAge(1)
-                            .setNumberOfNewerVersions(3)
-                            .setIsLive(false)
-                            .setCreatedBeforeOffsetDateTime(OffsetDateTime.now())
-                            .setMatchesStorageClass(ImmutableList.of(StorageClass.COLDLINE))
-                            .setDaysSinceNoncurrentTime(30)
-                            .setNoncurrentTimeBeforeOffsetDateTime(OffsetDateTime.now())
-                            .setCustomTimeBeforeOffsetDateTime(OffsetDateTime.now())
-                            .setDaysSinceCustomTime(30)
-                            .build())))
-            .build());
-    Bucket remoteBucket =
-        storage.get(lifecycleTestBucketName, Storage.BucketGetOption.fields(BucketField.LIFECYCLE));
-    LifecycleRule lifecycleRule = remoteBucket.getLifecycleRules().get(0);
-    try {
-      assertTrue(
-          lifecycleRule
-              .getAction()
-              .getActionType()
-              .equals(LifecycleRule.SetStorageClassLifecycleAction.TYPE));
-      assertEquals(3, lifecycleRule.getCondition().getNumberOfNewerVersions().intValue());
-      assertNotNull(lifecycleRule.getCondition().getCreatedBeforeOffsetDateTime());
-      assertFalse(lifecycleRule.getCondition().getIsLive());
-      assertEquals(1, lifecycleRule.getCondition().getAge().intValue());
-      assertEquals(1, lifecycleRule.getCondition().getMatchesStorageClass().size());
-      assertEquals(30, lifecycleRule.getCondition().getDaysSinceNoncurrentTime().intValue());
-      assertNotNull(lifecycleRule.getCondition().getNoncurrentTimeBeforeOffsetDateTime());
-      assertEquals(30, lifecycleRule.getCondition().getDaysSinceCustomTime().intValue());
-      assertNotNull(lifecycleRule.getCondition().getCustomTimeBeforeOffsetDateTime());
-    } finally {
-      storage.delete(lifecycleTestBucketName);
-    }
-  }
-
-  @Test
-  public void testGetBucketAbortMPULifecycle() {
-    String lifecycleTestBucketName = RemoteStorageHelper.generateBucketName();
-    storage.create(
-        BucketInfo.newBuilder(lifecycleTestBucketName)
-            .setLocation("us")
-            .setLifecycleRules(
-                ImmutableList.of(
-                    new LifecycleRule(
-                        LifecycleAction.newAbortIncompleteMPUploadAction(),
-                        LifecycleCondition.newBuilder().setAge(1).build())))
-            .build());
-    Bucket remoteBucket =
-        storage.get(lifecycleTestBucketName, Storage.BucketGetOption.fields(BucketField.LIFECYCLE));
-    LifecycleRule lifecycleRule = remoteBucket.getLifecycleRules().get(0);
-    try {
-      assertEquals(AbortIncompleteMPUAction.TYPE, lifecycleRule.getAction().getActionType());
-      assertEquals(1, lifecycleRule.getCondition().getAge().intValue());
-    } finally {
-      storage.delete(lifecycleTestBucketName);
-    }
-  }
-
   @Test
   public void testCreateBlob() {
     String blobName = "test-create-blob";
@@ -1266,163 +1069,6 @@ public class ITStorageTest {
     }
   }
 
-  @Test
-  public void testBatchRequest() {
-    String sourceBlobName1 = "test-batch-request-blob-1";
-    String sourceBlobName2 = "test-batch-request-blob-2";
-    BlobInfo sourceBlob1 = BlobInfo.newBuilder(BUCKET, sourceBlobName1).build();
-    BlobInfo sourceBlob2 = BlobInfo.newBuilder(BUCKET, sourceBlobName2).build();
-    assertNotNull(storage.create(sourceBlob1));
-    assertNotNull(storage.create(sourceBlob2));
-
-    // Batch update request
-    BlobInfo updatedBlob1 = sourceBlob1.toBuilder().setContentType(CONTENT_TYPE).build();
-    BlobInfo updatedBlob2 = sourceBlob2.toBuilder().setContentType(CONTENT_TYPE).build();
-    StorageBatch updateBatch = storage.batch();
-    StorageBatchResult<Blob> updateResult1 = updateBatch.update(updatedBlob1);
-    StorageBatchResult<Blob> updateResult2 = updateBatch.update(updatedBlob2);
-    updateBatch.submit();
-    Blob remoteUpdatedBlob1 = updateResult1.get();
-    Blob remoteUpdatedBlob2 = updateResult2.get();
-    assertEquals(sourceBlob1.getBucket(), remoteUpdatedBlob1.getBucket());
-    assertEquals(sourceBlob1.getName(), remoteUpdatedBlob1.getName());
-    assertEquals(sourceBlob2.getBucket(), remoteUpdatedBlob2.getBucket());
-    assertEquals(sourceBlob2.getName(), remoteUpdatedBlob2.getName());
-    assertEquals(updatedBlob1.getContentType(), remoteUpdatedBlob1.getContentType());
-    assertEquals(updatedBlob2.getContentType(), remoteUpdatedBlob2.getContentType());
-
-    // Batch get request
-    StorageBatch getBatch = storage.batch();
-    StorageBatchResult<Blob> getResult1 = getBatch.get(BUCKET, sourceBlobName1);
-    StorageBatchResult<Blob> getResult2 = getBatch.get(BUCKET, sourceBlobName2);
-    getBatch.submit();
-    Blob remoteBlob1 = getResult1.get();
-    Blob remoteBlob2 = getResult2.get();
-    assertEquals(remoteUpdatedBlob1, remoteBlob1);
-    assertEquals(remoteUpdatedBlob2, remoteBlob2);
-
-    // Batch delete request
-    StorageBatch deleteBatch = storage.batch();
-    StorageBatchResult<Boolean> deleteResult1 = deleteBatch.delete(BUCKET, sourceBlobName1);
-    StorageBatchResult<Boolean> deleteResult2 = deleteBatch.delete(BUCKET, sourceBlobName2);
-    deleteBatch.submit();
-    assertTrue(deleteResult1.get());
-    assertTrue(deleteResult2.get());
-  }
-
-  @Test
-  public void testBatchRequestManyOperations() {
-    List<StorageBatchResult<Boolean>> deleteResults =
-        Lists.newArrayListWithCapacity(MAX_BATCH_SIZE);
-    List<StorageBatchResult<Blob>> getResults = Lists.newArrayListWithCapacity(MAX_BATCH_SIZE / 2);
-    List<StorageBatchResult<Blob>> updateResults =
-        Lists.newArrayListWithCapacity(MAX_BATCH_SIZE / 2);
-    StorageBatch batch = storage.batch();
-    for (int i = 0; i < MAX_BATCH_SIZE; i++) {
-      BlobId blobId = BlobId.of(BUCKET, "test-batch-request-many-operations-blob-" + i);
-      deleteResults.add(batch.delete(blobId));
-    }
-    for (int i = 0; i < MAX_BATCH_SIZE / 2; i++) {
-      BlobId blobId = BlobId.of(BUCKET, "test-batch-request-many-operations-blob-" + i);
-      getResults.add(batch.get(blobId));
-    }
-    for (int i = 0; i < MAX_BATCH_SIZE / 2; i++) {
-      BlobInfo blob =
-          BlobInfo.newBuilder(BlobId.of(BUCKET, "test-batch-request-many-operations-blob-" + i))
-              .build();
-      updateResults.add(batch.update(blob));
-    }
-
-    String sourceBlobName1 = "test-batch-request-many-operations-source-blob-1";
-    String sourceBlobName2 = "test-batch-request-many-operations-source-blob-2";
-    BlobInfo sourceBlob1 = BlobInfo.newBuilder(BUCKET, sourceBlobName1).build();
-    BlobInfo sourceBlob2 = BlobInfo.newBuilder(BUCKET, sourceBlobName2).build();
-    assertNotNull(storage.create(sourceBlob1));
-    assertNotNull(storage.create(sourceBlob2));
-    BlobInfo updatedBlob2 = sourceBlob2.toBuilder().setContentType(CONTENT_TYPE).build();
-
-    StorageBatchResult<Blob> getResult = batch.get(BUCKET, sourceBlobName1);
-    StorageBatchResult<Blob> updateResult = batch.update(updatedBlob2);
-
-    batch.submit();
-
-    // Check deletes
-    for (StorageBatchResult<Boolean> failedDeleteResult : deleteResults) {
-      assertFalse(failedDeleteResult.get());
-    }
-
-    // Check gets
-    for (StorageBatchResult<Blob> failedGetResult : getResults) {
-      assertNull(failedGetResult.get());
-    }
-    Blob remoteBlob1 = getResult.get();
-    assertEquals(sourceBlob1.getBucket(), remoteBlob1.getBucket());
-    assertEquals(sourceBlob1.getName(), remoteBlob1.getName());
-
-    // Check updates
-    for (StorageBatchResult<Blob> failedUpdateResult : updateResults) {
-      try {
-        failedUpdateResult.get();
-        fail("Expected StorageException");
-      } catch (StorageException ex) {
-        // expected
-      }
-    }
-    Blob remoteUpdatedBlob2 = updateResult.get();
-    assertEquals(sourceBlob2.getBucket(), remoteUpdatedBlob2.getBucket());
-    assertEquals(sourceBlob2.getName(), remoteUpdatedBlob2.getName());
-    assertEquals(updatedBlob2.getContentType(), remoteUpdatedBlob2.getContentType());
-  }
-
-  @Test
-  public void testBatchRequestFail() {
-    String blobName = "test-batch-request-blob-fail";
-    BlobInfo blob = BlobInfo.newBuilder(BUCKET, blobName).build();
-    Blob remoteBlob = storage.create(blob);
-    assertNotNull(remoteBlob);
-    BlobInfo updatedBlob = BlobInfo.newBuilder(BUCKET, blobName, -1L).build();
-    StorageBatch batch = storage.batch();
-    StorageBatchResult<Blob> updateResult =
-        batch.update(updatedBlob, Storage.BlobTargetOption.generationMatch());
-    StorageBatchResult<Boolean> deleteResult1 =
-        batch.delete(BUCKET, blobName, Storage.BlobSourceOption.generationMatch(-1L));
-    StorageBatchResult<Boolean> deleteResult2 = batch.delete(BlobId.of(BUCKET, blobName, -1L));
-    StorageBatchResult<Blob> getResult1 =
-        batch.get(BUCKET, blobName, Storage.BlobGetOption.generationMatch(-1L));
-    StorageBatchResult<Blob> getResult2 = batch.get(BlobId.of(BUCKET, blobName, -1L));
-    batch.submit();
-    try {
-      updateResult.get();
-      fail("Expected StorageException");
-    } catch (StorageException ex) {
-      // expected
-    }
-    try {
-      deleteResult1.get();
-      fail("Expected StorageException");
-    } catch (StorageException ex) {
-      // expected
-    }
-    try {
-      deleteResult2.get();
-      fail("Expected an 'Invalid argument' exception");
-    } catch (StorageException e) {
-      assertThat(e.getMessage()).contains("Invalid argument");
-    }
-    try {
-      getResult1.get();
-      fail("Expected StorageException");
-    } catch (StorageException ex) {
-      // expected
-    }
-    try {
-      getResult2.get();
-      fail("Expected an 'Invalid argument' exception");
-    } catch (StorageException e) {
-      assertThat(e.getMessage()).contains("Invalid argument");
-    }
-  }
-
   @Test
   public void testReadAndWriteChannels() throws IOException {
     String blobName = "test-read-and-write-channels-blob";
@@ -1788,78 +1434,6 @@ public class ITStorageTest {
     assertNull(updatedBlobs.get(1));
   }
 
-  @Test
-  public void testBucketAcl() {
-    unsetRequesterPays();
-    testBucketAclRequesterPays(true);
-    testBucketAclRequesterPays(false);
-  }
-
-  private void testBucketAclRequesterPays(boolean requesterPays) {
-    if (requesterPays) {
-      Bucket remoteBucket =
-          storage.get(
-              BUCKET_REQUESTER_PAYS,
-              Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING));
-      assertTrue(remoteBucket.requesterPays() == null || !remoteBucket.requesterPays());
-      remoteBucket = remoteBucket.toBuilder().setRequesterPays(true).build();
-      Bucket updatedBucket = storage.update(remoteBucket);
-      assertTrue(updatedBucket.requesterPays());
-    }
-
-    String projectId = storage.getOptions().getProjectId();
-
-    Storage.BucketSourceOption[] bucketOptions =
-        requesterPays
-            ? new Storage.BucketSourceOption[] {Storage.BucketSourceOption.userProject(projectId)}
-            : new Storage.BucketSourceOption[] {};
-
-    assertNull(
-        storage.getAcl(BUCKET_REQUESTER_PAYS, User.ofAllAuthenticatedUsers(), bucketOptions));
-    assertFalse(
-        storage.deleteAcl(BUCKET_REQUESTER_PAYS, User.ofAllAuthenticatedUsers(), bucketOptions));
-    Acl acl = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER);
-    assertNotNull(storage.createAcl(BUCKET_REQUESTER_PAYS, acl, bucketOptions));
-    Acl updatedAcl =
-        storage.updateAcl(
-            BUCKET_REQUESTER_PAYS, acl.toBuilder().setRole(Role.WRITER).build(), bucketOptions);
-    assertEquals(Role.WRITER, updatedAcl.getRole());
-    Set<Acl> acls = new HashSet<>();
-    acls.addAll(storage.listAcls(BUCKET_REQUESTER_PAYS, bucketOptions));
-    assertTrue(acls.contains(updatedAcl));
-    assertTrue(
-        storage.deleteAcl(BUCKET_REQUESTER_PAYS, User.ofAllAuthenticatedUsers(), bucketOptions));
-    assertNull(
-        storage.getAcl(BUCKET_REQUESTER_PAYS, User.ofAllAuthenticatedUsers(), bucketOptions));
-    if (requesterPays) {
-      Bucket remoteBucket =
-          storage.get(
-              BUCKET_REQUESTER_PAYS,
-              Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING),
-              Storage.BucketGetOption.userProject(projectId));
-      assertTrue(remoteBucket.requesterPays());
-      remoteBucket = remoteBucket.toBuilder().setRequesterPays(false).build();
-      Bucket updatedBucket =
-          storage.update(remoteBucket, Storage.BucketTargetOption.userProject(projectId));
-      assertFalse(updatedBucket.requesterPays());
-    }
-  }
-
-  @Test
-  public void testBucketDefaultAcl() {
-    assertNull(storage.getDefaultAcl(BUCKET, User.ofAllAuthenticatedUsers()));
-    assertFalse(storage.deleteDefaultAcl(BUCKET, User.ofAllAuthenticatedUsers()));
-    Acl acl = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER);
-    assertNotNull(storage.createDefaultAcl(BUCKET, acl));
-    Acl updatedAcl = storage.updateDefaultAcl(BUCKET, acl.toBuilder().setRole(Role.OWNER).build());
-    assertEquals(Role.OWNER, updatedAcl.getRole());
-    Set<Acl> acls = new HashSet<>();
-    acls.addAll(storage.listDefaultAcls(BUCKET));
-    assertTrue(acls.contains(updatedAcl));
-    assertTrue(storage.deleteDefaultAcl(BUCKET, User.ofAllAuthenticatedUsers()));
-    assertNull(storage.getDefaultAcl(BUCKET, User.ofAllAuthenticatedUsers()));
-  }
-
   @Test
   public void testBlobAcl() {
     BlobId blobId = BlobId.of(BUCKET, "test-blob-acl");
@@ -2021,434 +1595,6 @@ public class ITStorageTest {
     }
   }
 
-  @Test
-  public void testBucketPolicyV1RequesterPays() throws ExecutionException, InterruptedException {
-    unsetRequesterPays();
-    Bucket bucketDefault =
-        storage.get(
-            BUCKET_REQUESTER_PAYS,
-            Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING));
-    assertTrue(bucketDefault.requesterPays() == null || !bucketDefault.requesterPays());
-
-    Bucket bucketTrue = storage.update(bucketDefault.toBuilder().setRequesterPays(true).build());
-    assertTrue(bucketTrue.requesterPays());
-
-    String projectId = storage.getOptions().getProjectId();
-
-    Storage.BucketSourceOption[] bucketOptions =
-        new Storage.BucketSourceOption[] {Storage.BucketSourceOption.userProject(projectId)};
-    Identity projectOwner = Identity.projectOwner(projectId);
-    Identity projectEditor = Identity.projectEditor(projectId);
-    Identity projectViewer = Identity.projectViewer(projectId);
-    Map<com.google.cloud.Role, Set<Identity>> bindingsWithoutPublicRead =
-        ImmutableMap.of(
-            StorageRoles.legacyBucketOwner(),
-            new HashSet<>(Arrays.asList(projectOwner, projectEditor)),
-            StorageRoles.legacyBucketReader(),
-            (Set<Identity>) new HashSet<>(Collections.singleton(projectViewer)));
-    Map<com.google.cloud.Role, Set<Identity>> bindingsWithPublicRead =
-        ImmutableMap.of(
-            StorageRoles.legacyBucketOwner(),
-            new HashSet<>(Arrays.asList(projectOwner, projectEditor)),
-            StorageRoles.legacyBucketReader(),
-            new HashSet<>(Collections.singleton(projectViewer)),
-            StorageRoles.legacyObjectReader(),
-            (Set<Identity>) new HashSet<>(Collections.singleton(Identity.allUsers())));
-
-    // Validate getting policy.
-    Policy currentPolicy = storage.getIamPolicy(BUCKET_REQUESTER_PAYS, bucketOptions);
-    assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindings());
-
-    // Validate updating policy.
-    Policy updatedPolicy =
-        storage.setIamPolicy(
-            BUCKET_REQUESTER_PAYS,
-            currentPolicy
-                .toBuilder()
-                .addIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers())
-                .build(),
-            bucketOptions);
-    assertEquals(bindingsWithPublicRead, updatedPolicy.getBindings());
-    Policy revertedPolicy =
-        storage.setIamPolicy(
-            BUCKET_REQUESTER_PAYS,
-            updatedPolicy
-                .toBuilder()
-                .removeIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers())
-                .build(),
-            bucketOptions);
-    assertEquals(bindingsWithoutPublicRead, revertedPolicy.getBindings());
-
-    // Validate testing permissions.
-    List<Boolean> expectedPermissions = ImmutableList.of(true, true);
-    assertEquals(
-        expectedPermissions,
-        storage.testIamPermissions(
-            BUCKET_REQUESTER_PAYS,
-            ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"),
-            bucketOptions));
-    Bucket bucketFalse =
-        storage.update(
-            bucketTrue.toBuilder().setRequesterPays(false).build(),
-            Storage.BucketTargetOption.userProject(projectId));
-    assertFalse(bucketFalse.requesterPays());
-  }
-
-  @Test
-  public void testBucketPolicyV1() {
-    String projectId = storage.getOptions().getProjectId();
-
-    Storage.BucketSourceOption[] bucketOptions = new Storage.BucketSourceOption[] {};
-    Identity projectOwner = Identity.projectOwner(projectId);
-    Identity projectEditor = Identity.projectEditor(projectId);
-    Identity projectViewer = Identity.projectViewer(projectId);
-    Map<com.google.cloud.Role, Set<Identity>> bindingsWithoutPublicRead =
-        ImmutableMap.of(
-            StorageRoles.legacyBucketOwner(),
-            new HashSet<>(Arrays.asList(projectOwner, projectEditor)),
-            StorageRoles.legacyBucketReader(),
-            (Set<Identity>) new HashSet<>(Collections.singleton(projectViewer)));
-    Map<com.google.cloud.Role, Set<Identity>> bindingsWithPublicRead =
-        ImmutableMap.of(
-            StorageRoles.legacyBucketOwner(),
-            new HashSet<>(Arrays.asList(projectOwner, projectEditor)),
-            StorageRoles.legacyBucketReader(),
-            new HashSet<>(Collections.singleton(projectViewer)),
-            StorageRoles.legacyObjectReader(),
-            (Set<Identity>) new HashSet<>(Collections.singleton(Identity.allUsers())));
-
-    // Validate getting policy.
-    Policy currentPolicy = storage.getIamPolicy(BUCKET, bucketOptions);
-    assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindings());
-
-    // Validate updating policy.
-    Policy updatedPolicy =
-        storage.setIamPolicy(
-            BUCKET,
-            currentPolicy
-                .toBuilder()
-                .addIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers())
-                .build(),
-            bucketOptions);
-    assertEquals(bindingsWithPublicRead, updatedPolicy.getBindings());
-    Policy revertedPolicy =
-        storage.setIamPolicy(
-            BUCKET,
-            updatedPolicy
-                .toBuilder()
-                .removeIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers())
-                .build(),
-            bucketOptions);
-    assertEquals(bindingsWithoutPublicRead, revertedPolicy.getBindings());
-
-    // Validate testing permissions.
-    List<Boolean> expectedPermissions = ImmutableList.of(true, true);
-    assertEquals(
-        expectedPermissions,
-        storage.testIamPermissions(
-            BUCKET,
-            ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"),
-            bucketOptions));
-  }
-
-  @Test
-  public void testBucketPolicyV3() {
-    // Enable Uniform Bucket-Level Access
-    storage.update(
-        BucketInfo.newBuilder(BUCKET)
-            .setIamConfiguration(
-                BucketInfo.IamConfiguration.newBuilder()
-                    .setIsUniformBucketLevelAccessEnabled(true)
-                    .build())
-            .build());
-    String projectId = storage.getOptions().getProjectId();
-
-    Storage.BucketSourceOption[] bucketOptions =
-        new Storage.BucketSourceOption[] {Storage.BucketSourceOption.requestedPolicyVersion(3)};
-    Identity projectOwner = Identity.projectOwner(projectId);
-    Identity projectEditor = Identity.projectEditor(projectId);
-    Identity projectViewer = Identity.projectViewer(projectId);
-    List<com.google.cloud.Binding> bindingsWithoutPublicRead =
-        ImmutableList.of(
-            com.google.cloud.Binding.newBuilder()
-                .setRole(StorageRoles.legacyBucketOwner().toString())
-                .setMembers(ImmutableList.of(projectEditor.strValue(), projectOwner.strValue()))
-                .build(),
-            com.google.cloud.Binding.newBuilder()
-                .setRole(StorageRoles.legacyBucketReader().toString())
-                .setMembers(ImmutableList.of(projectViewer.strValue()))
-                .build());
-    List<com.google.cloud.Binding> bindingsWithPublicRead =
-        ImmutableList.of(
-            com.google.cloud.Binding.newBuilder()
-                .setRole(StorageRoles.legacyBucketReader().toString())
-                .setMembers(ImmutableList.of(projectViewer.strValue()))
-                .build(),
-            com.google.cloud.Binding.newBuilder()
-                .setRole(StorageRoles.legacyBucketOwner().toString())
-                .setMembers(ImmutableList.of(projectEditor.strValue(), projectOwner.strValue()))
-                .build(),
-            com.google.cloud.Binding.newBuilder()
-                .setRole(StorageRoles.legacyObjectReader().toString())
-                .setMembers(ImmutableList.of("allUsers"))
-                .build());
-
-    List<com.google.cloud.Binding> bindingsWithConditionalPolicy =
-        ImmutableList.of(
-            com.google.cloud.Binding.newBuilder()
-                .setRole(StorageRoles.legacyBucketReader().toString())
-                .setMembers(ImmutableList.of(projectViewer.strValue()))
-                .build(),
-            com.google.cloud.Binding.newBuilder()
-                .setRole(StorageRoles.legacyBucketOwner().toString())
-                .setMembers(ImmutableList.of(projectEditor.strValue(), projectOwner.strValue()))
-                .build(),
-            com.google.cloud.Binding.newBuilder()
-                .setRole(StorageRoles.legacyObjectReader().toString())
-                .setMembers(
-                    ImmutableList.of(
-                        "serviceAccount:storage-python@spec-test-ruby-samples.iam.gserviceaccount.com"))
-                .setCondition(
-                    Condition.newBuilder()
-                        .setTitle("Title")
-                        .setDescription("Description")
-                        .setExpression(
-                            "resource.name.startsWith(\"projects/_/buckets/bucket-name/objects/prefix-a-\")")
-                        .build())
-                .build());
-
-    // Validate getting policy.
-    Policy currentPolicy = storage.getIamPolicy(BUCKET, bucketOptions);
-    assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindingsList());
-
-    // Validate updating policy.
-    List<com.google.cloud.Binding> currentBindings = new ArrayList(currentPolicy.getBindingsList());
-    currentBindings.add(
-        com.google.cloud.Binding.newBuilder()
-            .setRole(StorageRoles.legacyObjectReader().getValue())
-            .addMembers(Identity.allUsers().strValue())
-            .build());
-    Policy updatedPolicy =
-        storage.setIamPolicy(
-            BUCKET, currentPolicy.toBuilder().setBindings(currentBindings).build(), bucketOptions);
-    assertTrue(
-        bindingsWithPublicRead.size() == updatedPolicy.getBindingsList().size()
-            && bindingsWithPublicRead.containsAll(updatedPolicy.getBindingsList()));
-
-    // Remove a member
-    List<com.google.cloud.Binding> updatedBindings = new ArrayList(updatedPolicy.getBindingsList());
-    for (int i = 0; i < updatedBindings.size(); i++) {
-      com.google.cloud.Binding binding = updatedBindings.get(i);
-      if (binding.getRole().equals(StorageRoles.legacyObjectReader().toString())) {
-        List<String> members = new ArrayList(binding.getMembers());
-        members.remove(Identity.allUsers().strValue());
-        updatedBindings.set(i, binding.toBuilder().setMembers(members).build());
-        break;
-      }
-    }
-
-    Policy revertedPolicy =
-        storage.setIamPolicy(
-            BUCKET, updatedPolicy.toBuilder().setBindings(updatedBindings).build(), bucketOptions);
-
-    assertEquals(bindingsWithoutPublicRead, revertedPolicy.getBindingsList());
-    assertTrue(
-        bindingsWithoutPublicRead.size() == revertedPolicy.getBindingsList().size()
-            && bindingsWithoutPublicRead.containsAll(revertedPolicy.getBindingsList()));
-
-    // Add Conditional Policy
-    List<com.google.cloud.Binding> conditionalBindings =
-        new ArrayList(revertedPolicy.getBindingsList());
-    conditionalBindings.add(
-        com.google.cloud.Binding.newBuilder()
-            .setRole(StorageRoles.legacyObjectReader().toString())
-            .addMembers(
-                "serviceAccount:storage-python@spec-test-ruby-samples.iam.gserviceaccount.com")
-            .setCondition(
-                Condition.newBuilder()
-                    .setTitle("Title")
-                    .setDescription("Description")
-                    .setExpression(
-                        "resource.name.startsWith(\"projects/_/buckets/bucket-name/objects/prefix-a-\")")
-                    .build())
-            .build());
-    Policy conditionalPolicy =
-        storage.setIamPolicy(
-            BUCKET,
-            revertedPolicy.toBuilder().setBindings(conditionalBindings).setVersion(3).build(),
-            bucketOptions);
-    assertTrue(
-        bindingsWithConditionalPolicy.size() == conditionalPolicy.getBindingsList().size()
-            && bindingsWithConditionalPolicy.containsAll(conditionalPolicy.getBindingsList()));
-
-    // Remove Conditional Policy
-    conditionalPolicy =
-        storage.setIamPolicy(
-            BUCKET,
-            conditionalPolicy.toBuilder().setBindings(updatedBindings).setVersion(3).build(),
-            bucketOptions);
-
-    // Validate testing permissions.
-    List<Boolean> expectedPermissions = ImmutableList.of(true, true);
-    assertEquals(
-        expectedPermissions,
-        storage.testIamPermissions(
-            BUCKET,
-            ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"),
-            bucketOptions));
-
-    // Disable Uniform Bucket-Level Access
-    storage.update(
-        BucketInfo.newBuilder(BUCKET)
-            .setIamConfiguration(
-                BucketInfo.IamConfiguration.newBuilder()
-                    .setIsUniformBucketLevelAccessEnabled(false)
-                    .build())
-            .build());
-  }
-
-  @Test
-  public void testUpdateBucketLabel() {
-    Bucket remoteBucket =
-        storage.get(BUCKET, Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING));
-    assertNull(remoteBucket.getLabels());
-    remoteBucket = remoteBucket.toBuilder().setLabels(BUCKET_LABELS).build();
-    Bucket updatedBucket = storage.update(remoteBucket);
-    assertEquals(BUCKET_LABELS, updatedBucket.getLabels());
-    remoteBucket.toBuilder().setLabels(REMOVE_BUCKET_LABELS).build().update();
-    assertNull(storage.get(BUCKET).getLabels());
-  }
-
-  @Test
-  public void testUpdateBucketRequesterPays() {
-    unsetRequesterPays();
-    Bucket remoteBucket =
-        storage.get(
-            BUCKET_REQUESTER_PAYS,
-            Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING));
-    assertTrue(remoteBucket.requesterPays() == null || !remoteBucket.requesterPays());
-    remoteBucket = remoteBucket.toBuilder().setRequesterPays(true).build();
-    Bucket updatedBucket = storage.update(remoteBucket);
-    assertTrue(updatedBucket.requesterPays());
-
-    String projectId = storage.getOptions().getProjectId();
-    Bucket.BlobTargetOption option = Bucket.BlobTargetOption.userProject(projectId);
-    String blobName = "test-create-empty-blob-requester-pays";
-    Blob remoteBlob = updatedBucket.create(blobName, BLOB_BYTE_CONTENT, option);
-    assertNotNull(remoteBlob);
-    byte[] readBytes =
-        storage.readAllBytes(
-            BUCKET_REQUESTER_PAYS, blobName, Storage.BlobSourceOption.userProject(projectId));
-    assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
-    remoteBucket = remoteBucket.toBuilder().setRequesterPays(false).build();
-    updatedBucket = storage.update(remoteBucket, Storage.BucketTargetOption.userProject(projectId));
-    assertFalse(updatedBucket.requesterPays());
-  }
-
-  @Test
-  public void testListBucketRequesterPaysFails() throws InterruptedException {
-    String projectId = storage.getOptions().getProjectId();
-    Iterator<Bucket> bucketIterator =
-        storage
-            .list(
-                Storage.BucketListOption.prefix(BUCKET),
-                Storage.BucketListOption.fields(),
-                Storage.BucketListOption.userProject(projectId))
-            .iterateAll()
-            .iterator();
-    while (!bucketIterator.hasNext()) {
-      Thread.sleep(500);
-      bucketIterator =
-          storage
-              .list(Storage.BucketListOption.prefix(BUCKET), Storage.BucketListOption.fields())
-              .iterateAll()
-              .iterator();
-    }
-    while (bucketIterator.hasNext()) {
-      Bucket remoteBucket = bucketIterator.next();
-      assertTrue(remoteBucket.getName().startsWith(BUCKET));
-      assertNull(remoteBucket.getCreateTime());
-      assertNull(remoteBucket.getSelfLink());
-    }
-  }
-
-  @Test
-  public void testRetentionPolicyNoLock() throws ExecutionException, InterruptedException {
-    String bucketName = RemoteStorageHelper.generateBucketName();
-    Bucket remoteBucket =
-        storage.create(
-            BucketInfo.newBuilder(bucketName).setRetentionPeriod(RETENTION_PERIOD).build());
-    try {
-      assertEquals(RETENTION_PERIOD, remoteBucket.getRetentionPeriod());
-      assertNotNull(remoteBucket.getRetentionEffectiveTime());
-      assertNull(remoteBucket.retentionPolicyIsLocked());
-      remoteBucket =
-          storage.get(bucketName, Storage.BucketGetOption.fields(BucketField.RETENTION_POLICY));
-      assertEquals(RETENTION_PERIOD, remoteBucket.getRetentionPeriod());
-      assertNotNull(remoteBucket.getRetentionEffectiveTime());
-      assertNull(remoteBucket.retentionPolicyIsLocked());
-      String blobName = "test-create-with-retention-policy-hold";
-      BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, blobName).build();
-      Blob remoteBlob = storage.create(blobInfo);
-      assertNotNull(remoteBlob.getRetentionExpirationTime());
-      remoteBucket = remoteBucket.toBuilder().setRetentionPeriod(null).build().update();
-      assertNull(remoteBucket.getRetentionPeriod());
-      remoteBucket = remoteBucket.toBuilder().setRetentionPeriod(null).build().update();
-      assertNull(remoteBucket.getRetentionPeriod());
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
-    }
-  }
-
-  @Test
-  public void testRetentionPolicyLock() throws ExecutionException, InterruptedException {
-    retentionPolicyLockRequesterPays(true);
-    retentionPolicyLockRequesterPays(false);
-  }
-
-  private void retentionPolicyLockRequesterPays(boolean requesterPays)
-      throws ExecutionException, InterruptedException {
-    String projectId = storage.getOptions().getProjectId();
-    String bucketName = RemoteStorageHelper.generateBucketName();
-    BucketInfo bucketInfo;
-    if (requesterPays) {
-      bucketInfo =
-          BucketInfo.newBuilder(bucketName)
-              .setRetentionPeriod(RETENTION_PERIOD)
-              .setRequesterPays(true)
-              .build();
-    } else {
-      bucketInfo = BucketInfo.newBuilder(bucketName).setRetentionPeriod(RETENTION_PERIOD).build();
-    }
-    Bucket remoteBucket = storage.create(bucketInfo);
-    try {
-      assertNull(remoteBucket.retentionPolicyIsLocked());
-      assertNotNull(remoteBucket.getRetentionEffectiveTime());
-      assertNotNull(remoteBucket.getMetageneration());
-      if (requesterPays) {
-        remoteBucket =
-            storage.lockRetentionPolicy(
-                remoteBucket,
-                Storage.BucketTargetOption.metagenerationMatch(),
-                Storage.BucketTargetOption.userProject(projectId));
-      } else {
-        remoteBucket =
-            storage.lockRetentionPolicy(
-                remoteBucket, Storage.BucketTargetOption.metagenerationMatch());
-      }
-      assertTrue(remoteBucket.retentionPolicyIsLocked());
-      assertNotNull(remoteBucket.getRetentionEffectiveTime());
-    } finally {
-      if (requesterPays) {
-        bucketInfo = bucketInfo.toBuilder().setRequesterPays(false).build();
-        Bucket updateBucket =
-            storage.update(bucketInfo, Storage.BucketTargetOption.userProject(projectId));
-        assertFalse(updateBucket.requesterPays());
-      }
-      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS, projectId);
-    }
-  }
-
   @Test
   public void testAttemptObjectDeleteWithRetentionPolicy()
       throws ExecutionException, InterruptedException {
@@ -2472,35 +1618,6 @@ public class ITStorageTest {
     }
   }
 
-  @Test
-  public void testEnableDisableBucketDefaultEventBasedHold()
-      throws ExecutionException, InterruptedException {
-    String bucketName = RemoteStorageHelper.generateBucketName();
-    Bucket remoteBucket =
-        storage.create(BucketInfo.newBuilder(bucketName).setDefaultEventBasedHold(true).build());
-    try {
-      assertTrue(remoteBucket.getDefaultEventBasedHold());
-      remoteBucket =
-          storage.get(
-              bucketName, Storage.BucketGetOption.fields(BucketField.DEFAULT_EVENT_BASED_HOLD));
-      assertTrue(remoteBucket.getDefaultEventBasedHold());
-      String blobName = "test-create-with-event-based-hold";
-      BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, blobName).build();
-      Blob remoteBlob = storage.create(blobInfo);
-      assertTrue(remoteBlob.getEventBasedHold());
-      remoteBlob =
-          storage.get(
-              blobInfo.getBlobId(), Storage.BlobGetOption.fields(BlobField.EVENT_BASED_HOLD));
-      assertTrue(remoteBlob.getEventBasedHold());
-      remoteBlob = remoteBlob.toBuilder().setEventBasedHold(false).build().update();
-      assertFalse(remoteBlob.getEventBasedHold());
-      remoteBucket = remoteBucket.toBuilder().setDefaultEventBasedHold(false).build().update();
-      assertFalse(remoteBucket.getDefaultEventBasedHold());
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
-    }
-  }
-
   @Test
   public void testEnableDisableTemporaryHold() {
     String blobName = "test-create-with-temporary-hold";
@@ -2554,76 +1671,6 @@ public class ITStorageTest {
     assertTrue(serviceAccount.getEmail().endsWith(SERVICE_ACCOUNT_EMAIL_SUFFIX));
   }
 
-  @Test
-  @SuppressWarnings({"unchecked", "deprecation"})
-  public void testBucketWithBucketPolicyOnlyEnabled() throws Exception {
-    String bucket = RemoteStorageHelper.generateBucketName();
-    try {
-      storage.create(
-          Bucket.newBuilder(bucket)
-              .setIamConfiguration(
-                  BucketInfo.IamConfiguration.newBuilder()
-                      .setIsBucketPolicyOnlyEnabled(true)
-                      .build())
-              .build());
-
-      Bucket remoteBucket =
-          storage.get(bucket, Storage.BucketGetOption.fields(BucketField.IAMCONFIGURATION));
-
-      assertTrue(remoteBucket.getIamConfiguration().isBucketPolicyOnlyEnabled());
-      assertNotNull(remoteBucket.getIamConfiguration().getBucketPolicyOnlyLockedTime());
-
-      try {
-        remoteBucket.listAcls();
-        fail("StorageException was expected.");
-      } catch (StorageException e) {
-        // Expected: Listing legacy ACLs should fail on a BPO enabled bucket
-      }
-      try {
-        remoteBucket.listDefaultAcls();
-        fail("StorageException was expected");
-      } catch (StorageException e) {
-        // Expected: Listing legacy ACLs should fail on a BPO enabled bucket
-      }
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, bucket, 1, TimeUnit.MINUTES);
-    }
-  }
-
-  @Test
-  public void testBucketWithUniformBucketLevelAccessEnabled() throws Exception {
-    String bucket = RemoteStorageHelper.generateBucketName();
-    try {
-      storage.create(
-          Bucket.newBuilder(bucket)
-              .setIamConfiguration(
-                  BucketInfo.IamConfiguration.newBuilder()
-                      .setIsUniformBucketLevelAccessEnabled(true)
-                      .build())
-              .build());
-
-      Bucket remoteBucket =
-          storage.get(bucket, Storage.BucketGetOption.fields(BucketField.IAMCONFIGURATION));
-
-      assertTrue(remoteBucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
-      assertNotNull(remoteBucket.getIamConfiguration().getUniformBucketLevelAccessLockedTime());
-      try {
-        remoteBucket.listAcls();
-        fail("StorageException was expected.");
-      } catch (StorageException e) {
-        // Expected: Listing legacy ACLs should fail on a BPO enabled bucket
-      }
-      try {
-        remoteBucket.listDefaultAcls();
-        fail("StorageException was expected");
-      } catch (StorageException e) {
-        // Expected: Listing legacy ACLs should fail on a BPO enabled bucket
-      }
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, bucket, 1, TimeUnit.MINUTES);
-    }
-  }
-
   @Test
   @SuppressWarnings({"unchecked", "deprecation"})
   public void testEnableAndDisableBucketPolicyOnlyOnExistingBucket() throws Exception {
@@ -2677,287 +1724,6 @@ public class ITStorageTest {
     }
   }
 
-  @Test
-  public void testEnableAndDisableUniformBucketLevelAccessOnExistingBucket() throws Exception {
-    String bpoBucket = RemoteStorageHelper.generateBucketName();
-    try {
-      BucketInfo.IamConfiguration ublaDisabledIamConfiguration =
-          BucketInfo.IamConfiguration.newBuilder()
-              .setIsUniformBucketLevelAccessEnabled(false)
-              .build();
-      Bucket bucket =
-          storage.create(
-              Bucket.newBuilder(bpoBucket)
-                  .setIamConfiguration(ublaDisabledIamConfiguration)
-                  .setAcl(ImmutableList.of(Acl.of(User.ofAllAuthenticatedUsers(), Role.READER)))
-                  .setDefaultAcl(
-                      ImmutableList.of(Acl.of(User.ofAllAuthenticatedUsers(), Role.READER)))
-                  .build());
-
-      bucket
-          .toBuilder()
-          .setAcl(null)
-          .setDefaultAcl(null)
-          .setIamConfiguration(
-              ublaDisabledIamConfiguration
-                  .toBuilder()
-                  .setIsUniformBucketLevelAccessEnabled(true)
-                  .build())
-          .build()
-          .update();
-
-      Bucket remoteBucket =
-          storage.get(bpoBucket, Storage.BucketGetOption.fields(BucketField.IAMCONFIGURATION));
-
-      assertTrue(remoteBucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
-      assertNotNull(remoteBucket.getIamConfiguration().getUniformBucketLevelAccessLockedTime());
-
-      remoteBucket.toBuilder().setIamConfiguration(ublaDisabledIamConfiguration).build().update();
-
-      remoteBucket =
-          storage.get(
-              bpoBucket,
-              Storage.BucketGetOption.fields(
-                  BucketField.IAMCONFIGURATION, BucketField.ACL, BucketField.DEFAULT_OBJECT_ACL));
-
-      assertFalse(remoteBucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
-      assertEquals(User.ofAllAuthenticatedUsers(), remoteBucket.getDefaultAcl().get(0).getEntity());
-      assertEquals(Role.READER, remoteBucket.getDefaultAcl().get(0).getRole());
-      assertEquals(User.ofAllAuthenticatedUsers(), remoteBucket.getAcl().get(0).getEntity());
-      assertEquals(Role.READER, remoteBucket.getAcl().get(0).getRole());
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, bpoBucket, 1, TimeUnit.MINUTES);
-    }
-  }
-
-  private Bucket generatePublicAccessPreventionBucket(String bucketName, boolean enforced) {
-    return storage.create(
-        Bucket.newBuilder(bucketName)
-            .setIamConfiguration(
-                BucketInfo.IamConfiguration.newBuilder()
-                    .setPublicAccessPrevention(
-                        enforced
-                            ? BucketInfo.PublicAccessPrevention.ENFORCED
-                            : BucketInfo.PublicAccessPrevention.INHERITED)
-                    .build())
-            .build());
-  }
-
-  @Test
-  public void testEnforcedPublicAccessPreventionOnBucket() throws Exception {
-    String papBucket = RemoteStorageHelper.generateBucketName();
-    try {
-      Bucket bucket = generatePublicAccessPreventionBucket(papBucket, true);
-      // Making bucket public should fail.
-      try {
-        storage.setIamPolicy(
-            papBucket,
-            Policy.newBuilder()
-                .setVersion(3)
-                .setBindings(
-                    ImmutableList.<com.google.cloud.Binding>of(
-                        com.google.cloud.Binding.newBuilder()
-                            .setRole("roles/storage.objectViewer")
-                            .addMembers("allUsers")
-                            .build()))
-                .build());
-        fail("pap: expected adding allUsers policy to bucket should fail");
-      } catch (StorageException storageException) {
-        // Creating a bucket with roles/storage.objectViewer is not
-        // allowed when publicAccessPrevention is enabled.
-        assertEquals(storageException.getCode(), 412);
-      }
-
-      // Making object public via ACL should fail.
-      try {
-        // Create a public object
-        bucket.create(
-            "pap-test-object",
-            "".getBytes(),
-            Bucket.BlobTargetOption.predefinedAcl(Storage.PredefinedAcl.PUBLIC_READ));
-        fail("pap: expected adding allUsers ACL to object should fail");
-      } catch (StorageException storageException) {
-        // Creating an object with allUsers roles/storage.viewer permission
-        // is not allowed. When Public Access Prevention is enabled.
-        assertEquals(storageException.getCode(), 412);
-      }
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, papBucket, 1, TimeUnit.MINUTES);
-    }
-  }
-
-  @Test
-  public void testUnspecifiedPublicAccessPreventionOnBucket() throws Exception {
-    String papBucket = RemoteStorageHelper.generateBucketName();
-    try {
-      Bucket bucket = generatePublicAccessPreventionBucket(papBucket, false);
-
-      // Now, making object public or making bucket public should succeed.
-      try {
-        // Create a public object
-        bucket.create(
-            "pap-test-object",
-            "".getBytes(),
-            Bucket.BlobTargetOption.predefinedAcl(Storage.PredefinedAcl.PUBLIC_READ));
-      } catch (StorageException storageException) {
-        fail("pap: expected adding allUsers ACL to object to succeed");
-      }
-
-      // Now, making bucket public should succeed.
-      try {
-        storage.setIamPolicy(
-            papBucket,
-            Policy.newBuilder()
-                .setVersion(3)
-                .setBindings(
-                    ImmutableList.<com.google.cloud.Binding>of(
-                        com.google.cloud.Binding.newBuilder()
-                            .setRole("roles/storage.objectViewer")
-                            .addMembers("allUsers")
-                            .build()))
-                .build());
-      } catch (StorageException storageException) {
-        fail("pap: expected adding allUsers policy to bucket to succeed");
-      }
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, papBucket, 1, TimeUnit.MINUTES);
-    }
-  }
-
-  @Test
-  public void testUBLAWithPublicAccessPreventionOnBucket() throws Exception {
-    String papBucket = RemoteStorageHelper.generateBucketName();
-    try {
-      Bucket bucket = generatePublicAccessPreventionBucket(papBucket, false);
-      assertEquals(
-          bucket.getIamConfiguration().getPublicAccessPrevention(),
-          BucketInfo.PublicAccessPrevention.INHERITED);
-      assertFalse(bucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
-      assertFalse(bucket.getIamConfiguration().isBucketPolicyOnlyEnabled());
-
-      // Update PAP setting to ENFORCED and should not affect UBLA setting.
-      bucket
-          .toBuilder()
-          .setIamConfiguration(
-              bucket
-                  .getIamConfiguration()
-                  .toBuilder()
-                  .setPublicAccessPrevention(BucketInfo.PublicAccessPrevention.ENFORCED)
-                  .build())
-          .build()
-          .update();
-      bucket = storage.get(papBucket, Storage.BucketGetOption.fields(BucketField.IAMCONFIGURATION));
-      assertEquals(
-          bucket.getIamConfiguration().getPublicAccessPrevention(),
-          BucketInfo.PublicAccessPrevention.ENFORCED);
-      assertFalse(bucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
-      assertFalse(bucket.getIamConfiguration().isBucketPolicyOnlyEnabled());
-
-      // Updating UBLA should not affect PAP setting.
-      bucket =
-          bucket
-              .toBuilder()
-              .setIamConfiguration(
-                  bucket
-                      .getIamConfiguration()
-                      .toBuilder()
-                      .setIsUniformBucketLevelAccessEnabled(true)
-                      .build())
-              .build()
-              .update();
-      assertTrue(bucket.getIamConfiguration().isUniformBucketLevelAccessEnabled());
-      assertTrue(bucket.getIamConfiguration().isBucketPolicyOnlyEnabled());
-      assertEquals(
-          bucket.getIamConfiguration().getPublicAccessPrevention(),
-          BucketInfo.PublicAccessPrevention.ENFORCED);
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, papBucket, 1, TimeUnit.MINUTES);
-    }
-  }
-
-  @Test
-  public void testBucketLocationType() throws ExecutionException, InterruptedException {
-    String bucketName = RemoteStorageHelper.generateBucketName();
-    long bucketMetageneration = 42;
-    storage.create(
-        BucketInfo.newBuilder(bucketName)
-            .setLocation("us")
-            .setRetentionPeriod(RETENTION_PERIOD)
-            .build());
-    Bucket bucket =
-        storage.get(
-            bucketName, Storage.BucketGetOption.metagenerationNotMatch(bucketMetageneration));
-    assertTrue(LOCATION_TYPES.contains(bucket.getLocationType()));
-
-    Bucket bucket1 =
-        storage.lockRetentionPolicy(bucket, Storage.BucketTargetOption.metagenerationMatch());
-    assertTrue(LOCATION_TYPES.contains(bucket1.getLocationType()));
-
-    Bucket updatedBucket =
-        storage.update(
-            BucketInfo.newBuilder(bucketName)
-                .setLocation("asia")
-                .setRetentionPeriod(RETENTION_PERIOD)
-                .build());
-    assertTrue(LOCATION_TYPES.contains(updatedBucket.getLocationType()));
-
-    Iterator<Bucket> bucketIterator =
-        storage.list(Storage.BucketListOption.prefix(bucketName)).iterateAll().iterator();
-    while (bucketIterator.hasNext()) {
-      Bucket remoteBucket = bucketIterator.next();
-      assertTrue(LOCATION_TYPES.contains(remoteBucket.getLocationType()));
-    }
-    RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
-  }
-
-  @Test
-  public void testBucketCustomPlacmentConfigDualRegion() {
-    String bucketName = RemoteStorageHelper.generateBucketName();
-    List<String> locations = new ArrayList<>();
-    locations.add("US-EAST1");
-    locations.add("US-WEST1");
-    CustomPlacementConfig customPlacementConfig =
-        CustomPlacementConfig.newBuilder().setDataLocations(locations).build();
-    Bucket bucket =
-        storage.create(
-            BucketInfo.newBuilder(bucketName)
-                .setCustomPlacementConfig(customPlacementConfig)
-                .setLocation("us")
-                .build());
-    assertTrue(bucket.getCustomPlacementConfig().getDataLocations().contains("US-EAST1"));
-    assertTrue(bucket.getCustomPlacementConfig().getDataLocations().contains("US-WEST1"));
-    assertTrue(bucket.getLocation().equalsIgnoreCase("us"));
-  }
-
-  @Test
-  public void testBucketLogging() throws ExecutionException, InterruptedException {
-    String logsBucket = RemoteStorageHelper.generateBucketName();
-    String loggingBucket = RemoteStorageHelper.generateBucketName();
-    try {
-      assertNotNull(storage.create(BucketInfo.newBuilder(logsBucket).setLocation("us").build()));
-      Policy policy = storage.getIamPolicy(logsBucket);
-      assertNotNull(policy);
-      BucketInfo.Logging logging =
-          BucketInfo.Logging.newBuilder()
-              .setLogBucket(logsBucket)
-              .setLogObjectPrefix("test-logs")
-              .build();
-      Bucket bucket =
-          storage.create(
-              BucketInfo.newBuilder(loggingBucket).setLocation("us").setLogging(logging).build());
-      assertEquals(logsBucket, bucket.getLogging().getLogBucket());
-      assertEquals("test-logs", bucket.getLogging().getLogObjectPrefix());
-
-      // Disable bucket logging.
-      Bucket updatedBucket = bucket.toBuilder().setLogging(null).build().update();
-      assertNull(updatedBucket.getLogging());
-
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, logsBucket, 5, TimeUnit.SECONDS);
-      RemoteStorageHelper.forceDelete(storage, loggingBucket, 5, TimeUnit.SECONDS);
-    }
-  }
-
   @Test
   public void testBlobReload() throws Exception {
     String blobName = "test-blob-reload";
@@ -2987,25 +1753,6 @@ public class ITStorageTest {
     assertNull(updated.reload());
   }
 
-  @Test
-  public void testDeleteLifecycleRules() throws ExecutionException, InterruptedException {
-    String bucketName = RemoteStorageHelper.generateBucketName();
-    Bucket bucket =
-        storage.create(
-            BucketInfo.newBuilder(bucketName)
-                .setLocation("us")
-                .setLifecycleRules(LIFECYCLE_RULES)
-                .build());
-    assertThat(bucket.getLifecycleRules()).isNotNull();
-    assertThat(bucket.getLifecycleRules()).hasSize(2);
-    try {
-      Bucket updatedBucket = bucket.toBuilder().deleteLifecycleRules().build().update();
-      assertThat(updatedBucket.getLifecycleRules()).hasSize(0);
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
-    }
-  }
-
   @Test
   public void testUploadWithEncryption() throws Exception {
     String blobName = "test-upload-withEncryption";
@@ -3085,121 +1832,6 @@ public class ITStorageTest {
     testAutoContentType("writer");
   }
 
-  @Test
-  public void testRemoveBucketCORS() throws ExecutionException, InterruptedException {
-    String bucketName = RemoteStorageHelper.generateBucketName();
-    List<Cors.Origin> origins = ImmutableList.of(Cors.Origin.of("http://cloud.google.com"));
-    List<HttpMethod> httpMethods = ImmutableList.of(HttpMethod.GET);
-    List<String> responseHeaders = ImmutableList.of("Content-Type");
-    try {
-      Cors cors =
-          Cors.newBuilder()
-              .setOrigins(origins)
-              .setMethods(httpMethods)
-              .setResponseHeaders(responseHeaders)
-              .setMaxAgeSeconds(100)
-              .build();
-      storage.create(BucketInfo.newBuilder(bucketName).setCors(ImmutableList.of(cors)).build());
-
-      // case-1 : Cors are set and field selector is selected then returns not-null.
-      Bucket remoteBucket =
-          storage.get(bucketName, Storage.BucketGetOption.fields(BucketField.CORS));
-      assertThat(remoteBucket.getCors()).isNotNull();
-      assertThat(remoteBucket.getCors().get(0).getMaxAgeSeconds()).isEqualTo(100);
-      assertThat(remoteBucket.getCors().get(0).getMethods()).isEqualTo(httpMethods);
-      assertThat(remoteBucket.getCors().get(0).getOrigins()).isEqualTo(origins);
-      assertThat(remoteBucket.getCors().get(0).getResponseHeaders()).isEqualTo(responseHeaders);
-
-      // case-2 : Cors are set but field selector isn't selected then returns not-null.
-      remoteBucket = storage.get(bucketName);
-      assertThat(remoteBucket.getCors()).isNotNull();
-
-      // Remove CORS configuration from the bucket.
-      Bucket updatedBucket = remoteBucket.toBuilder().setCors(null).build().update();
-      assertThat(updatedBucket.getCors()).isNull();
-
-      // case-3 : Cors are not set and field selector is selected then returns null.
-      updatedBucket = storage.get(bucketName, Storage.BucketGetOption.fields(BucketField.CORS));
-      assertThat(updatedBucket.getCors()).isNull();
-
-      // case-4 : Cors are not set and field selector isn't selected then returns null.
-      updatedBucket = storage.get(bucketName);
-      assertThat(updatedBucket.getCors()).isNull();
-
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
-    }
-  }
-
-  @Test
-  public void testBucketUpdateTime() throws ExecutionException, InterruptedException {
-    String bucketName = RemoteStorageHelper.generateBucketName();
-    BucketInfo bucketInfo =
-        BucketInfo.newBuilder(bucketName).setLocation("us").setVersioningEnabled(true).build();
-    try {
-      Bucket bucket = storage.create(bucketInfo);
-      assertThat(bucket).isNotNull();
-      assertThat(bucket.versioningEnabled()).isTrue();
-      assertThat(bucket.getCreateTime()).isNotNull();
-      assertThat(bucket.getUpdateTime()).isEqualTo(bucket.getCreateTime());
-
-      Bucket updatedBucket = bucket.toBuilder().setVersioningEnabled(false).build().update();
-      assertThat(updatedBucket.versioningEnabled()).isFalse();
-      assertThat(updatedBucket.getUpdateTime()).isNotNull();
-      assertThat(updatedBucket.getCreateTime()).isEqualTo(bucket.getCreateTime());
-      assertThat(updatedBucket.getUpdateTime()).isGreaterThan(bucket.getCreateTime());
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
-    }
-  }
-
-  @Test
-  public void testNotification() throws InterruptedException, ExecutionException {
-    String bucketName = RemoteStorageHelper.generateBucketName();
-    storage.create(BucketInfo.newBuilder(bucketName).setLocation("us").build());
-    NotificationInfo notificationInfo =
-        NotificationInfo.newBuilder(TOPIC)
-            .setCustomAttributes(CUSTOM_ATTRIBUTES)
-            .setPayloadFormat(PAYLOAD_FORMAT)
-            .build();
-    try {
-      assertThat(storage.listNotifications(bucketName)).isEmpty();
-      Notification notification = storage.createNotification(bucketName, notificationInfo);
-      assertThat(notification.getNotificationId()).isNotNull();
-      assertThat(CUSTOM_ATTRIBUTES).isEqualTo(notification.getCustomAttributes());
-      assertThat(PAYLOAD_FORMAT.name()).isEqualTo(notification.getPayloadFormat().name());
-      assertThat(notification.getTopic().contains(TOPIC)).isTrue();
-
-      // Gets the notification with the specified id.
-      Notification actualNotification =
-          storage.getNotification(bucketName, notification.getNotificationId());
-      assertThat(actualNotification.getNotificationId())
-          .isEqualTo(notification.getNotificationId());
-      assertThat(actualNotification.getTopic().trim()).isEqualTo(notification.getTopic().trim());
-      assertThat(actualNotification.getEtag()).isEqualTo(notification.getEtag());
-      assertThat(actualNotification.getEventTypes()).isEqualTo(notification.getEventTypes());
-      assertThat(actualNotification.getPayloadFormat()).isEqualTo(notification.getPayloadFormat());
-      assertThat(actualNotification.getSelfLink()).isEqualTo(notification.getSelfLink());
-      assertThat(actualNotification.getCustomAttributes())
-          .isEqualTo(notification.getCustomAttributes());
-
-      // Retrieves the list of notifications associated with the bucket.
-      List<Notification> notifications = storage.listNotifications(bucketName);
-      assertThat(notifications.size()).isEqualTo(1);
-      assertThat(notifications.get(0).getNotificationId())
-          .isEqualTo(actualNotification.getNotificationId());
-
-      // Deletes the notification with the specified id.
-      assertThat(storage.deleteNotification(bucketName, notification.getNotificationId())).isTrue();
-      assertThat(storage.deleteNotification(bucketName, notification.getNotificationId()))
-          .isFalse();
-      assertThat(storage.getNotification(bucketName, notification.getNotificationId())).isNull();
-      assertThat(storage.listNotifications(bucketName)).isEmpty();
-    } finally {
-      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
-    }
-  }
-
   @Test
   public void testBlobTimeStorageClassUpdated() {
     String blobName = "test-blob-with-storage-class";
@@ -3237,21 +1869,4 @@ public class ITStorageTest {
         .isEqualTo(updatedBlob1.getTimeStorageClassUpdated());
     assertThat(updatedBlob2.delete()).isTrue();
   }
-
-  @Test
-  public void testRpoConfig() {
-    String rpoBucket = RemoteStorageHelper.generateBucketName();
-    try {
-      Bucket bucket =
-          storage.create(
-              BucketInfo.newBuilder(rpoBucket).setLocation("NAM4").setRpo(Rpo.ASYNC_TURBO).build());
-      assertEquals("ASYNC_TURBO", bucket.getRpo().toString());
-
-      bucket.toBuilder().setRpo(Rpo.DEFAULT).build().update();
-
-      assertEquals("DEFAULT", storage.get(rpoBucket).getRpo().toString());
-    } finally {
-      storage.delete(rpoBucket);
-    }
-  }
 }
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 fd305e6d..bf114ae0 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
@@ -54,7 +54,7 @@ public class HmacKeyMetadataArbitraryProvider implements ArbitraryProvider {
                 (accessId, projectID, state, createTime, updateTime, email) ->
                     HmacKeyMetadata.newBuilder()
                         .setAccessId(accessId)
-                        .setProject(projectID.get())
+                        .setProject("projects/" + projectID.get())
                         .setId(projectID.get() + "/" + accessId)
                         .setState(state.toString())
                         .setCreateTime(createTime)