Repository: googleapis/java-storage
Pull request #1331 (2022-03-30 21:23:36)

deps: update kms.version to v0.95.2
[![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.api.grpc:proto-google-cloud-kms-v1](https://togithub.com/googleapis/java-kms) | `0.95.1` -> `0.95.2` | [![age](https://badges.renovateapi.com/packages/maven/com.google.api.grpc:proto-google-cloud-kms-v1/0.95.2/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/com.google.api.grpc:proto-google-cloud-kms-v1/0.95.2/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/com.google.api.grpc:proto-google-cloud-kms-v1/0.95.2/compatibility-slim/0.95.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/com.google.api.grpc:proto-google-cloud-kms-v1/0.95.2/confidence-slim/0.95.1)](https://docs.renovatebot.com/merge-confidence/) |
| [com.google.api.grpc:grpc-google-cloud-kms-v1](https://togithub.com/googleapis/java-kms) | `0.95.1` -> `0.95.2` | [![age](https://badges.renovateapi.com/packages/maven/com.google.api.grpc:grpc-google-cloud-kms-v1/0.95.2/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/com.google.api.grpc:grpc-google-cloud-kms-v1/0.95.2/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/com.google.api.grpc:grpc-google-cloud-kms-v1/0.95.2/compatibility-slim/0.95.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/com.google.api.grpc:grpc-google-cloud-kms-v1/0.95.2/confidence-slim/0.95.1)](https://docs.renovatebot.com/merge-confidence/) |

---

### Configuration

📅 **Schedule**: At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Renovate will not automatically rebase this PR, because other commits have been found.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, click this checkbox.

---

This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-storage).


Patch:
diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml
index 47afe3b4..5de852ee 100644
--- a/.github/sync-repo-settings.yaml
+++ b/.github/sync-repo-settings.yaml
@@ -91,6 +91,20 @@ branchProtectionRules:
       - units (11)
       - 'Kokoro - Test: Integration'
       - cla/google
+  - pattern: feat/grpc-storage
+    isAdminEnforced: true
+    requiredApprovingReviewCount: 1
+    requiresCodeOwnerReviews: true
+    requiresStrictStatusChecks: false
+    requiredStatusCheckContexts:
+      - dependencies (8)
+      - dependencies (11)
+      - lint
+      - clirr
+      - units (8)
+      - units (11)
+      - 'Kokoro - Test: Integration'
+      - cla/google
 permissionRules:
   - team: yoshi-admins
     permission: admin
diff --git a/README.md b/README.md
index 5f38caab..f6208c76 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>libraries-bom</artifactId>
-      <version>25.0.0</version>
+      <version>25.1.0</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
@@ -41,7 +41,7 @@ If you are using Maven without BOM, add this to your dependencies:
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.5.1</version>
+  <version>2.6.0</version>
 </dependency>
 
 ```
@@ -49,20 +49,20 @@ If you are using Maven without BOM, add this to your dependencies:
 If you are using Gradle 5.x or later, add this to your dependencies
 
 ```Groovy
-implementation platform('com.google.cloud:libraries-bom:25.0.0')
+implementation platform('com.google.cloud:libraries-bom:25.1.0')
 
 implementation 'com.google.cloud:google-cloud-storage'
 ```
 If you are using Gradle without BOM, add this to your dependencies
 
 ```Groovy
-implementation 'com.google.cloud:google-cloud-storage:2.5.1'
+implementation 'com.google.cloud:google-cloud-storage:2.6.0'
 ```
 
 If you are using SBT, add this to your dependencies
 
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.5.1"
+libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.6.0"
 ```
 
 ## Authentication
@@ -240,9 +240,12 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-storage/tree/
 | Change Default Storage Class | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/ChangeDefaultStorageClass.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/ChangeDefaultStorageClass.java) |
 | Configure Bucket Cors | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/ConfigureBucketCors.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/ConfigureBucketCors.java) |
 | Create Bucket | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucket.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/CreateBucket.java) |
+| Create Bucket Dual Region | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketDualRegion.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketDualRegion.java) |
+| Create Bucket Pub Sub Notification | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketPubSubNotification.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketPubSubNotification.java) |
 | Create Bucket With Storage Class And Location | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithStorageClassAndLocation.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithStorageClassAndLocation.java) |
 | Create Bucket With Turbo Replication | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithTurboReplication.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketWithTurboReplication.java) |
 | Delete Bucket | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/DeleteBucket.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/DeleteBucket.java) |
+| Delete Bucket Pub Sub Notification | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/DeleteBucketPubSubNotification.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/DeleteBucketPubSubNotification.java) |
 | Disable Bucket Versioning | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/DisableBucketVersioning.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/DisableBucketVersioning.java) |
 | Disable Default Event Based Hold | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/DisableDefaultEventBasedHold.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/DisableDefaultEventBasedHold.java) |
 | Disable Lifecycle Management | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/DisableLifecycleManagement.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/DisableLifecycleManagement.java) |
@@ -262,10 +265,12 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-storage/tree/
 | Get Uniform Bucket Level Access | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/GetUniformBucketLevelAccess.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/GetUniformBucketLevelAccess.java) |
 | List Bucket Iam Members | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/ListBucketIamMembers.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/ListBucketIamMembers.java) |
 | List Buckets | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/ListBuckets.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/ListBuckets.java) |
+| List Pub Sub Notifications | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/ListPubSubNotifications.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/ListPubSubNotifications.java) |
 | Lock Retention Policy | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/LockRetentionPolicy.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/LockRetentionPolicy.java) |
 | Make Bucket Public | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/MakeBucketPublic.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/MakeBucketPublic.java) |
 | Print Bucket Acl | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/PrintBucketAcl.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/PrintBucketAcl.java) |
 | Print Bucket Acl Filter By User | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/PrintBucketAclFilterByUser.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/PrintBucketAclFilterByUser.java) |
