diff --git a/aws-storage-s3/api/aws-storage-s3.api b/aws-storage-s3/api/aws-storage-s3.api index d2766e227..96aa6229b 100644 --- a/aws-storage-s3/api/aws-storage-s3.api +++ b/aws-storage-s3/api/aws-storage-s3.api @@ -176,6 +176,7 @@ public final class com/amplifyframework/storage/s3/options/AWSS3StorageGetPresig public static fun defaultInstance ()Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions; public fun equals (Ljava/lang/Object;)Z public static fun from (Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions;)Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions$Builder; + public fun getValidateObjectExistence ()Z public fun hashCode ()I public fun toString ()Ljava/lang/String; public fun useAccelerateEndpoint ()Z @@ -187,6 +188,7 @@ public final class com/amplifyframework/storage/s3/options/AWSS3StorageGetPresig public synthetic fun build ()Lcom/amplifyframework/storage/options/StorageOptions; public fun build ()Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions; public fun setUseAccelerateEndpoint (Z)Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions$Builder; + public fun setValidateObjectExistence (Z)Lcom/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions$Builder; } public final class com/amplifyframework/storage/s3/options/AWSS3StorageListOptions : com/amplifyframework/storage/options/StorageListOptions { @@ -283,11 +285,13 @@ public final class com/amplifyframework/storage/s3/request/AWSS3StorageDownloadF public final class com/amplifyframework/storage/s3/request/AWSS3StorageGetPresignedUrlRequest { public fun (Ljava/lang/String;Lcom/amplifyframework/storage/StorageAccessLevel;Ljava/lang/String;IZ)V + public fun (Ljava/lang/String;Lcom/amplifyframework/storage/StorageAccessLevel;Ljava/lang/String;IZZ)V public fun getAccessLevel ()Lcom/amplifyframework/storage/StorageAccessLevel; public fun getExpires ()I public fun getKey ()Ljava/lang/String; public fun getTargetIdentityId ()Ljava/lang/String; public fun useAccelerateEndpoint ()Z + public fun validateObjectExistence ()Z } public final class com/amplifyframework/storage/s3/request/AWSS3StorageListRequest { @@ -331,6 +335,7 @@ public abstract interface class com/amplifyframework/storage/s3/service/StorageS public abstract fun resumeTransfer (Lcom/amplifyframework/storage/s3/transfer/TransferObserver;)V public abstract fun uploadFile (Ljava/lang/String;Ljava/lang/String;Ljava/io/File;Lcom/amplifyframework/storage/ObjectMetadata;Z)Lcom/amplifyframework/storage/s3/transfer/TransferObserver; public abstract fun uploadInputStream (Ljava/lang/String;Ljava/lang/String;Ljava/io/InputStream;Lcom/amplifyframework/storage/ObjectMetadata;Z)Lcom/amplifyframework/storage/s3/transfer/TransferObserver; + public abstract fun validateObjectExists (Ljava/lang/String;)V } public abstract interface class com/amplifyframework/storage/s3/service/StorageService$Factory { diff --git a/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StoragePathGetUrlTest.kt b/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StoragePathGetUrlTest.kt index 4fc454a1e..ab250b466 100644 --- a/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StoragePathGetUrlTest.kt +++ b/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StoragePathGetUrlTest.kt @@ -16,11 +16,14 @@ package com.amplifyframework.storage.s3 import android.content.Context import androidx.test.core.app.ApplicationProvider +import aws.sdk.kotlin.services.s3.model.NotFound import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin import com.amplifyframework.storage.StorageCategory +import com.amplifyframework.storage.StorageException import com.amplifyframework.storage.StoragePath import com.amplifyframework.storage.options.StorageGetUrlOptions import com.amplifyframework.storage.options.StorageUploadFileOptions +import com.amplifyframework.storage.s3.options.AWSS3StorageGetPresignedUrlOptions import com.amplifyframework.storage.s3.test.R import com.amplifyframework.storage.s3.util.WorkmanagerTestUtils.initializeWorkmanagerTestUtil import com.amplifyframework.testutils.random.RandomTempFile @@ -28,6 +31,7 @@ import com.amplifyframework.testutils.sync.SynchronousAuth import com.amplifyframework.testutils.sync.SynchronousStorage import java.io.File import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.BeforeClass import org.junit.Test @@ -79,4 +83,42 @@ class AWSS3StoragePathGetUrlTest { assertEquals("/public/$SMALL_FILE_NAME", result.url.path) assertTrue(result.url.query.contains("X-Amz-Expires=30")) } + + @Test + fun testGetUrlWithObjectExistenceValidationEnabled() { + val result = synchronousStorage.getUrl( + SMALL_FILE_PATH, + AWSS3StorageGetPresignedUrlOptions.builder().setValidateObjectExistence(true).expires(30).build() + ) + + assertEquals("/public/$SMALL_FILE_NAME", result.url.path) + assertTrue(result.url.query.contains("X-Amz-Expires=30")) + } + + @Test + fun testGetUrlWithStorageExceptionObjectNotFoundThrown() { + val exception = assertThrows(StorageException::class.java) { + synchronousStorage.getUrl( + StoragePath.fromString("public/SOME_UNKNOWN_FILE"), + AWSS3StorageGetPresignedUrlOptions.builder().setValidateObjectExistence(true).expires(30).build() + ) + } + + assertTrue(exception.cause is NotFound) + } + + @Test + fun testGetUrlWithObjectExistenceValidationDisabledForNonExistentObject() { + val result = synchronousStorage.getUrl( + StoragePath.fromString("public/SOME_UNKNOWN_FILE"), + AWSS3StorageGetPresignedUrlOptions.builder().setValidateObjectExistence(false).expires(30).build() + ) + + assertEquals("/public/SOME_UNKNOWN_FILE", result.url.path) + assertTrue(result.url.query.contains("X-Amz-Expires=30")) + + assertThrows(java.io.FileNotFoundException::class.java) { + result.url.readBytes() + } + } } diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/AWSS3StoragePlugin.java b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/AWSS3StoragePlugin.java index 4af9a374e..e12f047d1 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/AWSS3StoragePlugin.java +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/AWSS3StoragePlugin.java @@ -319,6 +319,8 @@ public StorageGetUrlOperation getUrl( @NonNull Consumer onError) { boolean useAccelerateEndpoint = options instanceof AWSS3StorageGetPresignedUrlOptions && ((AWSS3StorageGetPresignedUrlOptions) options).useAccelerateEndpoint(); + boolean validateObjectExistence = options instanceof AWSS3StorageGetPresignedUrlOptions && + ((AWSS3StorageGetPresignedUrlOptions) options).getValidateObjectExistence(); AWSS3StorageGetPresignedUrlRequest request = new AWSS3StorageGetPresignedUrlRequest( key, options.getAccessLevel() != null @@ -328,7 +330,8 @@ public StorageGetUrlOperation getUrl( options.getExpires() != 0 ? options.getExpires() : defaultUrlExpiration, - useAccelerateEndpoint + useAccelerateEndpoint, + validateObjectExistence ); AWSS3StorageGetPresignedUrlOperation operation = @@ -355,10 +358,15 @@ public StorageGetUrlOperation getUrl( ) { boolean useAccelerateEndpoint = options instanceof AWSS3StorageGetPresignedUrlOptions && ((AWSS3StorageGetPresignedUrlOptions) options).useAccelerateEndpoint(); + + boolean validateObjectExistence = options instanceof AWSS3StorageGetPresignedUrlOptions && + ((AWSS3StorageGetPresignedUrlOptions) options).getValidateObjectExistence(); + AWSS3StoragePathGetPresignedUrlRequest request = new AWSS3StoragePathGetPresignedUrlRequest( path, options.getExpires() != 0 ? options.getExpires() : defaultUrlExpiration, - useAccelerateEndpoint + useAccelerateEndpoint, + validateObjectExistence ); AWSS3StoragePathGetPresignedUrlOperation operation = diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperation.java b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperation.java index 1d3f4eab4..295413985 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperation.java +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperation.java @@ -31,13 +31,14 @@ import java.util.concurrent.ExecutorService; /** - * An operation to retrieve pre-signed object URL from AWS S3. - * @deprecated Class should not be public and explicitly cast to. Cast to StorageGetUrlOperation. - * Internal usages are moving to AWSS3StoragePathGetPresignedUrlOperation + * An operation to retrieve pre-signed object URL from AWS S3. + * + * @deprecated Class should not be public and explicitly cast to. Cast to StorageGetUrlOperation. + * Internal usages are moving to AWSS3StoragePathGetPresignedUrlOperation */ @Deprecated public final class AWSS3StorageGetPresignedUrlOperation - extends StorageGetUrlOperation { + extends StorageGetUrlOperation { private final StorageService storageService; private final ExecutorService executorService; private final AuthCredentialsProvider authCredentialsProvider; @@ -58,13 +59,13 @@ public final class AWSS3StorageGetPresignedUrlOperation * @param onError Notified upon URL generation error */ public AWSS3StorageGetPresignedUrlOperation( - @NonNull StorageService storageService, - @NonNull ExecutorService executorService, - @NonNull AuthCredentialsProvider authCredentialsProvider, - @NonNull AWSS3StorageGetPresignedUrlRequest request, - @NonNull AWSS3StoragePluginConfiguration awss3StoragePluginConfiguration, - @NonNull Consumer onSuccess, - @NonNull Consumer onError + @NonNull StorageService storageService, + @NonNull ExecutorService executorService, + @NonNull AuthCredentialsProvider authCredentialsProvider, + @NonNull AWSS3StorageGetPresignedUrlRequest request, + @NonNull AWSS3StoragePluginConfiguration awss3StoragePluginConfiguration, + @NonNull Consumer onSuccess, + @NonNull Consumer onError ) { super(request); this.storageService = storageService; @@ -79,16 +80,26 @@ public AWSS3StorageGetPresignedUrlOperation( @Override public void start() { executorService.submit(() -> { - awsS3StoragePluginConfiguration.getAWSS3PluginPrefixResolver(authCredentialsProvider). + awsS3StoragePluginConfiguration.getAWSS3PluginPrefixResolver(authCredentialsProvider). resolvePrefix(getRequest().getAccessLevel(), getRequest().getTargetIdentityId(), prefix -> { try { String serviceKey = prefix.concat(getRequest().getKey()); + + if (getRequest().validateObjectExistence()) { + try { + storageService.validateObjectExists(serviceKey); + } catch (StorageException exception) { + onError.accept(exception); + return; + } + } + URL url = storageService.getPresignedUrl( - serviceKey, - getRequest().getExpires(), - getRequest().useAccelerateEndpoint()); + serviceKey, + getRequest().getExpires(), + getRequest().useAccelerateEndpoint()); onSuccess.accept(StorageGetUrlResult.fromUrl(url)); } catch (Exception exception) { onError.accept(new StorageException( diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathGetPresignedUrlOperation.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathGetPresignedUrlOperation.kt index 3faacef71..63862ca3f 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathGetPresignedUrlOperation.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathGetPresignedUrlOperation.kt @@ -48,6 +48,24 @@ internal class AWSS3StoragePathGetPresignedUrlOperation( return@submit } + if (request.validateObjectExistence) { + try { + storageService.validateObjectExists(serviceKey) + } catch (se: StorageException) { + onError.accept(se) + return@submit + } catch (exception: Exception) { + onError.accept( + StorageException( + "Encountered an issue while validating the existence of object", + exception, + "See included exception for more details and suggestions to fix." + ) + ) + return@submit + } + } + try { val url = storageService.getPresignedUrl( serviceKey, diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions.java b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions.java index 36b7ae7fc..5c204235f 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions.java +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/options/AWSS3StorageGetPresignedUrlOptions.java @@ -26,10 +26,12 @@ */ public final class AWSS3StorageGetPresignedUrlOptions extends StorageGetUrlOptions { private final boolean useAccelerationMode; + private final boolean validateObjectExistence; private AWSS3StorageGetPresignedUrlOptions(final Builder builder) { super(builder); this.useAccelerationMode = builder.useAccelerateEndpoint; + this.validateObjectExistence = builder.validateObjectExistence; } /** @@ -59,6 +61,8 @@ public static Builder from(@NonNull AWSS3StorageGetPresignedUrlOptions options) return builder() .accessLevel(options.getAccessLevel()) .targetIdentityId(options.getTargetIdentityId()) + .expires(options.getExpires()) + .setValidateObjectExistence(options.getValidateObjectExistence()) .expires(options.getExpires()); } @@ -80,6 +84,16 @@ public boolean useAccelerateEndpoint() { return useAccelerationMode; } + /** + * Gets the flag to determine whether to validate whether an S3 object exists. + * Note: Setting this to `true` will result in a latency cost since confirming the existence + * of the underlying S3 object will likely require a round-trip network call. + * @return boolean flag + */ + public boolean getValidateObjectExistence() { + return validateObjectExistence; + } + @Override @SuppressWarnings("deprecation") public boolean equals(Object obj) { @@ -91,7 +105,8 @@ public boolean equals(Object obj) { AWSS3StorageGetPresignedUrlOptions that = (AWSS3StorageGetPresignedUrlOptions) obj; return ObjectsCompat.equals(getAccessLevel(), that.getAccessLevel()) && ObjectsCompat.equals(getTargetIdentityId(), that.getTargetIdentityId()) && - ObjectsCompat.equals(getExpires(), that.getExpires()); + ObjectsCompat.equals(getExpires(), that.getExpires()) && + ObjectsCompat.equals(getValidateObjectExistence(), that.getValidateObjectExistence()); } } @@ -101,7 +116,8 @@ public int hashCode() { return ObjectsCompat.hash( getAccessLevel(), getTargetIdentityId(), - getExpires() + getExpires(), + getValidateObjectExistence() ); } @@ -113,6 +129,7 @@ public String toString() { "accessLevel=" + getAccessLevel() + ", targetIdentityId=" + getTargetIdentityId() + ", expires=" + getExpires() + + ", validateObjectExistence=" + getValidateObjectExistence() + '}'; } @@ -123,6 +140,7 @@ public String toString() { */ public static final class Builder extends StorageGetUrlOptions.Builder { private boolean useAccelerateEndpoint; + private boolean validateObjectExistence; /** * Configure to use acceleration mode on new StorageGetPresignedUrlOptions instances. @@ -134,6 +152,16 @@ public Builder setUseAccelerateEndpoint(boolean useAccelerateEndpoint) { return this; } + /** + * Configure to validate object existence flag on new StorageGetPresignedUrlOptions instances. + * @param validateObjectExistence boolean flag to represent flag to validate object existence. + * @return Current Builder instance for fluent chaining + */ + public Builder setValidateObjectExistence(boolean validateObjectExistence) { + this.validateObjectExistence = validateObjectExistence; + return this; + } + @Override @NonNull public AWSS3StorageGetPresignedUrlOptions build() { diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StorageGetPresignedUrlRequest.java b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StorageGetPresignedUrlRequest.java index 087f076c1..a193eb554 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StorageGetPresignedUrlRequest.java +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StorageGetPresignedUrlRequest.java @@ -33,6 +33,7 @@ public final class AWSS3StorageGetPresignedUrlRequest { private final String targetIdentityId; private final int expires; private final boolean useAccelerateEndpoint; + private final boolean validateObjectExistence; /** * Constructs a new AWSS3StorageGetUrlRequest. @@ -58,6 +59,37 @@ public AWSS3StorageGetPresignedUrlRequest( this.targetIdentityId = targetIdentityId; this.expires = expires; this.useAccelerateEndpoint = useAccelerateEndpoint; + this.validateObjectExistence = false; + } + + /** + * Constructs a new AWSS3StorageGetUrlRequest. + * Although this has public access, it is intended for internal use and should not be used directly by host + * applications. The behavior of this may change without warning. + * + * @param key key for item to obtain URL for + * @param accessLevel Storage access level + * @param targetIdentityId If set, this should override the current user's identity ID. + * If null, the operation will fetch the current identity ID. + * @param expires The number of seconds before the URL expires + * @param useAccelerateEndpoint Flag to enable acceleration mode + * @param validateObjectExistence Flag to validate if object exists in storage + */ + @SuppressWarnings("deprecation") + public AWSS3StorageGetPresignedUrlRequest( + @NonNull String key, + @NonNull StorageAccessLevel accessLevel, + @Nullable String targetIdentityId, + int expires, + boolean useAccelerateEndpoint, + boolean validateObjectExistence + ) { + this.key = key; + this.accessLevel = accessLevel; + this.targetIdentityId = targetIdentityId; + this.expires = expires; + this.useAccelerateEndpoint = useAccelerateEndpoint; + this.validateObjectExistence = validateObjectExistence; } /** @@ -104,5 +136,14 @@ public int getExpires() { public boolean useAccelerateEndpoint() { return useAccelerateEndpoint; } + + /** + * Gets the flag to determine whether to validate for object existence. + * + * @return boolean flag + */ + public boolean validateObjectExistence() { + return validateObjectExistence; + } } diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StoragePathGetPresignedUrlRequest.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StoragePathGetPresignedUrlRequest.kt index 47de0dec6..cf914e35b 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StoragePathGetPresignedUrlRequest.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/request/AWSS3StoragePathGetPresignedUrlRequest.kt @@ -22,5 +22,6 @@ import com.amplifyframework.storage.StoragePath internal data class AWSS3StoragePathGetPresignedUrlRequest( val path: StoragePath, val expires: Int, - val useAccelerateEndpoint: Boolean + val useAccelerateEndpoint: Boolean, + val validateObjectExistence: Boolean, ) diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/service/AWSS3StorageService.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/service/AWSS3StorageService.kt index ec7d087e3..b4d733a73 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/service/AWSS3StorageService.kt +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/service/AWSS3StorageService.kt @@ -20,11 +20,14 @@ import aws.sdk.kotlin.services.s3.S3Client import aws.sdk.kotlin.services.s3.deleteObject import aws.sdk.kotlin.services.s3.listObjectsV2 import aws.sdk.kotlin.services.s3.model.GetObjectRequest +import aws.sdk.kotlin.services.s3.model.HeadObjectRequest +import aws.sdk.kotlin.services.s3.model.NotFound import aws.sdk.kotlin.services.s3.paginators.listObjectsV2Paginated import aws.sdk.kotlin.services.s3.presigners.presignGetObject import aws.sdk.kotlin.services.s3.withConfig import com.amplifyframework.auth.AuthCredentialsProvider import com.amplifyframework.storage.ObjectMetadata +import com.amplifyframework.storage.StorageException import com.amplifyframework.storage.StorageItem import com.amplifyframework.storage.result.StorageListResult import com.amplifyframework.storage.s3.transfer.TransferManager @@ -85,6 +88,30 @@ internal class AWSS3StorageService( return URL(presignUrlRequest.url.toString()) } + /** + * Validate if S3 object exists for the given key. + * Throws StorageException if NoSuchKey S3 client exception is caught. + * @param serviceKey S3 service key + */ + override fun validateObjectExists(serviceKey: String) { + try { + runBlocking { + s3Client.headObject( + HeadObjectRequest { + bucket = s3BucketName + key = serviceKey + } + ) + } + } catch (ex: NotFound) { + throw StorageException( + "Unable to generate URL for non-existent path: $serviceKey", + ex, + "Please ensure the path is valid or the object has been uploaded" + ) + } + } + /** * Begin downloading a file. * @param serviceKey S3 service key diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/service/StorageService.java b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/service/StorageService.java index cc0eaf762..00fd39ede 100644 --- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/service/StorageService.java +++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/service/StorageService.java @@ -20,6 +20,7 @@ import androidx.annotation.Nullable; import com.amplifyframework.storage.ObjectMetadata; +import com.amplifyframework.storage.StorageException; import com.amplifyframework.storage.StorageItem; import com.amplifyframework.storage.result.StorageListResult; import com.amplifyframework.storage.s3.transfer.TransferObserver; @@ -36,6 +37,15 @@ */ public interface StorageService { + /** + * Validate if Storage object exists for the given key. + * Throws StorageException if object is not does not exist. + * + * @param serviceKey key to uniquely specify item to generate URL for + * @throws StorageException If object does not exist in storage + */ + void validateObjectExists(@NonNull String serviceKey) throws StorageException; + /** * Generate pre-signed download URL for an object. * diff --git a/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperationTest.kt b/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperationTest.kt index 01777f683..5460a0161 100644 --- a/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperationTest.kt +++ b/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StorageGetPresignedUrlOperationTest.kt @@ -26,6 +26,7 @@ import com.amplifyframework.storage.s3.service.StorageService import com.google.common.util.concurrent.MoreExecutors import io.mockk.coEvery import io.mockk.mockk +import io.mockk.verify import org.junit.Before import org.junit.Test import org.mockito.Mockito @@ -138,4 +139,81 @@ public class AWSS3StorageGetPresignedUrlOperationTest { awsS3StorageGetPresignedUrlOperation.start() Mockito.verify(storageService).getPresignedUrl(expectedKey, 1, false) } + + @Test + fun `getPresignedUrl fails with non existent S3 path when validateObjectExistence is enabled`() { + val key = "123" + val expectedKey = "public/123" + val request = AWSS3StorageGetPresignedUrlRequest( + key, + StorageAccessLevel.PUBLIC, + "", + 1, + false, + true + ) + val expectedException = StorageException("Test", "Test") + storageService = mockk(relaxed = true) + coEvery { storageService.validateObjectExists(any()) } throws expectedException + coEvery { authCredentialsProvider.getIdentityId() } returns "abc" + val onError = mockk>(relaxed = true) + awsS3StorageGetPresignedUrlOperation = AWSS3StorageGetPresignedUrlOperation( + storageService, + MoreExecutors.newDirectExecutorService(), + authCredentialsProvider, + request, + AWSS3StoragePluginConfiguration {}, + {}, + onError + ) + + // WHEN + awsS3StorageGetPresignedUrlOperation.start() + + // THEN + verify(exactly = 1) { onError.accept(expectedException) } + verify(exactly = 0) { + storageService.getPresignedUrl(any(), any(), any()) + } + } + + @Test + fun `getPresignedUrl succeeds when validateObjectExistence is enabled`() { + // GIVEN + val key = "123" + val expectedKey = "public/123" + val request = AWSS3StorageGetPresignedUrlRequest( + key, + StorageAccessLevel.PUBLIC, + "", + 1, + false, + true + ) + storageService = mockk(relaxed = true) + coEvery { authCredentialsProvider.getIdentityId() } returns "abc" + val onError = mockk>(relaxed = true) + awsS3StorageGetPresignedUrlOperation = AWSS3StorageGetPresignedUrlOperation( + storageService, + MoreExecutors.newDirectExecutorService(), + authCredentialsProvider, + request, + AWSS3StoragePluginConfiguration {}, + {}, + onError + ) + + // WHEN + awsS3StorageGetPresignedUrlOperation.start() + + // THEN + verify(exactly = 0) { onError.accept(any()) } + verify { + storageService.getPresignedUrl( + expectedKey, + 1, + false + ) + } + } } diff --git a/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathGetUrlOperationTest.kt b/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathGetUrlOperationTest.kt index 0c43c7566..40cabddf2 100644 --- a/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathGetUrlOperationTest.kt +++ b/aws-storage-s3/src/test/java/com/amplifyframework/storage/s3/operation/AWSS3StoragePathGetUrlOperationTest.kt @@ -33,7 +33,7 @@ import org.junit.Test class AWSS3StoragePathGetUrlOperationTest { - private lateinit var awsS3StorageDownloadFileOperation: AWSS3StoragePathGetPresignedUrlOperation + private lateinit var awsS3StorageGetPresignedUrlOperation: AWSS3StoragePathGetPresignedUrlOperation private lateinit var storageService: StorageService private lateinit var authCredentialsProvider: AuthCredentialsProvider @@ -53,10 +53,11 @@ class AWSS3StoragePathGetUrlOperationTest { val request = AWSS3StoragePathGetPresignedUrlRequest( path, expectedExpires, - false + false, + validateObjectExistence = false ) val onError = mockk>(relaxed = true) - awsS3StorageDownloadFileOperation = AWSS3StoragePathGetPresignedUrlOperation( + awsS3StorageGetPresignedUrlOperation = AWSS3StoragePathGetPresignedUrlOperation( request = request, storageService = storageService, executorService = MoreExecutors.newDirectExecutorService(), @@ -66,7 +67,7 @@ class AWSS3StoragePathGetUrlOperationTest { ) // WHEN - awsS3StorageDownloadFileOperation.start() + awsS3StorageGetPresignedUrlOperation.start() // THEN verify(exactly = 0) { onError.accept(any()) } @@ -88,10 +89,11 @@ class AWSS3StoragePathGetUrlOperationTest { val request = AWSS3StoragePathGetPresignedUrlRequest( path, expectedExpires, - false + false, + validateObjectExistence = false ) val onError = mockk>(relaxed = true) - awsS3StorageDownloadFileOperation = AWSS3StoragePathGetPresignedUrlOperation( + awsS3StorageGetPresignedUrlOperation = AWSS3StoragePathGetPresignedUrlOperation( request = request, storageService = storageService, executorService = MoreExecutors.newDirectExecutorService(), @@ -101,7 +103,7 @@ class AWSS3StoragePathGetUrlOperationTest { ) // WHEN - awsS3StorageDownloadFileOperation.start() + awsS3StorageGetPresignedUrlOperation.start() // THEN verify(exactly = 0) { onError.accept(any()) } @@ -122,10 +124,11 @@ class AWSS3StoragePathGetUrlOperationTest { val request = AWSS3StoragePathGetPresignedUrlRequest( path, expectedExpires, - false + false, + validateObjectExistence = false ) val onError = mockk>(relaxed = true) - awsS3StorageDownloadFileOperation = AWSS3StoragePathGetPresignedUrlOperation( + awsS3StorageGetPresignedUrlOperation = AWSS3StoragePathGetPresignedUrlOperation( request = request, storageService = storageService, executorService = MoreExecutors.newDirectExecutorService(), @@ -135,7 +138,7 @@ class AWSS3StoragePathGetUrlOperationTest { ) // WHEN - awsS3StorageDownloadFileOperation.start() + awsS3StorageGetPresignedUrlOperation.start() // THEN verify { onError.accept(StoragePathValidationException.invalidStoragePathException()) } @@ -153,10 +156,11 @@ class AWSS3StoragePathGetUrlOperationTest { val request = AWSS3StoragePathGetPresignedUrlRequest( path, expectedExpires, - false + false, + validateObjectExistence = false ) val onError = mockk>(relaxed = true) - awsS3StorageDownloadFileOperation = AWSS3StoragePathGetPresignedUrlOperation( + awsS3StorageGetPresignedUrlOperation = AWSS3StoragePathGetPresignedUrlOperation( request = request, storageService = storageService, executorService = MoreExecutors.newDirectExecutorService(), @@ -166,7 +170,7 @@ class AWSS3StoragePathGetUrlOperationTest { ) // WHEN - awsS3StorageDownloadFileOperation.start() + awsS3StorageGetPresignedUrlOperation.start() // THEN verify { @@ -190,10 +194,11 @@ class AWSS3StoragePathGetUrlOperationTest { val request = AWSS3StoragePathGetPresignedUrlRequest( path, expectedExpires, - false + false, + validateObjectExistence = false ) val onError = mockk>(relaxed = true) - awsS3StorageDownloadFileOperation = AWSS3StoragePathGetPresignedUrlOperation( + awsS3StorageGetPresignedUrlOperation = AWSS3StoragePathGetPresignedUrlOperation( request = request, storageService = storageService, executorService = MoreExecutors.newDirectExecutorService(), @@ -203,7 +208,7 @@ class AWSS3StoragePathGetUrlOperationTest { ) // WHEN - awsS3StorageDownloadFileOperation.start() + awsS3StorageGetPresignedUrlOperation.start() // THEN verify { onError.accept(StoragePathValidationException.unsupportedStoragePathException()) } @@ -212,5 +217,72 @@ class AWSS3StoragePathGetUrlOperationTest { } } + @Test + fun `getPresignedUrl fails with non existent S3 path when validateObjectExistence is enabled`() { + // GIVEN + val path = StoragePath.fromString("public/123") + val expectedException = StorageException("Test", "Test") + coEvery { storageService.validateObjectExists(any()) } throws expectedException + val request = AWSS3StoragePathGetPresignedUrlRequest( + path, + expectedExpires, + false, + validateObjectExistence = true + ) + val onError = mockk>(relaxed = true) + awsS3StorageGetPresignedUrlOperation = AWSS3StoragePathGetPresignedUrlOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + onSuccess = {}, + onError = onError + ) + + // WHEN + awsS3StorageGetPresignedUrlOperation.start() + + // THEN + verify(exactly = 1) { onError.accept(expectedException) } + verify(exactly = 0) { + storageService.getPresignedUrl(any(), any(), any()) + } + } + + @Test + fun `getPresignedUrl succeeds when validateObjectExistence is enabled`() { + // GIVEN + val path = StoragePath.fromString("public/123") + val expectedServiceKey = "public/123" + val request = AWSS3StoragePathGetPresignedUrlRequest( + path, + expectedExpires, + false, + validateObjectExistence = true + ) + val onError = mockk>(relaxed = true) + awsS3StorageGetPresignedUrlOperation = AWSS3StoragePathGetPresignedUrlOperation( + request = request, + storageService = storageService, + executorService = MoreExecutors.newDirectExecutorService(), + authCredentialsProvider = authCredentialsProvider, + onSuccess = {}, + onError = onError + ) + + // WHEN + awsS3StorageGetPresignedUrlOperation.start() + + // THEN + verify(exactly = 0) { onError.accept(any()) } + verify { + storageService.getPresignedUrl( + expectedServiceKey, + expectedExpires, + false + ) + } + } + class UnsupportedStoragePath : StoragePath() }