Repository: googleapis/java-storage
Pull request #2482 (2024-04-01 19:29:26)

fix: ensure all BlobWriteSession types conform to the semantics specified in BlobWriteSession
* Improve error handling to cover more cases where errors should be converted to StorageException
* Enforce BlobWriteSession#open only being able to be called once, subsequent calls will error
* Make JsonResumableSessionPutTask more graceful when attempting to determine object size (testbench can omit `.size` from its response when the value is 0, possibly due to protobuf to json conversion where protobuf won't explicitly include a 0 value.)
* Update com.google.cloud.storage.StorageException#coalesce to look for ApiException in causes the same way it does for BaseServiceException
* Add com.google.cloud.storage.StorageOptions.Builder#setBlobWriteSessionConfig now that both Http and Grpc support these, having it on the base class is convenient
* Add new integration test ITBlobWriteSessionCommonSemanticsTest which forces certain failure modes and ensures expected handling in accordance with the semantics outlined in BlobWriteSession




Patch:
diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml
index d2272068..cc9d6933 100644
--- a/google-cloud-storage/clirr-ignored-differences.xml
+++ b/google-cloud-storage/clirr-ignored-differences.xml
@@ -71,4 +71,11 @@
     <className>com/google/cloud/storage/transfermanager/TransferManagerConfig$Builder</className>
     <method>* setAllowDivideAndConquer(boolean)</method>
   </difference>
+
+  <difference>
+    <differenceType>7013</differenceType>
+    <className>com/google/cloud/storage/StorageOptions$Builder</className>
+    <method>com.google.cloud.storage.StorageOptions$Builder setBlobWriteSessionConfig(com.google.cloud.storage.BlobWriteSessionConfig)</method>
+  </difference>
+
 </differences>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSession.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSession.java
index 02ea23a6..81cecdc8 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSession.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSession.java
@@ -20,6 +20,7 @@ import com.google.api.core.ApiFuture;
 import com.google.api.core.BetaApi;
 import java.io.IOException;
 import java.nio.channels.WritableByteChannel;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A session to write an object to Google Cloud Storage.
@@ -50,6 +51,10 @@ public interface BlobWriteSession {
    * <p>Upon calling {@link WritableByteChannel#close()} the object creation will be finalized, and
    * {@link #getResult()}s future should resolve.
    *
+   * <p>The returned {@code WritableByteChannel} can throw IOExceptions from any of its usual
+   * methods. Any {@link IOException} thrown can have a cause of a {@link StorageException}.
+   * However, not all {@code IOExceptions} will have {@code StorageException}s.
+   *
    * @throws IOException When creating the {@link WritableByteChannel} if an unrecoverable
    *     underlying IOException occurs it can be rethrown
    * @throws IllegalStateException if open is called more than once
@@ -66,6 +71,10 @@ public interface BlobWriteSession {
    * Google Cloud Storage 2. A terminal failure occurs, the terminal failure will become the
    * exception result
    *
+   * <p>If a terminal failure is encountered, calling either {@link ApiFuture#get()} or {@link
+   * ApiFuture#get(long, TimeUnit)} will result in an {@link
+   * java.util.concurrent.ExecutionException} with a cause that is the {@link StorageException}.
+   *
    * @since 2.26.0 This new api is in preview and is subject to breaking changes.
    */
   @BetaApi
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessions.java
index 878552a1..c9da9ee0 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessions.java
@@ -17,6 +17,7 @@
 package com.google.cloud.storage;
 
 import com.google.api.core.ApiFuture;
+import com.google.common.base.Preconditions;
 import java.io.IOException;
 import java.nio.channels.WritableByteChannel;
 
@@ -30,14 +31,20 @@ final class BlobWriteSessions {
 
   static final class WritableByteChannelSessionAdapter implements BlobWriteSession {
     private final WritableByteChannelSession<?, BlobInfo> delegate;
+    private boolean open;
 
     private WritableByteChannelSessionAdapter(WritableByteChannelSession<?, BlobInfo> delegate) {
       this.delegate = delegate;
+      open = false;
     }
 
     @Override
     public WritableByteChannel open() throws IOException {
-      return delegate.open();
+      synchronized (this) {
+        Preconditions.checkState(!open, "already open");
+        open = true;
+        return delegate.open();
+      }
     }
 
     @Override
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java
index 1d45c9e3..ffcbcfa8 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java
@@ -32,6 +32,8 @@ import com.google.common.base.Preconditions;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.storage.v2.WriteObjectRequest;
 import com.google.storage.v2.WriteObjectResponse;
+import java.io.IOException;
+import java.nio.ByteBuffer;
 import java.nio.channels.WritableByteChannel;
 import java.time.Clock;
 import java.util.Map;
@@ -202,24 +204,25 @@ public final class DefaultBlobWriteSessionConfig extends BlobWriteSessionConfig
       this.decoder = decoder;
     }
 
-    @Override
-    public WBC open() {
-      try {
-        return WritableByteChannelSession.super.open();
-      } catch (Exception e) {
-        throw StorageException.coalesce(e);
-      }
-    }
-
     @Override
     public ApiFuture<WBC> openAsync() {
-      return delegate.openAsync();
+      return ApiFutures.catchingAsync(
+          delegate.openAsync(),
+          Throwable.class,
+          throwable -> ApiFutures.immediateFailedFuture(StorageException.coalesce(throwable)),
+          MoreExecutors.directExecutor());
     }
 
     @Override
     public ApiFuture<BlobInfo> getResult() {
-      return ApiFutures.transform(
-          delegate.getResult(), decoder::decode, MoreExecutors.directExecutor());
+      ApiFuture<BlobInfo> decodeResult =
+          ApiFutures.transform(
+              delegate.getResult(), decoder::decode, MoreExecutors.directExecutor());
+      return ApiFutures.catchingAsync(
+          decodeResult,
+          Throwable.class,
+          throwable -> ApiFutures.immediateFailedFuture(StorageException.coalesce(throwable)),
+          MoreExecutors.directExecutor());
     }
   }
 
@@ -233,7 +236,55 @@ public final class DefaultBlobWriteSessionConfig extends BlobWriteSessionConfig
 
     @Override
     public ApiFuture<BufferedWritableByteChannel> openAsync() {
-      return lazy.getSession().openAsync();
+      // make sure the errors coming out of the BufferedWritableByteChannel are either IOException
+      // or StorageException
+      return ApiFutures.transform(
+          lazy.getSession().openAsync(),
+          delegate ->
+              new BufferedWritableByteChannel() {
+                @Override
+                public int write(ByteBuffer src) throws IOException {
+                  try {
+                    return delegate.write(src);
+                  } catch (IOException e) {
+                    throw e;
+                  } catch (Exception e) {
+                    throw StorageException.coalesce(e);
+                  }
+                }
+
+                @Override
+                public void flush() throws IOException {
+                  try {
+                    delegate.flush();
+                  } catch (IOException e) {
+                    throw e;
+                  } catch (Exception e) {
+                    throw StorageException.coalesce(e);
+                  }
+                }
+
+                @Override
+                public boolean isOpen() {
+                  try {
+                    return delegate.isOpen();
+                  } catch (Exception e) {
+                    throw StorageException.coalesce(e);
+                  }
+                }
+
+                @Override
+                public void close() throws IOException {
+                  try {
+                    delegate.close();
+                  } catch (IOException e) {
+                    throw e;
+                  } catch (Exception e) {
+                    throw StorageException.coalesce(e);
+                  }
+                }
+              },
+          MoreExecutors.directExecutor());
     }
 
     @Override
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 b4a26ed4..32d5eb9d 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
@@ -92,7 +92,12 @@ final class GapicUnbufferedWritableByteChannel<
         throw e;
       }
     } else {
-      flusher.close(null);
+      try {
+        flusher.close(null);
+      } catch (RuntimeException e) {
+        resultFuture.setException(e);
+        throw e;
+      }
     }
     open = false;
   }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java
index ac520213..2d1daa54 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java
@@ -49,7 +49,7 @@ final class GapicUploadSessionBuilder {
   }
 
   ApiFuture<ResumableWrite> resumableWrite(
-      UnaryCallable<StartResumableWriteRequest, StartResumableWriteResponse> x,
+      UnaryCallable<StartResumableWriteRequest, StartResumableWriteResponse> callable,
       WriteObjectRequest writeObjectRequest) {
     StartResumableWriteRequest.Builder b = StartResumableWriteRequest.newBuilder();
     if (writeObjectRequest.hasWriteObjectSpec()) {
@@ -65,9 +65,16 @@ final class GapicUploadSessionBuilder {
     Function<String, WriteObjectRequest> f =
         uploadId ->
             writeObjectRequest.toBuilder().clearWriteObjectSpec().setUploadId(uploadId).build();
-    return ApiFutures.transform(
-        x.futureCall(req),
-        (resp) -> new ResumableWrite(req, resp, f),
+    ApiFuture<ResumableWrite> futureResumableWrite =
+        ApiFutures.transform(
+            callable.futureCall(req),
+            (resp) -> new ResumableWrite(req, resp, f),
+            MoreExecutors.directExecutor());
+    // make sure we wrap any failure as a storage exception
+    return ApiFutures.catchingAsync(
+        futureResumableWrite,
+        Throwable.class,
+        throwable -> ApiFutures.immediateFailedFuture(StorageException.coalesce(throwable)),
         MoreExecutors.directExecutor());
   }
 
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java
index 192cfec6..622988d4 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java
@@ -120,7 +120,7 @@ final class JsonResumableSessionPutTask
         }
       } else if (finalizing && JsonResumableSessionFailureScenario.isOk(code)) {
         @Nullable StorageObject storageObject;
-        @Nullable BigInteger actualSize;
+        BigInteger actualSize = BigInteger.ZERO;
 
         Long contentLength = response.getHeaders().getContentLength();
         String contentType = response.getHeaders().getContentType();
@@ -130,7 +130,12 @@ final class JsonResumableSessionPutTask
         boolean isJson = contentType != null && contentType.startsWith("application/json");
         if (isJson) {
           storageObject = response.parseAs(StorageObject.class);
-          actualSize = storageObject != null ? storageObject.getSize() : null;
+          if (storageObject != null) {
+            BigInteger size = storageObject.getSize();
+            if (size != null) {
+              actualSize = size;
+            }
+          }
         } else if ((contentLength == null || contentLength == 0) && storedContentLength != null) {
           // when a signed url is used, the finalize response is empty
           response.ignore();
@@ -150,7 +155,6 @@ final class JsonResumableSessionPutTask
         int compare = expectedSize.compareTo(actualSize);
         if (compare == 0) {
           success = true;
-          //noinspection DataFlowIssue  compareTo result will filter out actualSize == null
           return ResumableOperationResult.complete(storageObject, actualSize.longValue());
         } else if (compare > 0) {
           StorageException se =
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 f8b4f94a..eeb24a44 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
@@ -118,6 +118,9 @@ public final class StorageException extends BaseHttpServiceException {
     if (t instanceof ApiException) {
       return asStorageException((ApiException) t);
     }
+    if (t.getCause() instanceof ApiException) {
+      return asStorageException((ApiException) t.getCause());
+    }
     return getStorageException(t);
   }
 
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
index 53ad6142..ab32532a 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
@@ -22,14 +22,17 @@ import com.google.cloud.NoCredentials;
 import com.google.cloud.ServiceDefaults;
 import com.google.cloud.ServiceOptions;
 import com.google.cloud.http.HttpTransportOptions;
+import com.google.cloud.storage.GrpcStorageOptions.GrpcStorageDefaults;
 import com.google.cloud.storage.HttpStorageOptions.HttpStorageDefaults;
 import com.google.cloud.storage.HttpStorageOptions.HttpStorageFactory;
 import com.google.cloud.storage.HttpStorageOptions.HttpStorageRpcFactory;
+import com.google.cloud.storage.Storage.BlobWriteOption;
 import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.spi.StorageRpcFactory;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Properties;
+import org.checkerframework.checker.nullness.qual.NonNull;
 
 public abstract class StorageOptions extends ServiceOptions<Storage, StorageOptions> {
 
@@ -95,6 +98,18 @@ public abstract class StorageOptions extends ServiceOptions<Storage, StorageOpti
 
     public abstract Builder setStorageRetryStrategy(StorageRetryStrategy storageRetryStrategy);
 
+    /**
+     * @see BlobWriteSessionConfig
+     * @see BlobWriteSessionConfigs
+     * @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...)
+     * @see HttpStorageDefaults#getDefaultStorageWriterConfig()
+     * @see GrpcStorageDefaults#getDefaultStorageWriterConfig()
+     * @since 2.37.0 This new api is in preview and is subject to breaking changes.
+     */
+    @BetaApi
+    public abstract StorageOptions.Builder setBlobWriteSessionConfig(
+        @NonNull BlobWriteSessionConfig blobWriteSessionConfig);
+
     @Override
     public abstract StorageOptions build();
   }


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionCommonSemanticsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionCommonSemanticsTest.java
new file mode 100644
index 00000000..dc02c3fb
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionCommonSemanticsTest.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2024 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.cloud.storage.TestUtils.assertAll;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.BlobWriteSession;
+import com.google.cloud.storage.BlobWriteSessionConfig;
+import com.google.cloud.storage.BlobWriteSessionConfigs;
+import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.BufferAllocationStrategy;
+import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.ExecutorSupplier;
+import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartCleanupStrategy;
+import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartMetadataFieldDecorator;
+import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartNamingStrategy;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.Storage.BlobTargetOption;
+import com.google.cloud.storage.Storage.BlobWriteOption;
+import com.google.cloud.storage.StorageException;
+import com.google.cloud.storage.StorageOptions;
+import com.google.cloud.storage.TransportCompatibility.Transport;
+import com.google.cloud.storage.it.ITBlobWriteSessionCommonSemanticsTest.ParamsProvider;
+import com.google.cloud.storage.it.runner.StorageITRunner;
+import com.google.cloud.storage.it.runner.annotations.Backend;
+import com.google.cloud.storage.it.runner.annotations.CrossRun;
+import com.google.cloud.storage.it.runner.annotations.Inject;
+import com.google.cloud.storage.it.runner.annotations.Parameterized;
+import com.google.cloud.storage.it.runner.annotations.Parameterized.Parameter;
+import com.google.cloud.storage.it.runner.annotations.Parameterized.ParametersProvider;
+import com.google.cloud.storage.it.runner.registry.Generator;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+
+@RunWith(StorageITRunner.class)
+@CrossRun(
+    backends = {Backend.PROD},
+    transports = {Transport.HTTP, Transport.GRPC})
+@Parameterized(ParamsProvider.class)
+public final class ITBlobWriteSessionCommonSemanticsTest {
+
+  @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  @Inject public Storage injectedStorage;
+  @Inject public BucketInfo bucket;
+  @Inject public Generator generator;
+
+  @Parameter public Params params;
+
+  private Storage storage;
+
+  @Before
+  public void setUp() throws Exception {
+    Path tmpDir = temporaryFolder.newFolder().toPath();
+    BlobWriteSessionConfig config = params.ctor.apply(tmpDir);
+
+    StorageOptions originalOptions = injectedStorage.getOptions();
+    StorageOptions newOptions = null;
+    try {
+      newOptions = originalOptions.toBuilder().setBlobWriteSessionConfig(config).build();
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessageThat().contains("not compatible with this");
+      assumeTrue(false);
+    }
+    storage = newOptions.getService();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    if (storage != null) {
+      storage.close();
+    }
+  }
+
+  @Test
+  public void closingAnOpenedSessionWithoutCallingWriteShouldMakeAnEmptyObject()
+      throws IOException, ExecutionException, InterruptedException, TimeoutException {
+    BlobInfo info = BlobInfo.newBuilder(bucket, generator.randomObjectName()).build();
+    BlobWriteSession session = storage.blobWriteSession(info, BlobWriteOption.doesNotExist());
+
+    WritableByteChannel open = session.open();
+    open.close();
+    BlobInfo gen1 = session.getResult().get(1, TimeUnit.SECONDS);
+
+    // sometimes testbench will not define `.size = 0`, default it here if we get null
+    Long size = gen1.getSize();
+    if (size == null) {
+      size = 0L;
+    }
+    assertThat(size).isEqualTo(0);
+  }
+
+  @Test
+  public void attemptingToUseASessionWhichResultsInFailureShouldThrowAStorageException() {
+    // attempt to write to a bucket which we have not created
+    String badBucketName = bucket.getName() + "x";
+    BlobInfo info = BlobInfo.newBuilder(badBucketName, generator.randomObjectName()).build();
+
+    BlobWriteSession session = storage.blobWriteSession(info, BlobWriteOption.doesNotExist());
+    StorageException se =
+        assertThrows(
+            StorageException.class,
+            () -> {
+              WritableByteChannel open = session.open();
+              open.close();
+            });
+
+    assertThat(se.getCode()).isEqualTo(404);
+  }
+
+  @Test
+  public void callingOpenIsOnlyAllowedOnce() throws Exception {
+    BlobInfo info = BlobInfo.newBuilder(bucket, generator.randomObjectName()).build();
+    BlobWriteSession session = storage.blobWriteSession(info, BlobWriteOption.doesNotExist());
+
+    WritableByteChannel open = session.open();
+    IllegalStateException se = assertThrows(IllegalStateException.class, session::open);
+
+    assertAll(() -> assertThat(se.getMessage()).contains("already open"));
+  }
+
+  @Test
+  public void getResultErrorsWhenTheSessionErrors() throws Exception {
+    BlobInfo info = BlobInfo.newBuilder(bucket, generator.randomObjectName()).build();
+    byte[] helloWorld = "Hello World".getBytes(StandardCharsets.UTF_8);
+    storage.create(info, helloWorld, BlobTargetOption.doesNotExist());
+
+    BlobWriteSession session =
+        storage.blobWriteSession(
+            info,
+            // this precondition will result in failure
+            BlobWriteOption.doesNotExist());
+
+    try (WritableByteChannel open = session.open()) {
+      open.write(ByteBuffer.wrap(helloWorld));
+    } catch (StorageException se) {
+      assertThat(se.getCode()).isEqualTo(412);
+    } catch (IOException ioe) {
+      assertThat(ioe).hasCauseThat().isInstanceOf(StorageException.class);
+      StorageException se = (StorageException) ioe.getCause();
+      assertThat(se.getCode()).isEqualTo(412);
+    }
+
+    ExecutionException resultSe =
+        assertThrows(ExecutionException.class, () -> session.getResult().get(10, TimeUnit.SECONDS));
+
+    assertAll(
+        () -> assertThat(resultSe).hasCauseThat().isInstanceOf(StorageException.class),
+        () -> assertThat(((StorageException) resultSe.getCause()).getCode()).isEqualTo(412));
+  }
+
+  public static final class ParamsProvider implements ParametersProvider {
+    @Override
+    public ImmutableList<Params> parameters() {
+      final int _2MiB = 2 * 1024 * 1024;
+      final int _4MiB = 4 * 1024 * 1024;
+      return ImmutableList.of(
+          new Params("default", p -> BlobWriteSessionConfigs.getDefault()),
+          new Params("c!c.2MiB", p -> BlobWriteSessionConfigs.getDefault().withChunkSize(_2MiB)),
+          new Params("b!p.1", BlobWriteSessionConfigs::bufferToDiskThenUpload),
+          new Params("j!p.1", p -> BlobWriteSessionConfigs.journaling(ImmutableList.of(p))),
+          new Params(
+              "p!t.c&b.s*&p.4MiB&c.n&m.n",
+              p ->
+                  BlobWriteSessionConfigs.parallelCompositeUpload()
+                      .withExecutorSupplier(ExecutorSupplier.cachedPool())
+                      .withPartNamingStrategy(PartNamingStrategy.noPrefix())
+                      .withBufferAllocationStrategy(BufferAllocationStrategy.simple(_4MiB))
+                      .withPartCleanupStrategy(PartCleanupStrategy.never())
+                      .withPartMetadataFieldDecorator(PartMetadataFieldDecorator.noOp())),
+          new Params("d!c.2MiB", p -> BlobWriteSessionConfigs.bidiWrite().withBufferSize(_2MiB)));
+    }
+  }
+
+  public interface ParamsCtor {
+    BlobWriteSessionConfig apply(Path p) throws IOException;
+  }
+
+  public static final class Params {
+    private final String desc;
+    private final ParamsCtor ctor;
+
+    public Params(String desc, ParamsCtor ctor) {
+      this.desc = desc;
+      this.ctor = ctor;
+    }
+
+    @Override
+    public String toString() {
+      return desc;
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java
index 4abe9494..10ae174a 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java
@@ -17,7 +17,6 @@
 package com.google.cloud.storage.it;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
 import static org.junit.Assert.assertThrows;
 
 import com.google.cloud.storage.BlobInfo;
@@ -25,12 +24,10 @@ import com.google.cloud.storage.BlobWriteSession;
 import com.google.cloud.storage.BlobWriteSessionConfigs;
 import com.google.cloud.storage.BucketInfo;
 import com.google.cloud.storage.DataGenerator;
-import com.google.cloud.storage.GrpcStorageOptions;
 import com.google.cloud.storage.HttpStorageOptions;
 import com.google.cloud.storage.JournalingBlobWriteSessionConfig;
 import com.google.cloud.storage.Storage;
 import com.google.cloud.storage.Storage.BlobWriteOption;
-import com.google.cloud.storage.StorageException;
 import com.google.cloud.storage.StorageOptions;
 import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.it.runner.StorageITRunner;
@@ -39,13 +36,10 @@ import com.google.cloud.storage.it.runner.annotations.CrossRun;
 import com.google.cloud.storage.it.runner.annotations.Inject;
 import com.google.cloud.storage.it.runner.registry.Generator;
 import com.google.common.collect.ImmutableList;
-import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.WritableByteChannel;
 import java.nio.file.Path;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -60,7 +54,6 @@ public final class ITBlobWriteSessionTest {
   @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
 
   @Inject public Storage storage;
-  @Inject public Transport transport;
 
   @Inject public BucketInfo bucket;
 
@@ -73,22 +66,13 @@ public final class ITBlobWriteSessionTest {
 
   @Test
   public void bufferToTempDirThenUpload() throws Exception {
-    StorageOptions options = null;
-    if (transport == Transport.GRPC) {
-      options =
-          ((GrpcStorageOptions) storage.getOptions())
-              .toBuilder()
-              .setBlobWriteSessionConfig(BlobWriteSessionConfigs.bufferToTempDirThenUpload())
-              .build();
-    } else if (transport == Transport.HTTP) {
-      options =
-          ((HttpStorageOptions) storage.getOptions())
-              .toBuilder()
-              .setBlobWriteSessionConfig(BlobWriteSessionConfigs.bufferToTempDirThenUpload())
-              .build();
-    }
-    assertWithMessage("unable to resolve options").that(options).isNotNull();
-    //noinspection DataFlowIssue
+    Path path = temporaryFolder.newFolder().toPath();
+    StorageOptions options =
+        storage
+            .getOptions()
+            .toBuilder()
+            .setBlobWriteSessionConfig(BlobWriteSessionConfigs.bufferToDiskThenUpload(path))
+            .build();
     try (Storage s = options.getService()) {
       doTest(s);
     }
@@ -112,24 +96,13 @@ public final class ITBlobWriteSessionTest {
 
   @Test
   public void overrideDefaultBufferSize() throws Exception {
-    StorageOptions options = null;
-    if (transport == Transport.GRPC) {
-      options =
-          ((GrpcStorageOptions) storage.getOptions())
-              .toBuilder()
-              .setBlobWriteSessionConfig(
-                  BlobWriteSessionConfigs.getDefault().withChunkSize(256 * 1024))
-              .build();
-    } else if (transport == Transport.HTTP) {
-      options =
-          ((HttpStorageOptions) storage.getOptions())
-              .toBuilder()
-              .setBlobWriteSessionConfig(
-                  BlobWriteSessionConfigs.getDefault().withChunkSize(256 * 1024))
-              .build();
-    }
-    assertWithMessage("unable to resolve options").that(options).isNotNull();
-    //noinspection DataFlowIssue
+    StorageOptions options =
+        (storage.getOptions())
+            .toBuilder()
+            .setBlobWriteSessionConfig(
+                BlobWriteSessionConfigs.getDefault().withChunkSize(256 * 1024))
+            .build();
+
     try (Storage s = options.getService()) {
       doTest(s);
     }
@@ -138,47 +111,16 @@ public final class ITBlobWriteSessionTest {
   @Test
   @CrossRun.Exclude(transports = Transport.HTTP)
   public void bidiTest() throws Exception {
-    StorageOptions options = null;
-    if (transport == Transport.GRPC) {
-      options =
-          ((GrpcStorageOptions) storage.getOptions())
-              .toBuilder()
-              .setBlobWriteSessionConfig(BlobWriteSessionConfigs.bidiWrite())
-              .build();
-    }
-    assertWithMessage("unable to resolve options").that(options).isNotNull();
-
+    StorageOptions options =
+        (storage.getOptions())
+            .toBuilder()
+            .setBlobWriteSessionConfig(BlobWriteSessionConfigs.bidiWrite())
+            .build();
     try (Storage s = options.getService()) {
       doTest(s);
     }
   }
 
-  @Test
-  public void closingAnOpenedSessionWithoutCallingWriteShouldMakeAnEmptyObject()
-      throws IOException, ExecutionException, InterruptedException, TimeoutException {
-    BlobInfo info = BlobInfo.newBuilder(bucket, generator.randomObjectName()).build();
-    BlobWriteSession session = storage.blobWriteSession(info, BlobWriteOption.doesNotExist());
-
-    WritableByteChannel open = session.open();
-    open.close();
-    BlobInfo gen1 = session.getResult().get(1, TimeUnit.SECONDS);
-
-    assertThat(gen1.getSize()).isEqualTo(0);
-  }
-
-  @Test
-  public void attemptingToOpenASessionWhichResultsInFailureShouldThrowAStorageException() {
-    // attempt to write to a bucket which we have not created
-    String badBucketName = bucket.getName() + "x";
-    BlobInfo info = BlobInfo.newBuilder(badBucketName, generator.randomObjectName()).build();
-
-    BlobWriteSession session = storage.blobWriteSession(info, BlobWriteOption.doesNotExist());
-    StorageException se = assertThrows(StorageException.class, () -> session.open().close());
-
-    assertThat(se.getCode()).isEqualTo(404);
-    assertThat(se).hasMessageThat().contains(badBucketName);
-  }
-
   private void doTest(Storage underTest) throws Exception {
     BlobWriteSession sess =
         underTest.blobWriteSession(
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITJournalingBlobWriteSessionConfigTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITJournalingBlobWriteSessionConfigTest.java
index 4526a09f..70e369f3 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITJournalingBlobWriteSessionConfigTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITJournalingBlobWriteSessionConfigTest.java
@@ -24,7 +24,6 @@ import com.google.cloud.storage.BlobWriteSession;
 import com.google.cloud.storage.BlobWriteSessionConfigs;
 import com.google.cloud.storage.BucketInfo;
 import com.google.cloud.storage.DataGenerator;
-import com.google.cloud.storage.GrpcStorageOptions;
 import com.google.cloud.storage.JournalingBlobWriteSessionConfig;
 import com.google.cloud.storage.Storage;
 import com.google.cloud.storage.Storage.BlobWriteOption;
@@ -75,7 +74,9 @@ public final class ITJournalingBlobWriteSessionConfigTest {
     JournalingBlobWriteSessionConfig journaling =
         BlobWriteSessionConfigs.journaling(ImmutableList.of(tempDir));
     journalingStorage =
-        ((GrpcStorageOptions.Builder) this.storage.getOptions().toBuilder())
+        this.storage
+            .getOptions()
+            .toBuilder()
             .setBlobWriteSessionConfig(journaling)
             .build()
             .getService();
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITParallelCompositeUploadBlobWriteSessionConfigTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITParallelCompositeUploadBlobWriteSessionConfigTest.java
index 3352a92a..9b7d630f 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITParallelCompositeUploadBlobWriteSessionConfigTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITParallelCompositeUploadBlobWriteSessionConfigTest.java
@@ -18,7 +18,6 @@ package com.google.cloud.storage.it;
 
 import static com.google.cloud.storage.TestUtils.xxd;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
 import static org.junit.Assert.assertThrows;
 
 import com.google.api.gax.paging.Page;
@@ -30,8 +29,6 @@ import com.google.cloud.storage.BlobWriteSession;
 import com.google.cloud.storage.BlobWriteSessionConfigs;
 import com.google.cloud.storage.BucketInfo;
 import com.google.cloud.storage.DataGenerator;
-import com.google.cloud.storage.GrpcStorageOptions;
-import com.google.cloud.storage.HttpStorageOptions;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.BufferAllocationStrategy;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.ExecutorSupplier;
@@ -119,22 +116,8 @@ public final class ITParallelCompositeUploadBlobWriteSessionConfigTest {
             // let our fixtures take care of cleaning things
             .withPartCleanupStrategy(PartCleanupStrategy.never());
 
-    StorageOptions storageOptions = null;
-    if (transport == Transport.GRPC) {
-      storageOptions =
-          ((GrpcStorageOptions) injectedStorage.getOptions())
-              .toBuilder()
-              .setBlobWriteSessionConfig(pcu)
-              .build();
-    } else if (transport == Transport.HTTP) {
-      storageOptions =
-          ((HttpStorageOptions) injectedStorage.getOptions())
-              .toBuilder()
-              .setBlobWriteSessionConfig(pcu)
-              .build();
-    }
-    assertWithMessage("unable to resolve options").that(storageOptions).isNotNull();
-    //noinspection DataFlowIssue
+    StorageOptions storageOptions =
+        injectedStorage.getOptions().toBuilder().setBlobWriteSessionConfig(pcu).build();
     storage = storageOptions.getService();
     rand = new Random();
   }