+| Print Pub Sub Notification | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/PrintPubSubNotification.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/PrintPubSubNotification.java) |
 | Remove Bucket Cors | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/RemoveBucketCors.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/RemoveBucketCors.java) |
 | Remove Bucket Default Kms Key | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/RemoveBucketDefaultKmsKey.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/RemoveBucketDefaultKmsKey.java) |
 | Remove Bucket Default Owner | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/bucket/RemoveBucketDefaultOwner.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/bucket/RemoveBucketDefaultOwner.java) |
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index db88133b..1929f718 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -16,7 +16,7 @@
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
-    <kms.version>0.95.1</kms.version>
+    <kms.version>0.95.2</kms.version>
   </properties>
   <dependencies>
     <dependency>
@@ -92,6 +92,10 @@
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-core</artifactId>
     </dependency>
+    <dependency>
+      <groupId>com.google.code.findbugs</groupId>
+      <artifactId>jsr305</artifactId>
+    </dependency>
 
     <!-- Test dependencies -->
     <dependency>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
index 5e06b6ce..e03110ff 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
@@ -1224,7 +1224,7 @@ public class BucketInfo implements Serializable {
 
     /**
      * Sets the bucket's location. Data for blobs in the bucket resides in physical storage within
-     * this region. A list of supported values is available <a
+     * this region or regions. A list of supported values is available <a
      * href="https://cloud.google.com/storage/docs/bucket-locations">here</a>.
      */
     public abstract Builder setLocation(String location);
@@ -1731,7 +1731,7 @@ public class BucketInfo implements Serializable {
 
   /**
    * Returns the bucket's location. Data for blobs in the bucket resides in physical storage within
-   * this region.
+   * this region or regions.
    *
    * @see <a href="https://cloud.google.com/storage/docs/bucket-locations">Bucket Locations</a>
    */
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 3daf2c17..51ce17bf 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
@@ -22,6 +22,7 @@ import com.google.api.core.ApiClock;
 import com.google.api.gax.retrying.ResultRetryAlgorithm;
 import com.google.api.gax.retrying.RetrySettings;
 import com.google.cloud.RetryHelper.RetryHelperException;
+import com.google.cloud.storage.spi.v1.HttpRpcContext;
 import java.util.concurrent.Callable;
 import java.util.function.Function;
 
@@ -47,11 +48,15 @@ final class Retrying {
    */
   static <T, U> U run(
       StorageOptions options, ResultRetryAlgorithm<?> algorithm, Callable<T> c, Function<T, U> f) {
+    HttpRpcContext httpRpcContext = HttpRpcContext.getInstance();
     try {
+      httpRpcContext.newInvocationId();
       T result = runWithRetries(c, options.getRetrySettings(), algorithm, options.getClock());
       return result == null ? null : f.apply(result);
     } catch (RetryHelperException e) {
       throw StorageException.coalesce(e);
+    } finally {
+      httpRpcContext.clearInvocationId();
     }
   }
 }
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 f3de842f..49f1b9e0 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
@@ -38,8 +38,9 @@ public class StorageOptions extends ServiceOptions<Storage, StorageOptions> {
   private static final String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control";
   private static final Set<String> SCOPES = ImmutableSet.of(GCS_SCOPE);
   private static final String DEFAULT_HOST = "https://storage.googleapis.com";
-
+  private static final boolean DEFAULT_INCLUDE_INVOCATION_ID = true;
   private final RetryAlgorithmManager retryAlgorithmManager;
+  private final boolean includeInvocationId;
 
   public static class DefaultStorageFactory implements StorageFactory {
 
@@ -64,11 +65,13 @@ public class StorageOptions extends ServiceOptions<Storage, StorageOptions> {
   public static class Builder extends ServiceOptions.Builder<Storage, StorageOptions, Builder> {
 
     private StorageRetryStrategy storageRetryStrategy;
+    private boolean includeInvocationId;
 
     private Builder() {}
 
     private Builder(StorageOptions options) {
       super(options);
+      this.includeInvocationId = options.includeInvocationId;
     }
 
     @Override
@@ -93,6 +96,17 @@ public class StorageOptions extends ServiceOptions<Storage, StorageOptions> {
       return this;
     }
 
+    /**
+     * Override default enablement of invocation id added to x-goog-api-client header.
+     *
+     * @param includeInvocationId a boolean to change enablement of invocation id
+     * @return the builder
+     */
+    public Builder setIncludeInvocationId(boolean includeInvocationId) {
+      this.includeInvocationId = includeInvocationId;
+      return this;
+    }
+
     @Override
     public StorageOptions build() {
       return new StorageOptions(this, new StorageDefaults());
@@ -105,6 +119,7 @@ public class StorageOptions extends ServiceOptions<Storage, StorageOptions> {
         new RetryAlgorithmManager(
             MoreObjects.firstNonNull(
                 builder.storageRetryStrategy, serviceDefaults.getStorageRetryStrategy()));
+    this.includeInvocationId = builder.includeInvocationId;
   }
 
   private static class StorageDefaults implements ServiceDefaults<Storage, StorageOptions> {
@@ -127,6 +142,10 @@ public class StorageOptions extends ServiceOptions<Storage, StorageOptions> {
     public StorageRetryStrategy getStorageRetryStrategy() {
       return StorageRetryStrategy.getDefaultStorageRetryStrategy();
     }
+
+    public boolean isIncludeInvocationId() {
+      return DEFAULT_INCLUDE_INVOCATION_ID;
+    }
   }
 
   public static HttpTransportOptions getDefaultHttpTransportOptions() {
@@ -153,6 +172,11 @@ public class StorageOptions extends ServiceOptions<Storage, StorageOptions> {
     return retryAlgorithmManager;
   }
 
+  /** Returns if Invocation ID is enabled and transmitted through x-goog-api-client header. */
+  public boolean isIncludeInvocationId() {
+    return includeInvocationId;
+  }
+
   /** Returns a default {@code StorageOptions} instance. */
   public static StorageOptions getDefaultInstance() {
     return newBuilder().build();
@@ -180,6 +204,8 @@ public class StorageOptions extends ServiceOptions<Storage, StorageOptions> {
   }
 
   public static Builder newBuilder() {
-    return new Builder().setHost(DEFAULT_HOST);
+    return new Builder()
+        .setHost(DEFAULT_HOST)
+        .setIncludeInvocationId(DEFAULT_INCLUDE_INVOCATION_ID);
   }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpRpcContext.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpRpcContext.java
new file mode 100644
index 00000000..a62e7f99
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpRpcContext.java
@@ -0,0 +1,72 @@
+/*
+ * 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.spi.v1;
+
+import com.google.api.core.InternalApi;
+import java.util.UUID;
+import java.util.function.Supplier;
+import javax.annotation.Nullable;
+
+@InternalApi
+public final class HttpRpcContext {
+
+  private static final Object GET_INSTANCE_LOCK = new Object();
+
+  private static volatile HttpRpcContext instance;
+
+  private final ThreadLocal<UUID> invocationId;
+  private final Supplier<UUID> supplier;
+
+  HttpRpcContext(Supplier<UUID> randomUUID) {
+    this.invocationId = new InheritableThreadLocal<>();
+    this.supplier = randomUUID;
+  }
+
+  @InternalApi
+  @Nullable
+  public UUID getInvocationId() {
+    return invocationId.get();
+  }
+
+  @InternalApi
+  public UUID newInvocationId() {
+    invocationId.set(supplier.get());
+    return getInvocationId();
+  }
+
+  @InternalApi
+  public void clearInvocationId() {
+    invocationId.remove();
+  }
+
+  @InternalApi
+  public static HttpRpcContext init() {
+    return new HttpRpcContext(UUID::randomUUID);
+  }
+
+  @InternalApi
+  public static HttpRpcContext getInstance() {
+    if (instance == null) {
+      synchronized (GET_INSTANCE_LOCK) {
+        if (instance == null) {
+          instance = init();
+        }
+      }
+    }
+    return instance;
+  }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
index d0c1ff8a..e990a004 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
@@ -18,6 +18,7 @@ package com.google.cloud.storage.spi.v1;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
 
 import com.google.api.client.googleapis.batch.BatchRequest;
@@ -26,6 +27,7 @@ import com.google.api.client.googleapis.json.GoogleJsonError;
 import com.google.api.client.http.ByteArrayContent;
 import com.google.api.client.http.EmptyContent;
 import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpExecuteInterceptor;
 import com.google.api.client.http.HttpHeaders;
 import com.google.api.client.http.HttpRequest;
 import com.google.api.client.http.HttpRequestFactory;
@@ -86,6 +88,8 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.UUID;
+import javax.annotation.Nullable;
 
 public class HttpStorageRpc implements StorageRpc {
   public static final String DEFAULT_PROJECTION = "full";
@@ -114,6 +118,9 @@ public class HttpStorageRpc implements StorageRpc {
     // Open Census initialization
     censusHttpModule = new CensusHttpModule(tracer, true);
     initializer = censusHttpModule.getHttpRequestInitializer(initializer);
+    if (options.isIncludeInvocationId()) {
+      initializer = new InvocationIdInitializer(initializer);
+    }
     batchRequestInitializer = censusHttpModule.getHttpRequestInitializer(null);
     storage =
         new Storage.Builder(transport, new JacksonFactory(), initializer)
@@ -122,6 +129,54 @@ public class HttpStorageRpc implements StorageRpc {
             .build();
   }
 
+  private static final class InvocationIdInitializer implements HttpRequestInitializer {
+    @Nullable HttpRequestInitializer initializer;
+
+    private InvocationIdInitializer(@Nullable HttpRequestInitializer initializer) {
+      this.initializer = initializer;
+    }
+
+    @Override
+    public void initialize(HttpRequest request) throws IOException {
+      checkNotNull(request);
+      if (this.initializer != null) {
+        this.initializer.initialize(request);
+      }
+      request.setInterceptor(new InvocationIdInterceptor(request.getInterceptor()));
+    }
+  }
+
+  private static final class InvocationIdInterceptor implements HttpExecuteInterceptor {
+    @Nullable HttpExecuteInterceptor interceptor;
+
+    private InvocationIdInterceptor(@Nullable HttpExecuteInterceptor interceptor) {
+      this.interceptor = interceptor;
+    }
+
+    @Override
+    public void intercept(HttpRequest request) throws IOException {
+      checkNotNull(request);
+      if (this.interceptor != null) {
+        this.interceptor.intercept(request);
+      }
+      UUID invocationId = HttpRpcContext.getInstance().getInvocationId();
+      final String signatureKey = "Signature="; // For V2 and V4 signedURLs
+      final String builtURL = request.getUrl().build();
+      if (invocationId != null && !builtURL.contains(signatureKey)) {
+        HttpHeaders headers = request.getHeaders();
+        String existing = (String) headers.get("x-goog-api-client");
+        String invocationEntry = "gccl-invocation-id/" + invocationId;
+        final String newValue;
+        if (existing != null && !existing.isEmpty()) {
+          newValue = existing + " " + invocationEntry;
+        } else {
+          newValue = invocationEntry;
+        }
+        headers.set("x-goog-api-client", newValue);
+      }
+    }
+  }
+
   private class DefaultRpcBatch implements RpcBatch {
 
     // Batch size is limited as, due to some current service implementation details, the service
diff --git a/pom.xml b/pom.xml
index f56d9c8f..b1e871af 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,12 +75,12 @@
       <dependency>
         <groupId>com.google.apis</groupId>
         <artifactId>google-api-services-storage</artifactId>
-        <version>v1-rev20220210-1.32.1</version>
+        <version>v1-rev20220401-1.32.1</version>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-pubsub</artifactId>
-        <version>1.116.2</version>
+        <version>1.116.3</version>
         <scope>test</scope>
       </dependency>
       <dependency>
@@ -126,7 +126,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-conformance-tests</artifactId>
-        <version>0.2.9</version>
+        <version>0.2.10</version>
         <scope>test</scope>
         <exclusions>
           <exclusion>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index 4ab5ccb9..add91938 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -30,7 +30,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.5.1</version>
+      <version>2.6.0</version>
     </dependency>
     <!-- [END storage_install_without_bom] -->
 
@@ -58,6 +58,12 @@
       <version>4.5.13</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.google.cloud</groupId>
+      <artifactId>google-cloud-pubsub</artifactId>
+      <version>1.116.3</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <!-- compile and run all snippet tests -->
diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml
index 36e2b626..b9065261 100644
--- a/samples/native-image-sample/pom.xml
+++ b/samples/native-image-sample/pom.xml
@@ -29,7 +29,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>25.0.0</version>
+        <version>25.1.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -58,6 +58,12 @@
       <version>1.1.3</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.google.cloud</groupId>
+      <artifactId>google-cloud-pubsub</artifactId>
+      <version>1.116.3</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
@@ -108,7 +114,7 @@
         <dependency>
           <groupId>com.google.cloud</groupId>
           <artifactId>native-image-support</artifactId>
-          <version>0.12.10</version>
+          <version>0.12.11</version>
         </dependency>
         <dependency>
           <groupId>org.junit.vintage</groupId>
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index b890f1ba..f8a74963 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -49,6 +49,12 @@
       <version>4.5.13</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.google.cloud</groupId>
+      <artifactId>google-cloud-pubsub</artifactId>
+      <version>1.116.3</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <!-- compile and run all snippet tests -->
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 54b99b74..01df9ad7 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -31,7 +31,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>25.0.0</version>
+        <version>25.1.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -69,5 +69,11 @@
       <version>4.5.13</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.google.cloud</groupId>
+      <artifactId>google-cloud-pubsub</artifactId>
+      <version>1.116.3</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 </project>
diff --git a/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketDualRegion.java b/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketDualRegion.java
new file mode 100644
index 00000000..e5a3fccb
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketDualRegion.java
@@ -0,0 +1,54 @@
+/*
+ * 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.example.storage.bucket;
+
+// [START storage_create_bucket_dual_region]
+
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+
+public class CreateBucketDualRegion {
+
+  public static void createBucketDualRegion(
+      String projectId, String bucketName, String firstRegion, String secondRegion) {
+    // The ID of your GCP project
+    // String projectId = "your-project-id";
+
+    // The ID to give your GCS bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    // One of the regions the dual region bucket is to be created in.
+    // String firstRegion = "US-EAST1";
+
+    // The second region the dual region bucket is to be created in.
+    // String secondRegion = "US-WEST1";
+
+    // Construct the dual region ie. "US-EAST1+US-WEST1"
+    String dualRegion = firstRegion + "+" + secondRegion;
+
+    Storage storage = StorageOptions.newBuilder().setProjectId(projectId).build().getService();
+
+    Bucket bucket =
+        storage.create(BucketInfo.newBuilder(bucketName).setLocation(dualRegion).build());
+
+    System.out.println(
+        "Created bucket " + bucket.getName() + " in location " + bucket.getLocation());
+  }
+}
+// [END storage_create_bucket_dual_region]
diff --git a/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketPubSubNotification.java b/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketPubSubNotification.java
new file mode 100644
index 00000000..da2b3b3d
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/bucket/CreateBucketPubSubNotification.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.example.storage.bucket;
+
+// [START storage_create_bucket_notifications]
+import com.google.cloud.storage.Notification;
+import com.google.cloud.storage.NotificationInfo;
+import com.google.cloud.storage.NotificationInfo.EventType;
+import com.google.cloud.storage.NotificationInfo.PayloadFormat;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+import java.util.Map;
+
+public class CreateBucketPubSubNotification {
+
+  public static void createBucketPubSubNotification(
+      String bucketName,
+      String topicName,
+      Map<String, String> customAttributes,
+      EventType[] eventTypes,
+      String objectNamePrefix,
+      PayloadFormat payloadFormat) {
+    // The ID to give your GCS bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    // The name of the topic you would like to create a notification for
+    // String topicName = "projects/{your-project}/topics/{your-topic}";
+
+    // Any custom attributes
+    // Map<String, String> customAttributes = Map.of("label", "value");
+
+    // The object name prefix for which this notification configuration applies
+    // String objectNamePrefix = "blob-";
+
+    // Desired content of the Payload
+    // PayloadFormat payloadFormat = PayloadFormat.JSON_API_V1.JSON_API_V1;
+
+    Storage storage = StorageOptions.newBuilder().build().getService();
+    NotificationInfo notificationInfo =
+        NotificationInfo.newBuilder(topicName)
+            .setCustomAttributes(customAttributes)
+            .setEventTypes(eventTypes)
+            .setObjectNamePrefix(objectNamePrefix)
+            .setPayloadFormat(payloadFormat)
+            .build();
+    Notification notification = storage.createNotification(bucketName, notificationInfo);
+    String topic = notification.getTopic();
+    System.out.println("Successfully created notification for topic " + topic);
+  }
+}
+// [END storage_create_bucket_notifications]
diff --git a/samples/snippets/src/main/java/com/example/storage/bucket/DeleteBucketPubSubNotification.java b/samples/snippets/src/main/java/com/example/storage/bucket/DeleteBucketPubSubNotification.java
new file mode 100644
index 00000000..dd59e2e1
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/bucket/DeleteBucketPubSubNotification.java
@@ -0,0 +1,41 @@
+/*
+ * 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.example.storage.bucket;
+
+// [START storage_delete_bucket_notification]
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+
+public class DeleteBucketPubSubNotification {
+
+  public static void deleteBucketPubSubNotification(String bucketName, String notificationId) {
+    // The ID to give your GCS bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    // The NotificationId for the notification you would like to delete
+    // String notificationId = "your-unique-notification-id"
+
+    Storage storage = StorageOptions.newBuilder().build().getService();
+    boolean success = storage.deleteNotification(bucketName, notificationId);
+    if (success) {
+      System.out.println("Successfully deleted notification");
+    } else {
+      System.out.println("Failed to find notification");
+    }
+  }
+}
+// [END storage_delete_bucket_notification]
diff --git a/samples/snippets/src/main/java/com/example/storage/bucket/ListPubSubNotifications.java b/samples/snippets/src/main/java/com/example/storage/bucket/ListPubSubNotifications.java
new file mode 100644
index 00000000..b3fb614f
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/bucket/ListPubSubNotifications.java
@@ -0,0 +1,39 @@
+/*
+ * 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.example.storage.bucket;
+
+// [START storage_list_bucket_notifications]
+import com.google.cloud.storage.Notification;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+import java.util.List;
+
+public class ListPubSubNotifications {
+
+  public static void listPubSubNotifications(String bucketName) {
+    // The ID to give your GCS bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    Storage storage = StorageOptions.newBuilder().build().getService();
+    List<Notification> notificationList = storage.listNotifications(bucketName);
+    for (Notification notification : notificationList) {
+      System.out.println(
+          "Found notification " + notification.getTopic() + " for bucket " + bucketName);
+    }
+  }
+}
+// [END storage_list_bucket_notifications]
diff --git a/samples/snippets/src/main/java/com/example/storage/bucket/PrintPubSubNotification.java b/samples/snippets/src/main/java/com/example/storage/bucket/PrintPubSubNotification.java
new file mode 100644
index 00000000..083a0b55
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/bucket/PrintPubSubNotification.java
@@ -0,0 +1,39 @@
+/*
+ * 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.example.storage.bucket;
+
+// [START storage_print_pubsub_bucket_notification]
+import com.google.cloud.storage.Notification;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+
+public class PrintPubSubNotification {
+
+  public static void printPubSubNotification(String bucketName, String notificationId) {
+    // The ID to give your GCS bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    // The Pub/Sub topic you would like to find
+    // String notificationId = "your-unique-notification-id"
+
+    Storage storage = StorageOptions.newBuilder().build().getService();
+    Notification notification = storage.getNotification(bucketName, notificationId);
+    System.out.println(
+        "Found notification " + notification.getTopic() + " for bucket " + bucketName);
+  }
+}
+// [END storage_print_pubsub_bucket_notification]


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java
index 5c9c708c..93d36b6e 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java
@@ -17,6 +17,8 @@
 package com.google.cloud.storage;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import com.google.cloud.TransportOptions;
 import org.easymock.EasyMock;
@@ -65,4 +67,18 @@ public class StorageOptionsTest {
 
     assertThat(opts1.getHost()).isEqualTo("https://storage.googleapis.com");
   }
+
+  @Test
+  public void testDefaultInvocationId() {
+    StorageOptions opts1 = StorageOptions.getDefaultInstance();
+
+    assertTrue(opts1.isIncludeInvocationId());
+  }
+
+  @Test
+  public void testDisableInvocationId() {
+    StorageOptions opts1 = StorageOptions.newBuilder().setIncludeInvocationId(false).build();
+
+    assertFalse(opts1.isIncludeInvocationId());
+  }
 }
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 80d1ebc3..d755d38d 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
@@ -3560,6 +3560,16 @@ public class ITStorageTest {
     RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
   }
 
+  @Test
+  public void testBucketLocationDualRegion() {
+    String bucketName = RemoteStorageHelper.generateBucketName();
+    String dualRegionLocation = "US-EAST1+US-WEST1";
+    Bucket bucket =
+        storage.create(BucketInfo.newBuilder(bucketName).setLocation(dualRegionLocation).build());
+    assertEquals(bucket.getLocation(), dualRegionLocation);
+    assertEquals(bucket.getLocationType(), "dual-region");
+  }
+
   @Test
   public void testBucketLogging() throws ExecutionException, InterruptedException {
     String logsBucket = RemoteStorageHelper.generateBucketName();
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/spi/v1/HttpRpcContextTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/spi/v1/HttpRpcContextTest.java
new file mode 100644
index 00000000..f1e93f5c
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/spi/v1/HttpRpcContextTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.spi.v1;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.cloud.TransportOptions;
+import com.google.cloud.Tuple;
+import com.google.cloud.WriteChannel;
+import com.google.cloud.http.HttpTransportOptions;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import org.junit.Test;
+
+public class HttpRpcContextTest {
+  @Test
+  public void testNewInvocationId() {
+    UUID uuid = UUID.fromString("28220dff-1e8b-4770-9e10-022c2a99d8f3");
+    HttpRpcContext testContext = new HttpRpcContext(() -> uuid);
+
+    assertThat(testContext.newInvocationId()).isEqualTo(uuid);
+    assertThat(testContext.getInvocationId()).isEqualTo(uuid);
+    // call again to ensure the id is consistent with our supplier
+    assertThat(testContext.newInvocationId()).isEqualTo(uuid);
+    assertThat(testContext.getInvocationId()).isEqualTo(uuid);
+  }
+
+  @Test
+  public void testInvocationIdIsPassedThrough() {
+    MockLowLevelHttpResponse response =
+        new MockLowLevelHttpResponse()
+            .setContentType("application/json")
+            .setContent(
+                "{\n"
+                    + "  \"kind\": \"storage#serviceAccount\",\n"
+                    + "  \"email_address\": \"service-234234@gs-project-accounts.iam.gserviceaccount.com\"\n"
+                    + "}\n")
+            .setStatusCode(200);
+    AuditingHttpTransport transport = new AuditingHttpTransport(response);
+    TransportOptions transportOptions =
+        HttpTransportOptions.newBuilder().setHttpTransportFactory(() -> transport).build();
+    Storage service =
+        StorageOptions.getDefaultInstance()
+            .toBuilder()
+            .setTransportOptions(transportOptions)
+            .build()
+            .getService();
+    service.getServiceAccount("test-project");
+    Optional<Tuple<String, String>> anyXGoogApiClientWithGcclInvocationId =
+        transport.getAddHeaderCalls().stream()
+            .filter(t -> "x-goog-api-client".equals(t.x()) && t.y().contains("gccl-invocation-id/"))
+            .findFirst();
+    assertTrue(anyXGoogApiClientWithGcclInvocationId.isPresent());
+    assertThat(transport.getBuildRequestCalls()).hasSize(1);
+  }
+
+  @Test
+  public void testInvocationIdIsNotPassedThroughWhenDisabled() {
+    MockLowLevelHttpResponse response =
+        new MockLowLevelHttpResponse()
+            .setContentType("application/json")
+            .setContent(
+                "{\n"
+                    + "  \"kind\": \"storage#serviceAccount\",\n"
+                    + "  \"email_address\": \"service-234234@gs-project-accounts.iam.gserviceaccount.com\"\n"
+                    + "}\n")
+            .setStatusCode(200);
+    AuditingHttpTransport transport = new AuditingHttpTransport(response);
+    TransportOptions transportOptions =
+        HttpTransportOptions.newBuilder().setHttpTransportFactory(() -> transport).build();
+    Storage service =
+        StorageOptions.getDefaultInstance()
+            .toBuilder()
+            .setTransportOptions(transportOptions)
+            .setIncludeInvocationId(false)
+            .build()
+            .getService();
+    service.getServiceAccount("test-project");
+    Optional<Tuple<String, String>> anyXGoogApiClientWithGcclInvocationId =
+        transport.getAddHeaderCalls().stream()
+            .filter(t -> "x-goog-api-client".equals(t.x()) && t.y().contains("gccl-invocation-id/"))
+            .findFirst();
+
+    assertFalse(anyXGoogApiClientWithGcclInvocationId.isPresent());
+    assertThat(transport.getBuildRequestCalls()).hasSize(1);
+  }
+
+  @Test
+  public void testInvocationIdNotInSignedURL_v2() throws IOException {
+    URL signedUrlV2 =
+        new URL(
+            "http://www.test.com/test-bucket/test1.txt?GoogleAccessId=testClient-test@test.com&Expires=1553839761&Signature=MJUBXAZ7");
+    doTestInvocationIdNotInSignedURL(signedUrlV2);
+  }
+
+  @Test
+  public void testInvocationIdNotInSignedURL_v4() throws IOException {
+    URL signedUrlV4 =
+        new URL(
+            "http://www.test.com/test-bucket/test1.txt?X-Goog-Algorithm=&X-Goog-Credential=&X-Goog-Date=&X-Goog-Expires=&X-Goog-SignedHeaders=&X-Goog-Signature=MJUBXAZ7");
+    doTestInvocationIdNotInSignedURL(signedUrlV4);
+  }
+
+  private void doTestInvocationIdNotInSignedURL(URL signedUrl) throws IOException {
+    MockLowLevelHttpResponse response =
+        new MockLowLevelHttpResponse()
+            .setContentType("text/plain")
+            .setHeaderNames(ImmutableList.of("Location"))
+            .setHeaderValues(ImmutableList.of("http://test"))
+            .setStatusCode(201);
+    AuditingHttpTransport transport = new AuditingHttpTransport(response);
+    TransportOptions transportOptions =
+        HttpTransportOptions.newBuilder().setHttpTransportFactory(() -> transport).build();
+    Storage service =
+        StorageOptions.getDefaultInstance()
+            .toBuilder()
+            .setTransportOptions(transportOptions)
+            .build()
+            .getService();
+    WriteChannel writer = service.writer(signedUrl);
+    writer.write(ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)));
+    Optional<Tuple<String, String>> anyXGoogApiClientWithGcclInvocationId =
+        transport.getAddHeaderCalls().stream()
+            .filter(t -> "x-goog-api-client".equals(t.x()) && t.y().contains("gccl-invocation-id/"))
+            .findFirst();
+    assertFalse(anyXGoogApiClientWithGcclInvocationId.isPresent());
+    assertThat(transport.getBuildRequestCalls()).hasSize(1);
+  }
+
+  private static final class AuditingHttpTransport extends HttpTransport {
+    private final LowLevelHttpResponse response;
+    private final List<Tuple<String, String>> buildRequestCalls;
+    private final List<Tuple<String, String>> addHeaderCalls;
+
+    private AuditingHttpTransport(LowLevelHttpResponse response) {
+      this.response = response;
+      this.buildRequestCalls = Collections.synchronizedList(new ArrayList<>());
+      this.addHeaderCalls = Collections.synchronizedList(new ArrayList<>());
+    }
+
+    public List<Tuple<String, String>> getBuildRequestCalls() {
+      return ImmutableList.copyOf(buildRequestCalls);
+    }
+
+    public List<Tuple<String, String>> getAddHeaderCalls() {
+      return ImmutableList.copyOf(addHeaderCalls);
+    }
+
+    @Override
+    protected LowLevelHttpRequest buildRequest(String method, String url) {
+      buildRequestCalls.add(Tuple.of(method, url));
+      return new LowLevelHttpRequest() {
+        @Override
+        public void addHeader(String name, String value) {
+          addHeaderCalls.add(Tuple.of(name, value));
+        }
+
+        @Override
+        public LowLevelHttpResponse execute() {
+          return response;
+        }
+      };
+    }
+  }
+}
diff --git a/samples/snippets/src/test/java/com/example/storage/bucket/CreateBucketDualRegionTest.java b/samples/snippets/src/test/java/com/example/storage/bucket/CreateBucketDualRegionTest.java
new file mode 100644
index 00000000..8d5222f1
--- /dev/null
+++ b/samples/snippets/src/test/java/com/example/storage/bucket/CreateBucketDualRegionTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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.example.storage.bucket;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
+
+import com.example.storage.TestBase;
+import com.google.cloud.storage.testing.RemoteStorageHelper;
+import org.junit.Test;
+
+public class CreateBucketDualRegionTest extends TestBase {
+
+  private static final String PROJECT_ID = System.getenv("GOOGLE_CLOUD_PROJECT");
+
+  @Test
+  public void testCreateBucketDualRegion() {
+    assertNotNull("Unable to determine Project ID", PROJECT_ID);
+    String newBucket = RemoteStorageHelper.generateBucketName();
+    CreateBucketDualRegion.createBucketDualRegion(PROJECT_ID, newBucket, "US-EAST1", "US-WEST1");
+    assertThat(stdOut.getCapturedOutputAsUtf8String()).contains("US-WEST1");
+    assertThat(stdOut.getCapturedOutputAsUtf8String()).contains("US-EAST1");
+    assertThat(stdOut.getCapturedOutputAsUtf8String()).contains("Created bucket");
+  }
+}
diff --git a/samples/snippets/src/test/java/com/example/storage/bucket/CreateBucketPubSubNotificationTest.java b/samples/snippets/src/test/java/com/example/storage/bucket/CreateBucketPubSubNotificationTest.java
new file mode 100644
index 00000000..bb28ec6d
--- /dev/null
+++ b/samples/snippets/src/test/java/com/example/storage/bucket/CreateBucketPubSubNotificationTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.example.storage.bucket;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
+
+import com.example.storage.TestBase;
+import com.google.cloud.pubsub.v1.TopicAdminClient;
+import com.google.cloud.storage.Notification;
+import com.google.cloud.storage.NotificationInfo.EventType;
+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.Map;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class CreateBucketPubSubNotificationTest extends TestBase {
+
+  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 String PROJECT = System.getenv("GOOGLE_CLOUD_PROJECT");
+  private static final String TOPIC = String.format("projects/%s/topics/new-topic-create", PROJECT);
+  private static final String OBJECT_NAME_PREFIX = "index.html";
+  private static final EventType[] EVENT_TYPES = {
+    EventType.OBJECT_FINALIZE, EventType.OBJECT_METADATA_UPDATE
+  };
+  private static TopicAdminClient topicAdminClient;
+
+  @BeforeClass
+  public static void configureTopicAdminClient() throws IOException {
+    if (PROJECT != null) {
+      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);
+    }
+  }
+
+  @AfterClass
+  public static void deleteTopicAndClient() {
+    /* Delete the Pub/Sub topic */
+    if (topicAdminClient != null) {
+      topicAdminClient.deleteTopic(TOPIC);
+      topicAdminClient.close();
+    }
+  }
+
+  @Test
+  public void testCreateBucketPubSubNotification() {
+    // Check that we can access project value and that topic admin client came up successfully
+    assertNotNull("Unable to determine project", PROJECT);
+    assertNotNull("Topic Admin Client did not start up", topicAdminClient);
+
+    com.example.storage.bucket.CreateBucketPubSubNotification.createBucketPubSubNotification(
+        bucketName, TOPIC, CUSTOM_ATTRIBUTES, EVENT_TYPES, OBJECT_NAME_PREFIX, PAYLOAD_FORMAT);
+    assertThat(stdOut.getCapturedOutputAsUtf8String()).contains(TOPIC);
+  }
+}
diff --git a/samples/snippets/src/test/java/com/example/storage/bucket/DeleteBucketPubSubNotificationTest.java b/samples/snippets/src/test/java/com/example/storage/bucket/DeleteBucketPubSubNotificationTest.java
new file mode 100644
index 00000000..e094f334
--- /dev/null
+++ b/samples/snippets/src/test/java/com/example/storage/bucket/DeleteBucketPubSubNotificationTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.example.storage.bucket;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
+
+import com.example.storage.TestBase;
+import com.google.cloud.pubsub.v1.TopicAdminClient;
+import com.google.cloud.storage.Notification;
+import com.google.cloud.storage.NotificationInfo;
+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.Map;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class DeleteBucketPubSubNotificationTest extends TestBase {
+
+  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 String PROJECT = System.getenv("GOOGLE_CLOUD_PROJECT");
+  private static final String TOPIC = String.format("projects/%s/topics/new-topic-delete", PROJECT);
+  private static TopicAdminClient topicAdminClient;
+  private static final NotificationInfo notificationInfo =
+      NotificationInfo.newBuilder(TOPIC)
+          .setCustomAttributes(CUSTOM_ATTRIBUTES)
+          .setPayloadFormat(PAYLOAD_FORMAT)
+          .build();
+
+  @BeforeClass
+  public static void configureTopicAdminClient() throws IOException {
+    if (PROJECT != null) {
+      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);
+    }
+  }
+
+  @AfterClass
+  public static void deleteTopicAndClient() {
+    /* Delete the Pub/Sub topic */
+    if (topicAdminClient != null) {
+      topicAdminClient.deleteTopic(TOPIC);
+      topicAdminClient.close();
+    }
+  }
+
+  @Test
+  public void testDeleteBucketPubSubNotification() {
+    // Check that we can access project value and that topic admin client came up successfully
+    assertNotNull("Unable to determine project", PROJECT);
+    assertNotNull("Topic Admin Client did not start up", topicAdminClient);
+
+    Notification notification = storage.createNotification(bucketName, notificationInfo);
+    com.example.storage.bucket.DeleteBucketPubSubNotification.deleteBucketPubSubNotification(
+        bucketName, notification.getNotificationId());
+    assertThat(stdOut.getCapturedOutputAsUtf8String())
+        .contains("Successfully deleted notification");
+  }
+
+  @Test
+  public void testNotificationNotFound() {
+    // Check that we can access project value and that topic admin client came up successfully
+    assertNotNull("Unable to determine project", PROJECT);
+    assertNotNull("Topic Admin Client did not start up", topicAdminClient);
+    Notification notification = storage.createNotification(bucketName, notificationInfo);
+    // Do a delete first.
+    storage.deleteNotification(bucketName, notification.getNotificationId());
+    // Now try to delete again.
+    com.example.storage.bucket.DeleteBucketPubSubNotification.deleteBucketPubSubNotification(
+        bucketName, notification.getNotificationId());
+    assertThat(stdOut.getCapturedOutputAsUtf8String()).contains("Failed to find notification");
+  }
+}
diff --git a/samples/snippets/src/test/java/com/example/storage/bucket/ListPubSubNotificationsTest.java b/samples/snippets/src/test/java/com/example/storage/bucket/ListPubSubNotificationsTest.java
new file mode 100644
index 00000000..c7dee854
--- /dev/null
+++ b/samples/snippets/src/test/java/com/example/storage/bucket/ListPubSubNotificationsTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.example.storage.bucket;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
+
+import com.example.storage.TestBase;
+import com.google.cloud.pubsub.v1.TopicAdminClient;
+import com.google.cloud.storage.Notification;
+import com.google.cloud.storage.NotificationInfo;
+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.Map;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class ListPubSubNotificationsTest extends TestBase {
+
+  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 String PROJECT = System.getenv("GOOGLE_CLOUD_PROJECT");
+  private static final String TOPIC = String.format("projects/%s/topics/new-topic-list", PROJECT);
+  private static TopicAdminClient topicAdminClient;
+
+  @BeforeClass
+  public static void configureTopicAdminClient() throws IOException {
+    if (PROJECT != null) {
+      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);
+    }
+  }
+
+  @AfterClass
+  public static void deleteTopicAndClient() {
+    /* Delete the Pub/Sub topic */
+    if (topicAdminClient != null) {
+      topicAdminClient.deleteTopic(TOPIC);
+      topicAdminClient.close();
+    }
+  }
+
+  @Test
+  public void testListBucketPubSubNotification() {
+    // Check that we can access project value and that topic admin client came up successfully
+    assertNotNull("Unable to determine project", PROJECT);
+    assertNotNull("Topic Admin Client did not start up", topicAdminClient);
+
+    NotificationInfo notificationInfo =
+        NotificationInfo.newBuilder(TOPIC)
+            .setCustomAttributes(CUSTOM_ATTRIBUTES)
+            .setPayloadFormat(PAYLOAD_FORMAT)
+            .build();
+    storage.createNotification(bucketName, notificationInfo);
+    ListPubSubNotifications.listPubSubNotifications(bucketName);
+    assertThat(stdOut.getCapturedOutputAsUtf8String()).contains(TOPIC);
+  }
+}
diff --git a/samples/snippets/src/test/java/com/example/storage/bucket/PrintPubSubNotificationTest.java b/samples/snippets/src/test/java/com/example/storage/bucket/PrintPubSubNotificationTest.java
new file mode 100644
index 00000000..05526675
--- /dev/null
+++ b/samples/snippets/src/test/java/com/example/storage/bucket/PrintPubSubNotificationTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.example.storage.bucket;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
+
+import com.example.storage.TestBase;
+import com.google.cloud.pubsub.v1.TopicAdminClient;
+import com.google.cloud.storage.Notification;
+import com.google.cloud.storage.NotificationInfo;
+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.Map;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class PrintPubSubNotificationTest extends TestBase {
+
+  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 String PROJECT = System.getenv("GOOGLE_CLOUD_PROJECT");
+  private static final String TOPIC = String.format("projects/%s/topics/new-topic-print", PROJECT);
+  private static TopicAdminClient topicAdminClient;
+
+  @BeforeClass
+  public static void configureTopicAdminClient() throws IOException {
+    if (PROJECT != null) {
+      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);
+    }
+  }
+
+  @AfterClass
+  public static void deleteTopicAndClient() {
+    /* Delete the Pub/Sub topic */
+    if (topicAdminClient != null) {
+      topicAdminClient.deleteTopic(TOPIC);
+      topicAdminClient.close();
+    }
+  }
+
+  @Test
+  public void testPrintBucketPubSubNotification() {
+    // Check that we can access project value and that topic admin client came up successfully
+    assertNotNull("Unable to determine project", PROJECT);
+    assertNotNull("Topic Admin Client did not start up", topicAdminClient);
+
+    NotificationInfo notificationInfo =
+        NotificationInfo.newBuilder(TOPIC)
+            .setCustomAttributes(CUSTOM_ATTRIBUTES)
+            .setPayloadFormat(PAYLOAD_FORMAT)
+            .build();
+    Notification notification = storage.createNotification(bucketName, notificationInfo);
+    PrintPubSubNotification.printPubSubNotification(bucketName, notification.getNotificationId());
+    assertThat(stdOut.getCapturedOutputAsUtf8String()).contains(TOPIC);
+  }
+}