diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java index 5b50a04e9..2fd275e8b 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java @@ -89,6 +89,11 @@ final class ApiaryConversions { // when converting from gRPC to apiary or vice-versa we want to preserve this property. Until // such a time as the apiary model has a project field, we manually apply it with this name. private static final String PROJECT_ID_FIELD_NAME = "x_project"; + // gRPC has a NotificationConfig.name property which contains the bucket the config is associated + // with which that apiary doesn't have yet. + // when converting from gRPC to apiary or vice-versa we want to preserve this property. Until + // such a time as the apiary model has a bucket field, we manually apply it with this name. + private static final String NOTIFICATION_BUCKET_FIELD_NAME = "x_bucket"; private final Codec entityCodec = Codec.of(this::entityEncode, this::entityDecode); @@ -774,6 +779,7 @@ private com.google.api.services.storage.model.Notification notificationEncode( to.setEtag(from.getEtag()); to.setSelfLink(from.getSelfLink()); to.setTopic(from.getTopic()); + ifNonNull(from.getBucket(), b -> to.set(NOTIFICATION_BUCKET_FIELD_NAME, b)); ifNonNull(from.getNotificationId(), to::setId); ifNonNull(from.getCustomAttributes(), to::setCustomAttributes); ifNonNull(from.getObjectNamePrefix(), to::setObjectNamePrefix); @@ -799,6 +805,7 @@ private com.google.api.services.storage.model.Notification notificationEncode( private NotificationInfo notificationDecode( com.google.api.services.storage.model.Notification from) { NotificationInfo.Builder builder = new NotificationInfo.BuilderImpl(from.getTopic()); + ifNonNull(from.get(NOTIFICATION_BUCKET_FIELD_NAME), String.class::cast, builder::setBucket); ifNonNull(from.getId(), builder::setNotificationId); ifNonNull(from.getEtag(), builder::setEtag); ifNonNull(from.getCustomAttributes(), builder::setCustomAttributes); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java index 69bd541db..38766b946 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java @@ -20,6 +20,7 @@ import static com.google.cloud.storage.Utils.ifNonNull; import static com.google.cloud.storage.Utils.lift; import static com.google.cloud.storage.Utils.projectNameCodec; +import static com.google.cloud.storage.Utils.topicNameCodec; import com.google.api.pathtemplate.PathTemplate; import com.google.cloud.Binding; @@ -35,6 +36,8 @@ import com.google.cloud.storage.BucketInfo.PublicAccessPrevention; import com.google.cloud.storage.Conversions.Codec; import com.google.cloud.storage.HmacKey.HmacKeyState; +import com.google.cloud.storage.NotificationInfo.EventType; +import com.google.cloud.storage.NotificationInfo.PayloadFormat; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -48,6 +51,8 @@ import com.google.storage.v2.BucketAccessControl; import com.google.storage.v2.CryptoKeyName; import com.google.storage.v2.HmacKeyMetadata; +import com.google.storage.v2.NotificationConfig; +import com.google.storage.v2.NotificationConfigName; import com.google.storage.v2.Object; import com.google.storage.v2.ObjectAccessControl; import com.google.storage.v2.ObjectChecksums; @@ -918,12 +923,54 @@ private BlobInfo blobInfoDecode(Object from) { return toBuilder.build(); } - private com.google.storage.v2.NotificationConfig notificationEncode(NotificationInfo from) { - return todo(); + private NotificationConfig notificationEncode(NotificationInfo from) { + NotificationConfig.Builder to = NotificationConfig.newBuilder(); + String id = from.getNotificationId(); + if (id != null) { + if (NotificationConfigName.isParsableFrom(id)) { + ifNonNull(id, to::setName); + } else { + NotificationConfigName name = NotificationConfigName.of("_", from.getBucket(), id); + to.setName(name.toString()); + } + } + ifNonNull(from.getTopic(), topicNameCodec::encode, to::setTopic); + ifNonNull(from.getEtag(), to::setEtag); + ifNonNull(from.getEventTypes(), toImmutableListOf(EventType::name), to::addAllEventTypes); + ifNonNull(from.getCustomAttributes(), to::putAllCustomAttributes); + ifNonNull(from.getObjectNamePrefix(), to::setObjectNamePrefix); + ifNonNull(from.getPayloadFormat(), PayloadFormat::name, to::setPayloadFormat); + return to.build(); } - private NotificationInfo notificationDecode(com.google.storage.v2.NotificationConfig from) { - return todo(); + private NotificationInfo notificationDecode(NotificationConfig from) { + NotificationInfo.Builder to = + NotificationInfo.newBuilder(topicNameCodec.decode(from.getTopic())); + if (!from.getName().isEmpty()) { + NotificationConfigName parse = NotificationConfigName.parse(from.getName()); + // the case where parse could return null is already guarded by the preceding isEmpty check + //noinspection DataFlowIssue + to.setNotificationId(parse.getNotificationConfig()); + to.setBucket(parse.getBucket()); + } + if (!from.getEtag().isEmpty()) { + to.setEtag(from.getEtag()); + } + if (!from.getEventTypesList().isEmpty()) { + EventType[] eventTypes = + from.getEventTypesList().stream().map(EventType::valueOf).toArray(EventType[]::new); + to.setEventTypes(eventTypes); + } + if (!from.getCustomAttributesMap().isEmpty()) { + to.setCustomAttributes(from.getCustomAttributesMap()); + } + if (!from.getObjectNamePrefix().isEmpty()) { + to.setObjectNamePrefix(from.getObjectNamePrefix()); + } + if (!from.getPayloadFormat().isEmpty()) { + to.setPayloadFormat(PayloadFormat.valueOf(from.getPayloadFormat())); + } + return to.build(); } private com.google.iam.v1.Policy policyEncode(Policy from) { diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java index dc332b2fd..ce5ebf043 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java @@ -30,7 +30,6 @@ import com.google.api.core.ApiFuture; import com.google.api.core.BetaApi; import com.google.api.gax.grpc.GrpcCallContext; -import com.google.api.gax.grpc.GrpcStatusCode; import com.google.api.gax.paging.AbstractPage; import com.google.api.gax.paging.Page; import com.google.api.gax.retrying.ResultRetryAlgorithm; @@ -39,7 +38,6 @@ import com.google.api.gax.rpc.NotFoundException; import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.UnaryCallable; -import com.google.api.gax.rpc.UnimplementedException; import com.google.cloud.BaseService; import com.google.cloud.Policy; import com.google.cloud.WriteChannel; @@ -84,18 +82,25 @@ import com.google.storage.v2.ComposeObjectRequest.SourceObject; import com.google.storage.v2.CreateBucketRequest; import com.google.storage.v2.CreateHmacKeyRequest; +import com.google.storage.v2.CreateNotificationConfigRequest; import com.google.storage.v2.DeleteBucketRequest; import com.google.storage.v2.DeleteHmacKeyRequest; +import com.google.storage.v2.DeleteNotificationConfigRequest; import com.google.storage.v2.DeleteObjectRequest; import com.google.storage.v2.GetBucketRequest; import com.google.storage.v2.GetHmacKeyRequest; +import com.google.storage.v2.GetNotificationConfigRequest; import com.google.storage.v2.GetObjectRequest; import com.google.storage.v2.GetServiceAccountRequest; import com.google.storage.v2.ListBucketsRequest; import com.google.storage.v2.ListHmacKeysRequest; +import com.google.storage.v2.ListNotificationConfigsRequest; +import com.google.storage.v2.ListNotificationConfigsResponse; import com.google.storage.v2.ListObjectsRequest; import com.google.storage.v2.ListObjectsResponse; import com.google.storage.v2.LockBucketRetentionPolicyRequest; +import com.google.storage.v2.NotificationConfig; +import com.google.storage.v2.NotificationConfigName; import com.google.storage.v2.Object; import com.google.storage.v2.ObjectAccessControl; import com.google.storage.v2.ObjectChecksums; @@ -103,13 +108,13 @@ import com.google.storage.v2.RewriteObjectRequest; import com.google.storage.v2.RewriteResponse; import com.google.storage.v2.StorageClient; +import com.google.storage.v2.StorageClient.ListNotificationConfigsPage; import com.google.storage.v2.UpdateBucketRequest; import com.google.storage.v2.UpdateHmacKeyRequest; import com.google.storage.v2.UpdateObjectRequest; import com.google.storage.v2.WriteObjectRequest; import com.google.storage.v2.WriteObjectResponse; import com.google.storage.v2.WriteObjectSpec; -import io.grpc.Status.Code; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -1404,23 +1409,92 @@ public ServiceAccount getServiceAccount(String projectId) { @Override public Notification createNotification(String bucket, NotificationInfo notificationInfo) { - return throwNotYetImplemented( - fmtMethodName("createNotification", String.class, NotificationInfo.class)); + NotificationConfig encode = codecs.notificationInfo().encode(notificationInfo); + CreateNotificationConfigRequest req = + CreateNotificationConfigRequest.newBuilder() + .setParent(bucketNameCodec.encode(bucket)) + .setNotificationConfig(encode) + .build(); + return Retrying.run( + getOptions(), + retryAlgorithmManager.getFor(req), + () -> storageClient.createNotificationConfigCallable().call(req), + syntaxDecoders.notificationConfig); } @Override public Notification getNotification(String bucket, String notificationId) { - return throwNotYetImplemented(fmtMethodName("getNotification", String.class, String.class)); + String name; + if (NotificationConfigName.isParsableFrom(notificationId)) { + name = notificationId; + } else { + NotificationConfigName configName = NotificationConfigName.of("_", bucket, notificationId); + name = configName.toString(); + } + GetNotificationConfigRequest req = + GetNotificationConfigRequest.newBuilder().setName(name).build(); + return Retrying.run( + getOptions(), + retryAlgorithmManager.getFor(req), + () -> { + try { + return storageClient.getNotificationConfigCallable().call(req); + } catch (NotFoundException e) { + return null; + } + }, + syntaxDecoders.notificationConfig); } @Override public List listNotifications(String bucket) { - return throwNotYetImplemented(fmtMethodName("listNotifications", String.class)); + ListNotificationConfigsRequest req = + ListNotificationConfigsRequest.newBuilder() + .setParent(bucketNameCodec.encode(bucket)) + .build(); + ResultRetryAlgorithm algorithm = retryAlgorithmManager.getFor(req); + return Retrying.run( + getOptions(), + algorithm, + () -> storageClient.listNotificationConfigsPagedCallable().call(req), + resp -> { + TransformingPageDecorator< + ListNotificationConfigsRequest, + ListNotificationConfigsResponse, + NotificationConfig, + ListNotificationConfigsPage, + Notification> + page = + new TransformingPageDecorator<>( + resp.getPage(), syntaxDecoders.notificationConfig, getOptions(), algorithm); + return ImmutableList.copyOf(page.iterateAll()); + }); } @Override public boolean deleteNotification(String bucket, String notificationId) { - return throwNotYetImplemented(fmtMethodName("deleteNotification", String.class, String.class)); + String name; + if (NotificationConfigName.isParsableFrom(notificationId)) { + name = notificationId; + } else { + NotificationConfigName configName = NotificationConfigName.of("_", bucket, notificationId); + name = configName.toString(); + } + DeleteNotificationConfigRequest req = + DeleteNotificationConfigRequest.newBuilder().setName(name).build(); + return Boolean.TRUE.equals( + Retrying.run( + getOptions(), + retryAlgorithmManager.getFor(req), + () -> { + try { + storageClient.deleteNotificationConfigCallable().call(req); + return true; + } catch (NotFoundException e) { + return false; + } + }, + Decoder.identity())); } @Override @@ -1448,6 +1522,8 @@ private final class SyntaxDecoders { o -> codecs.blobInfo().decode(o).asBlob(GrpcStorageImpl.this); final Decoder bucket = b -> codecs.bucketInfo().decode(b).asBucket(GrpcStorageImpl.this); + final Decoder notificationConfig = + n -> codecs.notificationInfo().decode(n).asNotification(GrpcStorageImpl.this); } /** @@ -1668,14 +1744,6 @@ static T throwHttpJsonOnly(Class clazz, String methodName) { throw new UnsupportedOperationException(message); } - static T throwNotYetImplemented(String methodName) { - String message = - String.format( - "%s#%s is not yet implemented for GRPC transport. Please use StorageOptions.http() to construct a compatible instance in the interim.", - Storage.class.getName(), methodName); - throw new UnimplementedException(message, null, GrpcStatusCode.of(Code.UNIMPLEMENTED), false); - } - private static String fmtMethodName(String name, Class... args) { return name + "(" diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java index 0652620a1..f3e2bb81a 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java @@ -88,6 +88,12 @@ public Builder setCustomAttributes(Map customAttributes) { return this; } + @Override + Builder setBucket(String bucket) { + infoBuilder.setBucket(bucket); + return this; + } + @Override public Notification build() { return new Notification(storage, infoBuilder); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/NotificationInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/NotificationInfo.java index 58621577d..7bf147790 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/NotificationInfo.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/NotificationInfo.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.api.core.InternalApi; import com.google.api.pathtemplate.PathTemplate; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; @@ -34,11 +35,13 @@ public class NotificationInfo implements Serializable { private static final PathTemplate PATH_TEMPLATE = PathTemplate.createWithoutUrlEncoding("projects/{project}/topics/{topic}"); + // TODO: Change to StringEnum in next major version public enum PayloadFormat { JSON_API_V1, NONE } + // TODO: Change to StringEnum in next major version public enum EventType { OBJECT_FINALIZE, OBJECT_METADATA_UPDATE, @@ -54,6 +57,7 @@ public enum EventType { private final String objectNamePrefix; private final String etag; private final String selfLink; + private final String bucket; /** Builder for {@code NotificationInfo}. */ public abstract static class Builder { @@ -75,6 +79,8 @@ public abstract static class Builder { public abstract Builder setCustomAttributes(Map customAttributes); + abstract Builder setBucket(String bucket); + /** Creates a {@code NotificationInfo} object. */ public abstract NotificationInfo build(); } @@ -90,6 +96,7 @@ public static class BuilderImpl extends Builder { private String objectNamePrefix; private String etag; private String selfLink; + private String bucket; BuilderImpl(String topic) { this.topic = topic; @@ -104,6 +111,7 @@ public static class BuilderImpl extends Builder { customAttributes = notificationInfo.customAttributes; payloadFormat = notificationInfo.payloadFormat; objectNamePrefix = notificationInfo.objectNamePrefix; + bucket = notificationInfo.bucket; } @Override @@ -156,6 +164,12 @@ public Builder setCustomAttributes(Map customAttributes) { return this; } + @Override + Builder setBucket(String bucket) { + this.bucket = bucket; + return this; + } + public NotificationInfo build() { checkNotNull(topic); checkTopicFormat(topic); @@ -172,6 +186,7 @@ public NotificationInfo build() { customAttributes = builder.customAttributes; payloadFormat = builder.payloadFormat; objectNamePrefix = builder.objectNamePrefix; + bucket = builder.bucket; } /** Returns the service-generated id for the notification. */ @@ -225,6 +240,15 @@ public Map getCustomAttributes() { return customAttributes; } + /** + * gRPC has the bucket name encoded in the notification name, use this internal property to track + * it. + */ + @InternalApi + String getBucket() { + return bucket; + } + @Override public int hashCode() { return Objects.hash( diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java index 0cd1ff07b..d91603937 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java @@ -4053,7 +4053,7 @@ List testIamPermissions( * @return the created notification * @throws StorageException upon failure */ - @TransportCompatibility({Transport.HTTP}) + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) Notification createNotification(String bucket, NotificationInfo notificationInfo); /** @@ -4072,7 +4072,7 @@ List testIamPermissions( * @return the {@code Notification} object with the given id or {@code null} if not found * @throws StorageException upon failure */ - @TransportCompatibility({Transport.HTTP}) + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) Notification getNotification(String bucket, String notificationId); /** @@ -4089,7 +4089,7 @@ List testIamPermissions( * @return a list of {@link Notification} objects added to the bucket. * @throws StorageException upon failure */ - @TransportCompatibility({Transport.HTTP}) + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) List listNotifications(String bucket); /** @@ -4113,7 +4113,7 @@ List testIamPermissions( * @return {@code true} if the notification has been deleted, {@code false} if not found * @throws StorageException upon failure */ - @TransportCompatibility({Transport.HTTP}) + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) boolean deleteNotification(String bucket, String notificationId); /** diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java index 0e67c8453..81d7fbb92 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java @@ -152,6 +152,26 @@ final class Utils { } }); + private static final String PUBSUB_PREFIX = "//pubsub.googleapis.com/"; + static final Codec topicNameCodec = + Codec.of( + topic -> { + requireNonNull(topic, "topic must be non null"); + if (topic.startsWith(PUBSUB_PREFIX)) { + return topic; + } else { + return PUBSUB_PREFIX + topic; + } + }, + resourceName -> { + requireNonNull(resourceName, "resourceName must be non null"); + if (resourceName.startsWith(PUBSUB_PREFIX)) { + return resourceName.substring(PUBSUB_PREFIX.length()); + } else { + return resourceName; + } + }); + static final Codec crc32cCodec = Codec.of(Utils::crc32cEncode, Utils::crc32cDecode); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationInfoPropertyTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationInfoPropertyTest.java new file mode 100644 index 000000000..5a20dfc60 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationInfoPropertyTest.java @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.storage; + +import com.google.storage.v2.NotificationConfig; + +final class NotificationInfoPropertyTest + extends BaseConvertablePropertyTest< + NotificationInfo, NotificationConfig, com.google.api.services.storage.model.Notification> {} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITNotificationTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITNotificationTest.java index 9aa528c1a..69a066580 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITNotificationTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITNotificationTest.java @@ -16,18 +16,21 @@ package com.google.cloud.storage.it; +import static com.google.cloud.storage.TestUtils.assertAll; import static com.google.common.truth.Truth.assertThat; import com.google.cloud.pubsub.v1.TopicAdminClient; import com.google.cloud.storage.BucketInfo; import com.google.cloud.storage.Notification; import com.google.cloud.storage.NotificationInfo; +import com.google.cloud.storage.NotificationInfo.PayloadFormat; import com.google.cloud.storage.Storage; import com.google.cloud.storage.TransportCompatibility.Transport; 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.registry.Generator; import com.google.common.collect.ImmutableMap; import com.google.iam.v1.Binding; import com.google.iam.v1.GetIamPolicyRequest; @@ -35,7 +38,6 @@ import java.io.IOException; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import org.junit.After; @@ -45,44 +47,58 @@ @RunWith(StorageITRunner.class) @CrossRun( - transports = {Transport.HTTP}, - backends = {Backend.PROD}) + transports = {Transport.HTTP, Transport.GRPC}, + backends = {Backend.PROD, Backend.TEST_BENCH}) public class ITNotificationTest { - private static final Notification.PayloadFormat PAYLOAD_FORMAT = - Notification.PayloadFormat.JSON_API_V1.JSON_API_V1; + private static final Notification.PayloadFormat PAYLOAD_FORMAT = PayloadFormat.JSON_API_V1; private static final Map CUSTOM_ATTRIBUTES = ImmutableMap.of("label1", "value1"); private static final Logger log = Logger.getLogger(ITNotificationTest.class.getName()); + private static final String DOES_NOT_EXIST_ID = "something-that-does-not-exist-probably"; + @Inject public Backend backend; + @Inject public Transport transport; @Inject public Storage storage; @Inject public BucketInfo bucket; + @Inject public Generator generator; private TopicAdminClient topicAdminClient; private String topic; + private NotificationInfo notificationInfo; @Before public void setup() throws IOException { - // Configure topic admin client for notification. - topicAdminClient = TopicAdminClient.create(); String projectId = storage.getOptions().getProjectId(); - String id = UUID.randomUUID().toString().substring(0, 8); - topic = String.format("projects/%s/topics/test_topic_foo_%s", projectId, id).trim(); + // square brackets are not acceptable characters for topic names, replace them with dash + // https://cloud.google.com/pubsub/docs/admin#resource_names + String name = generator.randomObjectName().replaceAll("[\\[\\]]", "-"); + topic = String.format("projects/%s/topics/%s", projectId, name).trim(); + notificationInfo = + NotificationInfo.newBuilder(topic) + .setCustomAttributes(CUSTOM_ATTRIBUTES) + .setPayloadFormat(PAYLOAD_FORMAT) + .build(); - topicAdminClient.createTopic(this.topic); + if (backend == Backend.PROD && transport == Transport.HTTP) { - GetIamPolicyRequest getIamPolicyRequest = - GetIamPolicyRequest.newBuilder().setResource(this.topic).build(); + // Configure topic admin client for notification. + topicAdminClient = TopicAdminClient.create(); + topicAdminClient.createTopic(this.topic); - com.google.iam.v1.Policy policy = topicAdminClient.getIamPolicy(getIamPolicyRequest); + GetIamPolicyRequest getIamPolicyRequest = + GetIamPolicyRequest.newBuilder().setResource(this.topic).build(); - Binding binding = - Binding.newBuilder().setRole("roles/owner").addMembers("allAuthenticatedUsers").build(); + com.google.iam.v1.Policy policy = topicAdminClient.getIamPolicy(getIamPolicyRequest); - SetIamPolicyRequest setIamPolicyRequest = - SetIamPolicyRequest.newBuilder() - .setResource(this.topic) - .setPolicy(policy.toBuilder().addBindings(binding).build()) - .build(); - topicAdminClient.setIamPolicy(setIamPolicyRequest); + Binding binding = + Binding.newBuilder().setRole("roles/owner").addMembers("allAuthenticatedUsers").build(); + + SetIamPolicyRequest setIamPolicyRequest = + SetIamPolicyRequest.newBuilder() + .setResource(this.topic) + .setPolicy(policy.toBuilder().addBindings(binding).build()) + .build(); + topicAdminClient.setIamPolicy(setIamPolicyRequest); + } } @After @@ -100,49 +116,80 @@ public void cleanup() { } @Test - public void testNotification() { - NotificationInfo notificationInfo = - NotificationInfo.newBuilder(topic) - .setCustomAttributes(CUSTOM_ATTRIBUTES) - .setPayloadFormat(PAYLOAD_FORMAT) - .build(); - try { - assertThat(storage.listNotifications(bucket.getName())).isEmpty(); - Notification notification = storage.createNotification(bucket.getName(), notificationInfo); - assertThat(notification.getNotificationId()).isNotNull(); - assertThat(CUSTOM_ATTRIBUTES).isEqualTo(notification.getCustomAttributes()); - assertThat(PAYLOAD_FORMAT.name()).isEqualTo(notification.getPayloadFormat().name()); - assertThat(notification.getTopic().contains(topic)).isTrue(); - - // Gets the notification with the specified id. - Notification actualNotification = - storage.getNotification(bucket.getName(), notification.getNotificationId()); - assertThat(actualNotification.getNotificationId()) - .isEqualTo(notification.getNotificationId()); - assertThat(actualNotification.getTopic().trim()).isEqualTo(notification.getTopic().trim()); - assertThat(actualNotification.getEtag()).isEqualTo(notification.getEtag()); - assertThat(actualNotification.getEventTypes()).isEqualTo(notification.getEventTypes()); - assertThat(actualNotification.getPayloadFormat()).isEqualTo(notification.getPayloadFormat()); - assertThat(actualNotification.getSelfLink()).isEqualTo(notification.getSelfLink()); - assertThat(actualNotification.getCustomAttributes()) - .isEqualTo(notification.getCustomAttributes()); - - // Retrieves the list of notifications associated with the bucket. - List notifications = storage.listNotifications(bucket.getName()); - assertThat(notifications.size()).isEqualTo(1); - assertThat(notifications.get(0).getNotificationId()) - .isEqualTo(actualNotification.getNotificationId()); - - // Deletes the notification with the specified id. - assertThat(storage.deleteNotification(bucket.getName(), notification.getNotificationId())) - .isTrue(); - assertThat(storage.deleteNotification(bucket.getName(), notification.getNotificationId())) - .isFalse(); - assertThat(storage.getNotification(bucket.getName(), notification.getNotificationId())) - .isNull(); - assertThat(storage.listNotifications(bucket.getName())).isEmpty(); - } finally { - // delete is taken care of by BucketFixture now + @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD) + public void listNotification_doesNotExist() throws Exception { + // create a temporary bucket to ensure we're immune from ordering on other tests + try (TemporaryBucket tempB = + TemporaryBucket.newBuilder() + .setBucketInfo(BucketInfo.of(generator.randomBucketName())) + .setStorage(storage) + .build()) { + List notifications = storage.listNotifications(tempB.getBucket().getName()); + assertThat(notifications).isEmpty(); } } + + @Test + @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD) + public void listNotification_exists() { + Notification notification = storage.createNotification(bucket.getName(), notificationInfo); + List notifications = storage.listNotifications(bucket.getName()); + assertThat(notifications).isNotEmpty(); + assertThat(notifications).contains(notification); + } + + @Test + @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD) + public void createNotification_doesNotExist() throws Exception { + Notification notification = storage.createNotification(bucket.getName(), notificationInfo); + assertAll( + () -> assertThat(notification.getNotificationId()).isNotNull(), + () -> assertThat(notification.getCustomAttributes()).isEqualTo(CUSTOM_ATTRIBUTES), + () -> assertThat(notification.getPayloadFormat()).isEqualTo(PAYLOAD_FORMAT), + () -> assertThat(notification.getTopic()).contains(topic)); + } + + @Test + @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD) + public void getNotification_exists() throws Exception { + Notification notification = storage.createNotification(bucket.getName(), notificationInfo); + + Notification getResult = + storage.getNotification(bucket.getName(), notification.getNotificationId()); + + assertAll( + () -> assertThat(getResult.getNotificationId()).isEqualTo(notification.getNotificationId()), + () -> assertThat(getResult.getTopic()).isEqualTo(notification.getTopic()), + () -> assertThat(getResult.getEtag()).isEqualTo(notification.getEtag()), + () -> assertThat(getResult.getEventTypes()).isEqualTo(notification.getEventTypes()), + () -> assertThat(getResult.getPayloadFormat()).isEqualTo(notification.getPayloadFormat()), + () -> + assertThat(getResult.getCustomAttributes()) + .isEqualTo(notification.getCustomAttributes()), + () -> assertThat(getResult).isEqualTo(notification)); + } + + @Test + @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD) + public void getNotification_doesNotExists() { + Notification getResult = storage.getNotification(bucket.getName(), DOES_NOT_EXIST_ID); + + assertThat(getResult).isNull(); + } + + @Test + @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD) + public void deleteNotification_exists() { + Notification notification = storage.createNotification(bucket.getName(), notificationInfo); + boolean deleteResult = + storage.deleteNotification(bucket.getName(), notification.getNotificationId()); + assertThat(deleteResult).isTrue(); + } + + @Test + @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD) + public void deleteNotification_doesNotExists() { + boolean deleteResult = storage.deleteNotification(bucket.getName(), DOES_NOT_EXIST_ID); + assertThat(deleteResult).isFalse(); + } } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/NotificationArbitraryProvider.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/NotificationArbitraryProvider.java new file mode 100644 index 000000000..8e960791c --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/NotificationArbitraryProvider.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023 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.jqwik; + +import static com.google.cloud.storage.PackagePrivateMethodWorkarounds.ifNonNull; + +import com.google.pubsub.v1.TopicName; +import com.google.storage.v2.NotificationConfig; +import com.google.storage.v2.NotificationConfigName; +import java.util.Collections; +import java.util.Set; +import net.jqwik.api.Arbitraries; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.Combinators; +import net.jqwik.api.providers.ArbitraryProvider; +import net.jqwik.api.providers.TypeUsage; +import org.checkerframework.checker.nullness.qual.NonNull; + +public final class NotificationArbitraryProvider implements ArbitraryProvider { + + @Override + public boolean canProvideFor(TypeUsage targetType) { + return targetType.isOfType(NotificationConfig.class); + } + + @NonNull + @Override + public Set> provideFor( + @NonNull TypeUsage targetType, @NonNull SubtypeProvider subtypeProvider) { + Arbitrary as = + Combinators.combine( + notificationName(), + topic(), + StorageArbitraries.etag().injectNull(0.5), + eventTypes(), + StorageArbitraries.buckets().labels(), + StorageArbitraries.objects().name().injectNull(0.5), + Arbitraries.of("JSON_API_V1", "NONE")) + .as( + (name, topic, etag, types, customAttributes, prefix, payloadFormat) -> { + NotificationConfig.Builder b = + NotificationConfig.newBuilder() + .setName(name.toString()) + .setTopic(topic) + .setPayloadFormat(payloadFormat); + ifNonNull(types, b::addAllEventTypes); + ifNonNull(customAttributes, b::putAllCustomAttributes); + ifNonNull(etag, b::setEtag); + ifNonNull(prefix, b::setObjectNamePrefix); + return b.build(); + }); + return Collections.singleton(as); + } + + private static Arbitrary> eventTypes() { + return Arbitraries.of( + "OBJECT_FINALIZE", "OBJECT_METADATA_UPDATE", "OBJECT_DELETE", "OBJECT_ARCHIVE") + .set() + .ofMinSize(0) + .ofMaxSize(4); + } + + @NonNull + private static Arbitrary topic() { + return Combinators.combine( + StorageArbitraries.projectID(), + StorageArbitraries.alphaString().ofMinLength(1).ofMaxLength(10)) + .as((p, t) -> TopicName.of(p.get(), t)) + .map(tn -> "//pubsub.googleapis.com/" + tn.toString()); + } + + @NonNull + private static Arbitrary notificationName() { + return Combinators.combine( + StorageArbitraries.buckets().name(), StorageArbitraries.alphaString().ofMinLength(1)) + .as( + (bucket, notification) -> + NotificationConfigName.of(bucket.getProject(), bucket.getBucket(), notification)); + } +} diff --git a/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider b/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider index 2014e4acf..2fcbe230f 100644 --- a/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider +++ b/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider @@ -19,3 +19,4 @@ com.google.cloud.storage.jqwik.BucketArbitraryProvider com.google.cloud.storage.jqwik.HmacKeyMetadataArbitraryProvider com.google.cloud.storage.jqwik.ServiceAccountArbitraryProvider com.google.cloud.storage.jqwik.IamPolicyArbitraryProvider +com.google.cloud.storage.jqwik.NotificationArbitraryProvider