diff --git a/.github/composite_actions/get_platform_parameters/action.yml b/.github/composite_actions/get_platform_parameters/action.yml
index 35353e4c98..c9e8e61e07 100644
--- a/.github/composite_actions/get_platform_parameters/action.yml
+++ b/.github/composite_actions/get_platform_parameters/action.yml
@@ -39,7 +39,8 @@ runs:
- id: get-xcode-version
run: |
LATEST_XCODE_VERSION=14.3.1
- MINIMUM_XCODE_VERSION=14.0.1
+ MINIMUM_XCODE_VERSION_IOS_MAC=14.1.0
+ MINIMUM_XCODE_VERSION_WATCH_TV=14.3.1
INPUT_XCODE_VERSION=${{ inputs.xcode_version }}
@@ -47,7 +48,13 @@ runs:
latest)
XCODE_VERSION=$LATEST_XCODE_VERSION ;;
minimum)
- XCODE_VERSION=$MINIMUM_XCODE_VERSION ;;
+ INPUT_PLATFORM=${{ inputs.platform }}
+ case $INPUT_PLATFORM in
+ iOS|macOS)
+ XCODE_VERSION=$MINIMUM_XCODE_VERSION_IOS_MAC ;;
+ tvOS|watchOS)
+ XCODE_VERSION=$MINIMUM_XCODE_VERSION_WATCH_TV ;;
+ esac ;;
*)
XCODE_VERSION=$INPUT_XCODE_VERSION ;;
esac
@@ -63,9 +70,9 @@ runs:
DESTINATION_MAPPING='{
"minimum": {
- "iOS": "platform=iOS Simulator,name=iPhone 14,OS=16.0",
- "tvOS": "platform=tvOS Simulator,name=Apple TV 4K (2nd generation),OS=16.0",
- "watchOS": "platform=watchOS Simulator,name=Apple Watch Series 8 (45mm),OS=9.0",
+ "iOS": "platform=iOS Simulator,name=iPhone 14,OS=16.1",
+ "tvOS": "platform=tvOS Simulator,name=Apple TV 4K (2nd generation),OS=16.1",
+ "watchOS": "platform=watchOS Simulator,name=Apple Watch Series 8 (45mm),OS=9.1",
"macOS": "platform=macOS,arch=x86_64"
},
"latest": {
diff --git a/.github/workflows/build_amplify_swift_platforms.yml b/.github/workflows/build_amplify_swift_platforms.yml
index 54fc16d50e..2d8c6ce5c5 100644
--- a/.github/workflows/build_amplify_swift_platforms.yml
+++ b/.github/workflows/build_amplify_swift_platforms.yml
@@ -52,8 +52,9 @@ jobs:
- platform: ${{ github.event.inputs.macos == 'false' && 'macOS' || 'None' }}
- platform: ${{ github.event.inputs.tvos == 'false' && 'tvOS' || 'None' }}
- platform: ${{ github.event.inputs.watchos == 'false' && 'watchOS' || 'None' }}
- uses: ./.github/workflows/build_amplify_swift.yml
+ uses: ./.github/workflows/build_scheme.yml
with:
+ scheme: Amplify-Package
platform: ${{ matrix.platform }}
confirm-pass:
diff --git a/.github/workflows/build_minimum_supported_swift_platforms.yml b/.github/workflows/build_minimum_supported_swift_platforms.yml
index 1b3c1cadfd..0e0522a479 100644
--- a/.github/workflows/build_minimum_supported_swift_platforms.yml
+++ b/.github/workflows/build_minimum_supported_swift_platforms.yml
@@ -1,11 +1,21 @@
-name: Build with Minimum Supported Xcode Versions
+name: Build with minimum Xcode version | Amplify Swift
on:
workflow_dispatch:
+ pull_request:
+ branches:
+ - main
+ push:
+ branches:
+ - main
permissions:
contents: read
actions: write
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.ref_name != 'main'}}
+
jobs:
build-amplify-with-minimum-supported-xcode:
name: Build Amplify Swift for ${{ matrix.platform }}
@@ -14,12 +24,13 @@ jobs:
matrix:
platform: [iOS, macOS, tvOS, watchOS]
- uses: ./.github/workflows/build_amplify_swift.yml
+ uses: ./.github/workflows/build_scheme.yml
with:
- os-runner: macos-12
+ scheme: Amplify-Build
+ os-runner: ${{ (matrix.platform == 'tvOS' || matrix.platform == 'watchOS') && 'macos-13' || 'macos-12' }}
xcode-version: 'minimum'
platform: ${{ matrix.platform }}
- cacheable: false
+ save_build_cache: false
confirm-pass:
runs-on: ubuntu-latest
diff --git a/.github/workflows/build_amplify_swift.yml b/.github/workflows/build_scheme.yml
similarity index 78%
rename from .github/workflows/build_amplify_swift.yml
rename to .github/workflows/build_scheme.yml
index 095edbcfab..1ec3b433fc 100644
--- a/.github/workflows/build_amplify_swift.yml
+++ b/.github/workflows/build_scheme.yml
@@ -1,7 +1,11 @@
-name: Build Amplify-Package for the given platform
+name: Build scheme for the given platform and other parameters
on:
workflow_call:
inputs:
+ scheme:
+ type: string
+ required: true
+
platform:
type: string
required: true
@@ -14,7 +18,7 @@ on:
type: string
default: 'macos-13'
- cacheable:
+ save_build_cache:
type: boolean
default: true
@@ -23,8 +27,8 @@ permissions:
actions: write
jobs:
- build-amplify-swift:
- name: Build Amplify-Package | ${{ inputs.platform }}
+ build-scheme:
+ name: Build ${{ inputs.scheme }} | ${{ inputs.platform }}
runs-on: ${{ inputs.os-runner }}
steps:
- name: Checkout repository
@@ -41,9 +45,8 @@ jobs:
- name: Attempt to use the dependencies cache
id: dependencies-cache
- if: inputs.cacheable
timeout-minutes: 4
- continue-on-error: ${{ inputs.cacheable }}
+ continue-on-error: true
uses: actions/cache/restore@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
with:
path: ~/Library/Developer/Xcode/DerivedData/Amplify
@@ -53,20 +56,18 @@ jobs:
- name: Attempt to restore the build cache from main
id: build-cache
- if: inputs.cacheable
timeout-minutes: 4
- continue-on-error: ${{ inputs.cacheable }}
+ continue-on-error: true
uses: actions/cache/restore@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
with:
path: ${{ github.workspace }}/Build
key: Amplify-${{ inputs.platform }}-build-cache
- - name: Build Amplify for Swift
+ - name: Build ${{ inputs.scheme }}
id: build-package
- continue-on-error: ${{ inputs.cacheable }}
uses: ./.github/composite_actions/run_xcodebuild
with:
- scheme: Amplify-Package
+ scheme: ${{ inputs.scheme }}
destination: ${{ steps.platform.outputs.destination }}
sdk: ${{ steps.platform.outputs.sdk }}
xcode_path: /Applications/Xcode_${{ steps.platform.outputs.xcode-version }}.app
@@ -75,22 +76,22 @@ jobs:
disable_package_resolution: ${{ steps.dependencies-cache.outputs.cache-hit }}
- name: Save the dependencies cache in main
- if: inputs.cacheable && steps.dependencies-cache.outputs.cache-hit != 'true' && github.ref_name == 'main'
+ if: inputs.save_build_cache && steps.dependencies-cache.outputs.cache-hit != 'true' && github.ref_name == 'main'
uses: actions/cache/save@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
with:
path: ~/Library/Developer/Xcode/DerivedData/Amplify
key: ${{ steps.dependencies-cache.outputs.cache-primary-key }}
- name: Delete the old build cache
- if: inputs.cacheable && steps.build-cache.outputs.cache-hit && github.ref_name == 'main'
+ if: inputs.save_build_cache && steps.build-cache.outputs.cache-hit && github.ref_name == 'main'
env:
GH_TOKEN: ${{ github.token }}
- continue-on-error: ${{ inputs.cacheable }}
+ continue-on-error: true
run: |
gh cache delete ${{ steps.build-cache.outputs.cache-primary-key }}
- name: Save the build cache
- if: inputs.cacheable && github.ref_name == 'main'
+ if: inputs.save_build_cache && github.ref_name == 'main'
uses: actions/cache/save@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
with:
path: ${{ github.workspace }}/Build
diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml
index 7873f7b5d8..1dbd2e4565 100644
--- a/.github/workflows/unit_test.yml
+++ b/.github/workflows/unit_test.yml
@@ -78,30 +78,30 @@ jobs:
scheme: ${{ matrix.scheme }}
generate_coverage_report: true
- report-coverage:
- name: ${{ matrix.file.scheme }} Unit Tests
- needs: [unit-tests-with-coverage]
- strategy:
- fail-fast: false
- matrix:
- file: [
- { scheme: Amplify, flags: 'Amplify,unit_tests' },
- { scheme: AWSPluginsCore, flags: 'AWSPluginsCore,unit_tests' },
- { scheme: AWSAPIPlugin, flags: 'API_plugin_unit_test,unit_tests' },
- { scheme: AWSCloudWatchLoggingPlugin, flags: 'Logging_plugin_unit_test,unit_tests' },
- { scheme: AWSCognitoAuthPlugin, flags: 'Auth_plugin_unit_test,unit_tests' },
- { scheme: AWSDataStorePlugin, flags: 'DataStore_plugin_unit_test,unit_tests' },
- { scheme: AWSLocationGeoPlugin, flags: 'Geo_plugin_unit_test,unit_tests' },
- { scheme: AWSPredictionsPlugin, flags: 'Predictions_plugin_unit_test,unit_tests' },
- { scheme: AWSPinpointAnalyticsPlugin, flags: 'Analytics_plugin_unit_test,unit_tests' },
- { scheme: AWSPinpointPushNotificationsPlugin, flags: 'PushNotifications_plugin_unit_test,unit_tests' },
- { scheme: AWSS3StoragePlugin, flags: 'Storage_plugin_unit_test,unit_tests' },
- { scheme: CoreMLPredictionsPlugin, flags: 'CoreMLPredictions_plugin_unit_test,unit_tests' }
- ]
- uses: ./.github/workflows/upload_coverage_report.yml
- with:
- scheme: ${{ matrix.file.scheme }}
- flags: ${{ matrix.file.flags }}
+ # report-coverage:
+ # name: ${{ matrix.file.scheme }} Unit Tests
+ # needs: [unit-tests-with-coverage]
+ # strategy:
+ # fail-fast: false
+ # matrix:
+ # file: [
+ # { scheme: Amplify, flags: 'Amplify,unit_tests' },
+ # { scheme: AWSPluginsCore, flags: 'AWSPluginsCore,unit_tests' },
+ # { scheme: AWSAPIPlugin, flags: 'API_plugin_unit_test,unit_tests' },
+ # { scheme: AWSCloudWatchLoggingPlugin, flags: 'Logging_plugin_unit_test,unit_tests' },
+ # { scheme: AWSCognitoAuthPlugin, flags: 'Auth_plugin_unit_test,unit_tests' },
+ # { scheme: AWSDataStorePlugin, flags: 'DataStore_plugin_unit_test,unit_tests' },
+ # { scheme: AWSLocationGeoPlugin, flags: 'Geo_plugin_unit_test,unit_tests' },
+ # { scheme: AWSPredictionsPlugin, flags: 'Predictions_plugin_unit_test,unit_tests' },
+ # { scheme: AWSPinpointAnalyticsPlugin, flags: 'Analytics_plugin_unit_test,unit_tests' },
+ # { scheme: AWSPinpointPushNotificationsPlugin, flags: 'PushNotifications_plugin_unit_test,unit_tests' },
+ # { scheme: AWSS3StoragePlugin, flags: 'Storage_plugin_unit_test,unit_tests' },
+ # { scheme: CoreMLPredictionsPlugin, flags: 'CoreMLPredictions_plugin_unit_test,unit_tests' }
+ # ]
+ # uses: ./.github/workflows/upload_coverage_report.yml
+ # with:
+ # scheme: ${{ matrix.file.scheme }}
+ # flags: ${{ matrix.file.flags }}
unit-test-pass-confirmation:
runs-on: ubuntu-latest
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Build.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Build.xcscheme
new file mode 100644
index 0000000000..dd2eea7c5b
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Build.xcscheme
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift
index f4f7ee0b83..1a8ed260b9 100644
--- a/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift
+++ b/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift
@@ -13,9 +13,15 @@ import Foundation
/// - Tag: StorageDownloadDataRequest
public struct StorageDownloadDataRequest: AmplifyOperationRequest {
+ /// The path for the object in storage
+ ///
+ /// - Tag: StorageDownloadFileRequest.path
+ public let path: (any StoragePath)?
+
/// The unique identifier for the object in storage
///
/// - Tag: StorageDownloadDataRequest.key
+ @available(*, deprecated, message: "Use `path` instead of `key`")
public let key: String
/// Options to adjust the behavior of this request, including plugin-options
@@ -23,10 +29,19 @@ public struct StorageDownloadDataRequest: AmplifyOperationRequest {
/// - Tag: StorageDownloadDataRequest.options
public let options: Options
- /// - Tag: StorageDownloadDataRequest.key
+ /// - Tag: StorageDownloadDataRequest.init
+ @available(*, deprecated, message: "Use init(path:local:options)")
public init(key: String, options: Options) {
self.key = key
self.options = options
+ self.path = nil
+ }
+
+ /// - Tag: StorageDownloadDataRequest.init
+ public init(path: any StoragePath, options: Options) {
+ self.key = ""
+ self.options = options
+ self.path = path
}
}
@@ -40,11 +55,13 @@ public extension StorageDownloadDataRequest {
/// Access level of the storage system. Defaults to `public`
///
/// - Tag: StorageDownloadDataRequestOptions.accessLevel
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let accessLevel: StorageAccessLevel
/// Target user to apply the action on.
///
/// - Tag: StorageDownloadDataRequestOptions.targetIdentityId
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let targetIdentityId: String?
/// Extra plugin specific options, only used in special circumstances when the existing options do not provide
@@ -73,6 +90,7 @@ public extension StorageDownloadDataRequest {
///
/// - Tag: StorageDownloadDataRequestOptions.init
+ @available(*, deprecated, message: "Use init(pluginOptions)")
public init(accessLevel: StorageAccessLevel = .guest,
targetIdentityId: String? = nil,
pluginOptions: Any? = nil) {
@@ -80,5 +98,13 @@ public extension StorageDownloadDataRequest {
self.targetIdentityId = targetIdentityId
self.pluginOptions = pluginOptions
}
+
+ ///
+ /// - Tag: StorageDownloadDataRequestOptions.init
+ public init(pluginOptions: Any? = nil) {
+ self.accessLevel = .guest
+ self.targetIdentityId = nil
+ self.pluginOptions = pluginOptions
+ }
}
}
diff --git a/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift
index 78d7be6ffd..7ec34c222f 100644
--- a/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift
+++ b/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift
@@ -13,9 +13,15 @@ import Foundation
/// - Tag: StorageDownloadFileRequest
public struct StorageDownloadFileRequest: AmplifyOperationRequest {
+ /// The path for the object in storage
+ ///
+ /// - Tag: StorageDownloadFileRequest.path
+ public let path: (any StoragePath)?
+
/// The unique identifier for the object in storage
///
/// - Tag: StorageDownloadFileRequest.key
+ @available(*, deprecated, message: "Use `path` instead of `key`")
public let key: String
/// The local file to download the object to
@@ -29,10 +35,20 @@ public struct StorageDownloadFileRequest: AmplifyOperationRequest {
public let options: Options
/// - Tag: StorageDownloadFileRequest.init
+ @available(*, deprecated, message: "Use init(path:local:options)")
public init(key: String, local: URL, options: Options) {
self.key = key
self.local = local
self.options = options
+ self.path = nil
+ }
+
+ /// - Tag: StorageDownloadFileRequest.init
+ public init(path: any StoragePath, local: URL, options: Options) {
+ self.key = ""
+ self.local = local
+ self.options = options
+ self.path = path
}
}
@@ -46,11 +62,13 @@ public extension StorageDownloadFileRequest {
/// Access level of the storage system. Defaults to `public`
///
/// - Tag: StorageDownloadFileRequestOptions.accessLevel
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let accessLevel: StorageAccessLevel
/// Target user to apply the action on.
///
/// - Tag: StorageDownloadFileRequestOptions.targetIdentityId
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let targetIdentityId: String?
/// Extra plugin specific options, only used in special circumstances when the existing options do not provide
@@ -61,6 +79,7 @@ public extension StorageDownloadFileRequest {
public let pluginOptions: Any?
/// - Tag: StorageDownloadFileRequestOptions.init
+ @available(*, deprecated, message: "Use init(pluginOptions)")
public init(accessLevel: StorageAccessLevel = .guest,
targetIdentityId: String? = nil,
pluginOptions: Any? = nil) {
@@ -68,5 +87,13 @@ public extension StorageDownloadFileRequest {
self.targetIdentityId = targetIdentityId
self.pluginOptions = pluginOptions
}
+
+ /// - Tag: StorageDownloadFileRequestOptions.init
+ @available(*, deprecated, message: "Use init(pluginOptions)")
+ public init(pluginOptions: Any? = nil) {
+ self.accessLevel = .guest
+ self.targetIdentityId = nil
+ self.pluginOptions = pluginOptions
+ }
}
}
diff --git a/Amplify/Categories/Storage/Operation/Request/StorageGetURLRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageGetURLRequest.swift
index 25c4563098..e8ffd22c00 100644
--- a/Amplify/Categories/Storage/Operation/Request/StorageGetURLRequest.swift
+++ b/Amplify/Categories/Storage/Operation/Request/StorageGetURLRequest.swift
@@ -14,18 +14,33 @@ public struct StorageGetURLRequest: AmplifyOperationRequest {
/// The unique identifier for the object in storage
///
- /// - Tag: StorageListRequest.key
+ /// - Tag: StorageGetURLRequest.key
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `key`")
public let key: String
- /// Options to adjust the behavior of this request, including plugin-options
+ /// The unique path for the object in storage
+ ///
+ /// - Tag: StorageGetURLRequest.path
+ public let path: (any StoragePath)?
+
+ /// Options to adjust the behaviour of this request, including plugin-options
///
- /// - Tag: StorageListRequest.options
+ /// - Tag: StorageGetURLRequest.options
public let options: Options
- /// - Tag: StorageListRequest.init
+ /// - Tag: StorageGetURLRequest.init
+ @available(*, deprecated, message: "Use init(path:options)")
public init(key: String, options: Options) {
self.key = key
self.options = options
+ self.path = nil
+ }
+
+ /// - Tag: StorageGetURLRequest.init
+ public init(path: any StoragePath, options: Options) {
+ self.key = ""
+ self.options = options
+ self.path = path
}
}
@@ -33,27 +48,29 @@ public extension StorageGetURLRequest {
/// Options to adjust the behavior of this request, including plugin-options
///
- /// - Tag: StorageListRequestOptions
+ /// - Tag: StorageGetURLRequest.Options
struct Options {
/// The default amount of time before the URL expires is 18000 seconds, or 5 hours.
///
- /// - Tag: StorageListRequestOptions.defaultExpireInSeconds
+ /// - Tag: StorageGetURLRequest.Options.defaultExpireInSeconds
public static let defaultExpireInSeconds = 18_000
/// Access level of the storage system. Defaults to `public`
///
- /// - Tag: StorageListRequestOptions.accessLevel
+ /// - Tag: StorageGetURLRequest.Options.accessLevel
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let accessLevel: StorageAccessLevel
/// Target user to apply the action on.
///
- /// - Tag: StorageListRequestOptions.targetIdentityId
+ /// - Tag: StorageGetURLRequest.Options.targetIdentityId
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let targetIdentityId: String?
/// Number of seconds before the URL expires. Defaults to
/// [defaultExpireInSeconds](x-source-tag://StorageListRequestOptions.defaultExpireInSeconds)
///
- /// - Tag: StorageListRequestOptions.expires
+ /// - Tag: StorageGetURLRequest.Options.expires
public let expires: Int
/// Extra plugin specific options, only used in special circumstances when the existing options do
@@ -62,10 +79,11 @@ public extension StorageGetURLRequest {
/// [AWSStorageGetURLOptions](x-source-tag://AWSStorageGetURLOptions) for
/// expected key/values.
///
- /// - Tag: StorageListRequestOptions.pluginOptions
+ /// - Tag: StorageGetURLRequest.Options.pluginOptions
public let pluginOptions: Any?
- /// - Tag: StorageListRequestOptions.init
+ /// - Tag: StorageGetURLRequest.Options.init
+ @available(*, deprecated, message: "Use init(expires:pluginOptions)")
public init(accessLevel: StorageAccessLevel = .guest,
targetIdentityId: String? = nil,
expires: Int = Options.defaultExpireInSeconds,
@@ -75,5 +93,14 @@ public extension StorageGetURLRequest {
self.expires = expires
self.pluginOptions = pluginOptions
}
+
+ /// - Tag: StorageGetURLRequest.Options.init
+ public init(expires: Int = Options.defaultExpireInSeconds,
+ pluginOptions: Any? = nil) {
+ self.expires = expires
+ self.pluginOptions = pluginOptions
+ self.accessLevel = .guest
+ self.targetIdentityId = nil
+ }
}
}
diff --git a/Amplify/Categories/Storage/Operation/Request/StorageListRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageListRequest.swift
index d82d4f1718..6cc7dbb496 100644
--- a/Amplify/Categories/Storage/Operation/Request/StorageListRequest.swift
+++ b/Amplify/Categories/Storage/Operation/Request/StorageListRequest.swift
@@ -15,9 +15,22 @@ public struct StorageListRequest: AmplifyOperationRequest {
/// - Tag: StorageListRequest
public let options: Options
+ /// The unique path for the object in storage
+ ///
+ /// - Tag: StorageListRequest.path
+ public let path: (any StoragePath)?
+
/// - Tag: StorageListRequest.init
+ @available(*, deprecated, message: "Use init(path:options)")
public init(options: Options) {
self.options = options
+ self.path = nil
+ }
+
+ /// - Tag: StorageListRequest.init
+ public init(path: any StoragePath, options: Options) {
+ self.options = options
+ self.path = path
}
}
@@ -32,16 +45,19 @@ public extension StorageListRequest {
/// Access level of the storage system. Defaults to `public`
///
/// - Tag: StorageListRequestOptions.accessLevel
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let accessLevel: StorageAccessLevel
/// Target user to apply the action on
///
/// - Tag: StorageListRequestOptions.targetIdentityId
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let targetIdentityId: String?
/// Path to the keys
///
/// - Tag: StorageListRequestOptions.path
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let path: String?
/// Number between 1 and 1,000 that indicates the limit of how many entries to fetch when
diff --git a/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift
index 91492a3c70..31ae1de4f3 100644
--- a/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift
+++ b/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift
@@ -14,17 +14,32 @@ public struct StorageRemoveRequest: AmplifyOperationRequest {
/// The unique identifier for the object in storage
///
/// - Tag: StorageRemoveRequest.key
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `key`")
public let key: String
+ /// The unique path for the object in storage
+ ///
+ /// - Tag: StorageRemoveRequest.path
+ public let path: (any StoragePath)?
+
/// Options to adjust the behavior of this request, including plugin-options
///
/// - Tag: StorageRemoveRequest.options
public let options: Options
/// - Tag: StorageRemoveRequest.init
+ @available(*, deprecated, message: "Use init(path:options)")
public init(key: String, options: Options) {
self.key = key
self.options = options
+ self.path = nil
+ }
+
+ /// - Tag: StorageRemoveRequest.init
+ public init(path: any StoragePath, options: Options) {
+ self.key = ""
+ self.options = options
+ self.path = path
}
}
@@ -38,6 +53,7 @@ public extension StorageRemoveRequest {
/// Access level of the storage system. Defaults to `public`
///
/// - Tag: StorageRemoveRequestOptions.accessLevel
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let accessLevel: StorageAccessLevel
/// Extra plugin specific options, only used in special circumstances when the existing options do not provide
diff --git a/Amplify/Categories/Storage/Operation/Request/StorageUploadDataRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageUploadDataRequest.swift
index 2f892b936d..572b160bf8 100644
--- a/Amplify/Categories/Storage/Operation/Request/StorageUploadDataRequest.swift
+++ b/Amplify/Categories/Storage/Operation/Request/StorageUploadDataRequest.swift
@@ -13,9 +13,15 @@ import Foundation
/// - Tag: StorageUploadDataRequest
public struct StorageUploadDataRequest: AmplifyOperationRequest {
+ /// The path for the object in storage
+ ///
+ /// - Tag: StorageDownloadFileRequest.path
+ public let path: (any StoragePath)?
+
/// The unique identifier for the object in storage
///
/// - Tag: StorageUploadDataRequest.key
+ @available(*, deprecated, message: "Use `path` instead of `key`")
public let key: String
/// The data in memory to be uploaded
@@ -29,10 +35,19 @@ public struct StorageUploadDataRequest: AmplifyOperationRequest {
public let options: Options
/// - Tag: StorageUploadDataRequest.init
+ @available(*, deprecated, message: "Use init(path:data:options)")
public init(key: String, data: Data, options: Options) {
self.key = key
self.data = data
self.options = options
+ self.path = nil
+ }
+
+ public init(path: any StoragePath, data: Data, options: Options) {
+ self.key = ""
+ self.data = data
+ self.options = options
+ self.path = path
}
}
@@ -46,11 +61,13 @@ public extension StorageUploadDataRequest {
/// Access level of the storage system. Defaults to `public`
///
/// - Tag: StorageUploadDataRequestOptions.accessLevel
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let accessLevel: StorageAccessLevel
/// Target user to apply the action on.
///
/// - Tag: StorageUploadDataRequestOptions.targetIdentityId
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let targetIdentityId: String?
/// Metadata for the object to store
@@ -71,16 +88,30 @@ public extension StorageUploadDataRequest {
public let pluginOptions: Any?
/// - Tag: StorageUploadDataRequestOptions.init
+ @available(*, deprecated, message: "Use init(metadata:contentType:options)")
public init(accessLevel: StorageAccessLevel = .guest,
targetIdentityId: String? = nil,
metadata: [String: String]? = nil,
contentType: String? = nil,
- pluginOptions: Any? = nil) {
+ pluginOptions: Any? = nil
+ ) {
self.accessLevel = accessLevel
self.targetIdentityId = targetIdentityId
self.metadata = metadata
self.contentType = contentType
self.pluginOptions = pluginOptions
}
+
+ /// - Tag: StorageUploadDataRequestOptions.init
+ public init(metadata: [String: String]? = nil,
+ contentType: String? = nil,
+ pluginOptions: Any? = nil
+ ) {
+ self.accessLevel = .guest
+ self.targetIdentityId = nil
+ self.metadata = metadata
+ self.contentType = contentType
+ self.pluginOptions = pluginOptions
+ }
}
}
diff --git a/Amplify/Categories/Storage/Operation/Request/StorageUploadFileRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageUploadFileRequest.swift
index 9382776c5b..23c8d159f6 100644
--- a/Amplify/Categories/Storage/Operation/Request/StorageUploadFileRequest.swift
+++ b/Amplify/Categories/Storage/Operation/Request/StorageUploadFileRequest.swift
@@ -13,8 +13,14 @@ import Foundation
/// - Tag: StorageUploadFileRequest
public struct StorageUploadFileRequest: AmplifyOperationRequest {
+ /// The path for the object in storage
+ ///
+ /// - Tag: StorageDownloadFileRequest.path
+ public let path: (any StoragePath)?
+
/// The unique identifier for the object in storage
/// - Tag: StorageUploadFileRequest.key
+ @available(*, deprecated, message: "Use `path` instead of `key`")
public let key: String
/// The file to be uploaded
@@ -26,10 +32,19 @@ public struct StorageUploadFileRequest: AmplifyOperationRequest {
public let options: Options
/// - Tag: StorageUploadFileRequest.init
+ @available(*, deprecated, message: "Use init(path:local:options)")
public init(key: String, local: URL, options: Options) {
self.key = key
self.local = local
self.options = options
+ self.path = nil
+ }
+
+ public init(path: any StoragePath, local: URL, options: Options) {
+ self.key = ""
+ self.local = local
+ self.options = options
+ self.path = path
}
}
@@ -43,11 +58,13 @@ public extension StorageUploadFileRequest {
/// Access level of the storage system. Defaults to `public`
///
/// - Tag: StorageUploadFileRequestOptions.accessLevel
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let accessLevel: StorageAccessLevel
/// Target user to apply the action on.
///
/// - Tag: StorageUploadFileRequestOptions.targetIdentityId
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public let targetIdentityId: String?
/// Metadata for the object to store
@@ -68,16 +85,30 @@ public extension StorageUploadFileRequest {
public let pluginOptions: Any?
/// - Tag: StorageUploadFileRequestOptions.init
+ @available(*, deprecated, message: "Use init(metadata:contentType:pluginOptions)")
public init(accessLevel: StorageAccessLevel = .guest,
targetIdentityId: String? = nil,
metadata: [String: String]? = nil,
contentType: String? = nil,
- pluginOptions: Any? = nil) {
+ pluginOptions: Any? = nil
+ ) {
self.accessLevel = accessLevel
self.targetIdentityId = targetIdentityId
self.metadata = metadata
self.contentType = contentType
self.pluginOptions = pluginOptions
}
+
+ /// - Tag: StorageUploadFileRequestOptions.init
+ public init(metadata: [String: String]? = nil,
+ contentType: String? = nil,
+ pluginOptions: Any? = nil
+ ) {
+ self.accessLevel = .guest
+ self.targetIdentityId = nil
+ self.metadata = metadata
+ self.contentType = contentType
+ self.pluginOptions = pluginOptions
+ }
}
}
diff --git a/Amplify/Categories/Storage/Result/StorageListResult.swift b/Amplify/Categories/Storage/Result/StorageListResult.swift
index f01a517c44..057b9e177a 100644
--- a/Amplify/Categories/Storage/Result/StorageListResult.swift
+++ b/Amplify/Categories/Storage/Result/StorageListResult.swift
@@ -42,9 +42,15 @@ extension StorageListResult {
/// - Tag: StorageListResultItem
public struct Item {
+ /// The path of the object in storage.
+ ///
+ /// - Tag: StorageListResultItem.path
+ public let path: String
+
/// The unique identifier of the object in storage.
///
/// - Tag: StorageListResultItem.key
+ @available(*, deprecated, message: "Use `path` instead.")
public let key: String
/// Size in bytes of the object
@@ -72,16 +78,35 @@ extension StorageListResult {
/// [StorageCategoryBehavior.list](x-source-tag://StorageCategoryBehavior.list).
///
/// - Tag: StorageListResultItem.init
- public init(key: String,
- size: Int? = nil,
- eTag: String? = nil,
- lastModified: Date? = nil,
- pluginResults: Any? = nil) {
+ @available(*, deprecated, message: "Use init(path:size:lastModifiedDate:eTag:pluginResults)")
+ public init(
+ key: String,
+ size: Int? = nil,
+ eTag: String? = nil,
+ lastModified: Date? = nil,
+ pluginResults: Any? = nil
+ ) {
self.key = key
self.size = size
self.eTag = eTag
self.lastModified = lastModified
self.pluginResults = pluginResults
+ self.path = ""
+ }
+
+ public init(
+ path: String,
+ size: Int? = nil,
+ eTag: String? = nil,
+ lastModified: Date? = nil,
+ pluginResults: Any? = nil
+ ) {
+ self.path = path
+ self.key = path
+ self.size = size
+ self.eTag = eTag
+ self.lastModified = lastModified
+ self.pluginResults = pluginResults
}
}
}
diff --git a/Amplify/Categories/Storage/StorageAccessLevel.swift b/Amplify/Categories/Storage/StorageAccessLevel.swift
index 8319818bc8..726effc9be 100644
--- a/Amplify/Categories/Storage/StorageAccessLevel.swift
+++ b/Amplify/Categories/Storage/StorageAccessLevel.swift
@@ -11,6 +11,7 @@ import Foundation
/// See https://aws-amplify.github.io/docs/ios/storage#storage-access
///
/// - Tag: StorageAccessLevel
+@available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
public enum StorageAccessLevel: String {
/// Objects can be read or written by any user without authentication
diff --git a/Amplify/Categories/Storage/StorageCategory+ClientBehavior.swift b/Amplify/Categories/Storage/StorageCategory+ClientBehavior.swift
index f36e1f8954..55b69bbe43 100644
--- a/Amplify/Categories/Storage/StorageCategory+ClientBehavior.swift
+++ b/Amplify/Categories/Storage/StorageCategory+ClientBehavior.swift
@@ -17,6 +17,14 @@ extension StorageCategory: StorageCategoryBehavior {
try await plugin.getURL(key: key, options: options)
}
+ @discardableResult
+ public func getURL(
+ path: any StoragePath,
+ options: StorageGetURLOperation.Request.Options? = nil
+ ) async throws -> URL {
+ try await plugin.getURL(path: path, options: options)
+ }
+
@discardableResult
public func downloadData(
key: String,
@@ -25,6 +33,14 @@ extension StorageCategory: StorageCategoryBehavior {
plugin.downloadData(key: key, options: options)
}
+ @discardableResult
+ public func downloadData(
+ path: any StoragePath,
+ options: StorageDownloadDataOperation.Request.Options? = nil
+ ) -> StorageDownloadDataTask {
+ plugin.downloadData(path: path, options: options)
+ }
+
@discardableResult
public func downloadFile(
key: String,
@@ -34,6 +50,15 @@ extension StorageCategory: StorageCategoryBehavior {
plugin.downloadFile(key: key, local: local, options: options)
}
+ @discardableResult
+ public func downloadFile(
+ path: any StoragePath,
+ local: URL,
+ options: StorageDownloadFileOperation.Request.Options? = nil
+ ) -> StorageDownloadFileTask {
+ plugin.downloadFile(path: path, local: local, options: options)
+ }
+
@discardableResult
public func uploadData(
key: String,
@@ -43,6 +68,15 @@ extension StorageCategory: StorageCategoryBehavior {
plugin.uploadData(key: key, data: data, options: options)
}
+ @discardableResult
+ public func uploadData(
+ path: any StoragePath,
+ data: Data,
+ options: StorageUploadDataOperation.Request.Options? = nil
+ ) -> StorageUploadDataTask {
+ plugin.uploadData(path: path, data: data, options: options)
+ }
+
@discardableResult
public func uploadFile(
key: String,
@@ -52,6 +86,15 @@ extension StorageCategory: StorageCategoryBehavior {
plugin.uploadFile(key: key, local: local, options: options)
}
+ @discardableResult
+ public func uploadFile(
+ path: any StoragePath,
+ local: URL,
+ options: StorageUploadFileOperation.Request.Options? = nil
+ ) -> StorageUploadFileTask {
+ plugin.uploadFile(path: path, local: local, options: options)
+ }
+
@discardableResult
public func remove(
key: String,
@@ -60,6 +103,14 @@ extension StorageCategory: StorageCategoryBehavior {
try await plugin.remove(key: key, options: options)
}
+ @discardableResult
+ public func remove(
+ path: any StoragePath,
+ options: StorageRemoveRequest.Options? = nil
+ ) async throws -> String {
+ try await plugin.remove(path: path, options: options)
+ }
+
@discardableResult
public func list(
options: StorageListOperation.Request.Options? = nil
@@ -67,6 +118,14 @@ extension StorageCategory: StorageCategoryBehavior {
try await plugin.list(options: options)
}
+ @discardableResult
+ public func list(
+ path: any StoragePath,
+ options: StorageListOperation.Request.Options? = nil
+ ) async throws -> StorageListResult {
+ try await plugin.list(path: path, options: options)
+ }
+
public func handleBackgroundEvents(identifier: String) async -> Bool {
await plugin.handleBackgroundEvents(identifier: identifier)
}
diff --git a/Amplify/Categories/Storage/StorageCategoryBehavior.swift b/Amplify/Categories/Storage/StorageCategoryBehavior.swift
index b09b450113..0b933d4dfc 100644
--- a/Amplify/Categories/Storage/StorageCategoryBehavior.swift
+++ b/Amplify/Categories/Storage/StorageCategoryBehavior.swift
@@ -22,9 +22,26 @@ public protocol StorageCategoryBehavior {
/// - Returns: requested Get URL
///
/// - Tag: StorageCategoryBehavior.getURL
+ @available(*, deprecated, message: "Use getURL(path:options:)")
@discardableResult
- func getURL(key: String,
- options: StorageGetURLOperation.Request.Options?) async throws -> URL
+ func getURL(
+ key: String,
+ options: StorageGetURLOperation.Request.Options?
+ ) async throws -> URL
+
+ /// Retrieve the remote URL for the object from storage.
+ ///
+ /// - Parameters:
+ /// - path: the path to the object in storage.
+ /// - options: Parameters to specific plugin behavior
+ /// - Returns: requested Get URL
+ ///
+ /// - Tag: StorageCategoryBehavior.getURL
+ @discardableResult
+ func getURL(
+ path: any StoragePath,
+ options: StorageGetURLOperation.Request.Options?
+ ) async throws -> URL
/// Retrieve the object from storage into memory.
///
@@ -34,10 +51,24 @@ public protocol StorageCategoryBehavior {
/// - Returns: A task that provides progress updates and the key which was used to download
///
/// - Tag: StorageCategoryBehavior.downloadData
+ @available(*, deprecated, message: "Use downloadData(path:options:)")
@discardableResult
func downloadData(key: String,
options: StorageDownloadDataOperation.Request.Options?) -> StorageDownloadDataTask
+ /// Retrieve the object from storage into memory.
+ ///
+ /// - Parameters:
+ /// - path: The path for the object in storage
+ /// - options: Options to adjust the behavior of this request, including plugin-options
+ /// - Returns: A task that provides progress updates and the key which was used to download
+ ///
+ /// - Tag: StorageCategoryBehavior.downloadData
+ func downloadData(
+ path: any StoragePath,
+ options: StorageDownloadDataOperation.Request.Options?
+ ) -> StorageDownloadDataTask
+
/// Download to file the object from storage.
///
/// - Parameters:
@@ -47,10 +78,29 @@ public protocol StorageCategoryBehavior {
/// - Returns: A task that provides progress updates and the key which was used to download
///
/// - Tag: StorageCategoryBehavior.downloadFile
+ @available(*, deprecated, message: "Use downloadFile(path:options:)")
@discardableResult
- func downloadFile(key: String,
- local: URL,
- options: StorageDownloadFileOperation.Request.Options?) -> StorageDownloadFileTask
+ func downloadFile(
+ key: String,
+ local: URL,
+ options: StorageDownloadFileOperation.Request.Options?
+ ) -> StorageDownloadFileTask
+
+ /// Download to file the object from storage.
+ ///
+ /// - Parameters:
+ /// - path: The path for the object in storage.
+ /// - local: The local file to download destination
+ /// - options: Parameters to specific plugin behavior
+ /// - Returns: A task that provides progress updates and the key which was used to download
+ ///
+ /// - Tag: StorageCategoryBehavior.downloadFile
+ @discardableResult
+ func downloadFile(
+ path: any StoragePath,
+ local: URL,
+ options: StorageDownloadFileOperation.Request.Options?
+ ) -> StorageDownloadFileTask
/// Upload data to storage
///
@@ -61,10 +111,29 @@ public protocol StorageCategoryBehavior {
/// - Returns: A task that provides progress updates and the key which was used to upload
///
/// - Tag: StorageCategoryBehavior.uploadData
+ @available(*, deprecated, message: "Use uploadData(path:options:)")
@discardableResult
- func uploadData(key: String,
- data: Data,
- options: StorageUploadDataOperation.Request.Options?) -> StorageUploadDataTask
+ func uploadData(
+ key: String,
+ data: Data,
+ options: StorageUploadDataOperation.Request.Options?
+ ) -> StorageUploadDataTask
+
+ /// Upload data to storage
+ ///
+ /// - Parameters:
+ /// - path: The path of the object in storage.
+ /// - data: The data in memory to be uploaded
+ /// - options: Parameters to specific plugin behavior
+ /// - Returns: A task that provides progress updates and the key which was used to upload
+ ///
+ /// - Tag: StorageCategoryBehavior.uploadData
+ @discardableResult
+ func uploadData(
+ path: any StoragePath,
+ data: Data,
+ options: StorageUploadDataOperation.Request.Options?
+ ) -> StorageUploadDataTask
/// Upload local file to storage
///
@@ -75,10 +144,29 @@ public protocol StorageCategoryBehavior {
/// - Returns: A task that provides progress updates and the key which was used to upload
///
/// - Tag: StorageCategoryBehavior.uploadFile
+ @available(*, deprecated, message: "Use uploadFile(path:options:)")
+ @discardableResult
+ func uploadFile(
+ key: String,
+ local: URL,
+ options: StorageUploadFileOperation.Request.Options?
+ ) -> StorageUploadFileTask
+
+ /// Upload local file to storage
+ ///
+ /// - Parameters:
+ /// - path: The path of the object in storage.
+ /// - local: The path to a local file.
+ /// - options: Parameters to specific plugin behavior
+ /// - Returns: A task that provides progress updates and the key which was used to upload
+ ///
+ /// - Tag: StorageCategoryBehavior.uploadFile
@discardableResult
- func uploadFile(key: String,
- local: URL,
- options: StorageUploadFileOperation.Request.Options?) -> StorageUploadFileTask
+ func uploadFile(
+ path: any StoragePath,
+ local: URL,
+ options: StorageUploadFileOperation.Request.Options?
+ ) -> StorageUploadFileTask
/// Delete object from storage
///
@@ -88,21 +176,52 @@ public protocol StorageCategoryBehavior {
/// - Returns: An operation object that provides notifications and actions related to the execution of the work
///
/// - Tag: StorageCategoryBehavior.remove
+ @available(*, deprecated, message: "Use remove(path:options:)")
+ @discardableResult
+ func remove(
+ key: String,
+ options: StorageRemoveOperation.Request.Options?
+ ) async throws -> String
+
+ /// Delete object from storage
+ ///
+ /// - Parameters:
+ /// - path: The path of the object in storage.
+ /// - options: Parameters to specific plugin behavior
+ /// - Returns: An operation object that provides notifications and actions related to the execution of the work
+ ///
+ /// - Tag: StorageCategoryBehavior.remove
@discardableResult
- func remove(key: String,
- options: StorageRemoveOperation.Request.Options?) async throws -> String
+ func remove(
+ path: any StoragePath,
+ options: StorageRemoveOperation.Request.Options?
+ ) async throws -> String
/// List the object identifiers under the hierarchy specified by the path, relative to access level, from storage
///
/// - Parameters:
/// - options: Parameters to specific plugin behavior
- /// - resultListener: Triggered when the list is complete
/// - Returns: An operation object that provides notifications and actions related to the execution of the work
///
/// - Tag: StorageCategoryBehavior.list
+ @available(*, deprecated, message: "Use list(path:options:)")
@discardableResult
func list(options: StorageListOperation.Request.Options?) async throws -> StorageListResult
+ /// List the object identifiers under the hierarchy specified by the path, relative to access level, from storage
+ ///
+ /// - Parameters:
+ /// - path: The path of the object in storage.
+ /// - options: Parameters to specific plugin behavior
+ /// - Returns: An operation object that provides notifications and actions related to the execution of the work
+ ///
+ /// - Tag: StorageCategoryBehavior.list
+ @discardableResult
+ func list(
+ path: any StoragePath,
+ options: StorageListOperation.Request.Options?
+ ) async throws -> StorageListResult
+
/// Handles background events which are related to URLSession
/// - Parameter identifier: identifier
/// - Returns: returns true if the identifier is handled by Amplify
diff --git a/Amplify/Categories/Storage/StoragePath.swift b/Amplify/Categories/Storage/StoragePath.swift
new file mode 100644
index 0000000000..b3cf55867a
--- /dev/null
+++ b/Amplify/Categories/Storage/StoragePath.swift
@@ -0,0 +1,45 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import Foundation
+
+public typealias IdentityIDPathResolver = (String) -> String
+
+/// Protocol that provides a closure to resolve the storage path.
+///
+/// - Tag: StoragePath
+public protocol StoragePath {
+ associatedtype Input
+ var resolve: (Input) -> String { get }
+}
+
+public extension StoragePath where Self == StringStoragePath {
+ static func fromString(_ path: String) -> Self {
+ return StringStoragePath(resolve: { _ in return path })
+ }
+}
+
+public extension StoragePath where Self == IdentityIDStoragePath {
+ static func fromIdentityID(_ identityIdPathResolver: @escaping IdentityIDPathResolver) -> Self {
+ return IdentityIDStoragePath(resolve: identityIdPathResolver)
+ }
+}
+
+/// Conforms to StoragePath protocol. Provides a storage path based on a string storage path.
+///
+/// - Tag: StringStoragePath
+public struct StringStoragePath: StoragePath {
+ public let resolve: (String) -> String
+}
+
+/// Conforms to StoragePath protocol.
+/// Provides a storage path constructed from an unique identity identifer.
+///
+/// - Tag: IdentityStoragePath
+public struct IdentityIDStoragePath: StoragePath {
+ public let resolve: IdentityIDPathResolver
+}
diff --git a/Amplify/Core/Support/AmplifyTaskExecution.swift b/Amplify/Core/Support/AmplifyTaskExecution.swift
new file mode 100644
index 0000000000..ff73c60f26
--- /dev/null
+++ b/Amplify/Core/Support/AmplifyTaskExecution.swift
@@ -0,0 +1,71 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+import Foundation
+
+/// Task that supports hub with execution of a single unit of work. .
+///
+/// See Also: [AmplifyTask](x-source-tag://AmplifyTask)
+///
+/// - Tag: AmplifyTaskExecution
+public protocol AmplifyTaskExecution {
+
+ associatedtype Success
+ associatedtype Request
+ associatedtype Failure: AmplifyError
+
+ typealias AmplifyTaskExecutionResult = Result
+
+ /// Blocks until the receiver has successfully collected a result or throws if an error was encountered.
+ ///
+ /// - Tag: AmplifyTaskExecution.value
+ var value: Success { get async throws }
+
+ /// Hub event name for the task
+ ///
+ /// - Tag: AmplifyTaskExecution.eventName
+ var eventName: HubPayloadEventName { get }
+
+ /// Category for which the Hub event would be dispatched for.
+ ///
+ /// - Tag: AmplifyTaskExecution.eventNameCategoryType
+ var eventNameCategoryType: CategoryType { get }
+
+ /// Executes work represented by the receiver.
+ ///
+ /// - Tag: AmplifyTaskExecution.execute
+ func execute() async throws -> Success
+
+ /// Dispatches a hub event.
+ ///
+ /// - Tag: AmplifyTaskExecution.dispatch
+ func dispatch(result: AmplifyTaskExecutionResult)
+
+}
+
+public extension AmplifyTaskExecution where Self: DefaultLogger {
+ var value: Success {
+ get async throws {
+ do {
+ log.info("Starting execution for \(eventName)")
+ let valueReturned = try await execute()
+ log.info("Successfully completed execution for \(eventName) with result:\n\(valueReturned)")
+ dispatch(result: .success(valueReturned))
+ return valueReturned
+ } catch let error as Failure {
+ log.error("Failed execution for \(eventName) with error:\n\(error)")
+ dispatch(result: .failure(error))
+ throw error
+ }
+ }
+ }
+
+ func dispatch(result: AmplifyTaskExecutionResult) {
+ let channel = HubChannel(from: eventNameCategoryType)
+ let payload = HubPayload(eventName: eventName, context: nil, data: result)
+ Amplify.Hub.dispatch(to: channel, payload: payload)
+ }
+}
diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift
index 42f647f95e..877571243c 100644
--- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift
+++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift
@@ -220,7 +220,7 @@ struct ConfigurationHelper {
AuthPluginErrorConstants.configurationMissingError
)
}
- let userPoolConfig = try parseUserPoolData(config)
+ let userPoolConfig = parseUserPoolData(config)
let identityPoolConfig = parseIdentityPoolData(config)
return try createAuthConfiguration(userPoolConfig: userPoolConfig,
@@ -281,11 +281,10 @@ struct ConfigurationHelper {
"verificationMechanism": .array(verificationMechanisms)])
}
- return JSONValue.object(
- ["auth": .object(
- ["plugins": .object(
- ["awsCognitoAuthPlugin": .object(
- ["Auth": .object(
- ["Default": authConfigObject])])])])])
+ return JSONValue.object([
+ "Auth": .object([
+ "Default": authConfigObject
+ ])
+ ])
}
}
diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift
index 812cb05b7c..d2f5f732ff 100644
--- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift
+++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift
@@ -246,7 +246,7 @@ final class ConfigurationHelperTests: XCTestCase {
]))
let json = ConfigurationHelper.createUserPoolJsonConfiguration(config)
- guard let authConfig = json.auth?.plugins?.awsCognitoAuthPlugin?.Auth?.Default else {
+ guard let authConfig = json.Auth?.Default else {
XCTFail("Could not retrieve auth configuration from json")
return
}
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift
index 80167b691d..a960ec1e07 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift
@@ -45,6 +45,34 @@ extension AWSS3StoragePlugin {
return result
}
+ public func getURL(
+ path: any StoragePath,
+ options: StorageGetURLOperation.Request.Options? = nil
+ ) async throws -> URL {
+ let options = options ?? StorageGetURLRequest.Options()
+ let request = StorageGetURLRequest(path: path, options: options)
+ let task = AWSS3StorageGetURLTask(
+ request,
+ storageBehaviour: storageService)
+ return try await task.value
+ }
+
+ public func downloadData(
+ path: any StoragePath,
+ options: StorageDownloadDataOperation.Request.Options? = nil
+ ) -> StorageDownloadDataTask {
+ let options = options ?? StorageDownloadDataRequest.Options()
+ let request = StorageDownloadDataRequest(path: path, options: options)
+ let operation = AWSS3StorageDownloadDataOperation(request,
+ storageConfiguration: storageConfiguration,
+ storageService: storageService,
+ authService: authService)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ queue.addOperation(operation)
+
+ return taskAdapter
+ }
+
@discardableResult
public func downloadData(
key: String,
@@ -80,6 +108,24 @@ extension AWSS3StoragePlugin {
return taskAdapter
}
+ @discardableResult
+ public func downloadFile(
+ path: any StoragePath,
+ local: URL,
+ options: StorageDownloadFileOperation.Request.Options? = nil
+ ) -> StorageDownloadFileTask {
+ let options = options ?? StorageDownloadFileRequest.Options()
+ let request = StorageDownloadFileRequest(path: path, local: local, options: options)
+ let operation = AWSS3StorageDownloadFileOperation(request,
+ storageConfiguration: storageConfiguration,
+ storageService: storageService,
+ authService: authService)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ queue.addOperation(operation)
+
+ return taskAdapter
+ }
+
@discardableResult
public func uploadData(
key: String,
@@ -98,6 +144,24 @@ extension AWSS3StoragePlugin {
return taskAdapter
}
+ @discardableResult
+ public func uploadData(
+ path: any StoragePath,
+ data: Data,
+ options: StorageUploadDataOperation.Request.Options? = nil
+ ) -> StorageUploadDataTask {
+ let options = options ?? StorageUploadDataRequest.Options()
+ let request = StorageUploadDataRequest(path: path, data: data, options: options)
+ let operation = AWSS3StorageUploadDataOperation(request,
+ storageConfiguration: storageConfiguration,
+ storageService: storageService,
+ authService: authService)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ queue.addOperation(operation)
+
+ return taskAdapter
+ }
+
@discardableResult
public func uploadFile(
key: String,
@@ -116,6 +180,24 @@ extension AWSS3StoragePlugin {
return taskAdapter
}
+ @discardableResult
+ public func uploadFile(
+ path: any StoragePath,
+ local: URL,
+ options: StorageUploadFileOperation.Request.Options? = nil
+ ) -> StorageUploadFileTask {
+ let options = options ?? StorageUploadFileRequest.Options()
+ let request = StorageUploadFileRequest(path: path, local: local, options: options)
+ let operation = AWSS3StorageUploadFileOperation(request,
+ storageConfiguration: storageConfiguration,
+ storageService: storageService,
+ authService: authService)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ queue.addOperation(operation)
+
+ return taskAdapter
+ }
+
@discardableResult
public func remove(
key: String,
@@ -133,6 +215,20 @@ extension AWSS3StoragePlugin {
return try await taskAdapter.value
}
+ @discardableResult
+ public func remove(
+ path: any StoragePath,
+ options: StorageRemoveOperation.Request.Options? = nil
+ ) async throws -> String {
+ let options = options ?? StorageRemoveRequest.Options()
+ let request = StorageRemoveRequest(path: path, options: options)
+ let task = AWSS3StorageRemoveTask(
+ request,
+ storageConfiguration: storageConfiguration,
+ storageBehaviour: storageService)
+ return try await task.value
+ }
+
public func list(
options: StorageListRequest.Options? = nil
) async throws -> StorageListResult {
@@ -148,6 +244,19 @@ extension AWSS3StoragePlugin {
return result
}
+ public func list(
+ path: any StoragePath,
+ options: StorageListRequest.Options? = nil
+ ) async throws -> StorageListResult {
+ let options = options ?? StorageListRequest.Options()
+ let request = StorageListRequest(path: path, options: options)
+ let task = AWSS3StorageListObjectsTask(
+ request,
+ storageConfiguration: storageConfiguration,
+ storageBehaviour: storageService)
+ return try await task.value
+ }
+
public func handleBackgroundEvents(identifier: String) async -> Bool {
await withCheckedContinuation { (continuation: CheckedContinuation) in
StorageBackgroundEventsRegistry.handleBackgroundEvents(identifier: identifier, continuation: continuation)
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin.swift
index cd029024a7..dc4bbe3f09 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin.swift
@@ -25,6 +25,7 @@ final public class AWSS3StoragePlugin: StorageCategoryPlugin {
var queue: OperationQueue!
/// The default access level used for API calls.
+ @available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
var defaultAccessLevel: StorageAccessLevel!
/// The unique key of the plugin within the storage category.
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3PluginPrefixResolver.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3PluginPrefixResolver.swift
index e26f2aee94..d7c970f15c 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3PluginPrefixResolver.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3PluginPrefixResolver.swift
@@ -12,6 +12,7 @@ import AWSPluginsCore
/// Resolves the final prefix prepended to the S3 key for a given request.
///
/// - Tag: AWSS3PluginPrefixResolver
+@available(*, deprecated)
public protocol AWSS3PluginPrefixResolver {
/// - Tag: AWSS3PluginPrefixResolver.resolvePrefix
func resolvePrefix(for accessLevel: StorageAccessLevel,
@@ -21,6 +22,7 @@ public protocol AWSS3PluginPrefixResolver {
/// Convenience resolver. Resolves the provided key as-is, with no manipulation
///
/// - Tag: PassThroughPrefixResolver
+@available(*, deprecated)
public struct PassThroughPrefixResolver: AWSS3PluginPrefixResolver {
public func resolvePrefix(for accessLevel: StorageAccessLevel,
targetIdentityId: String?) async throws -> String {
@@ -31,6 +33,7 @@ public struct PassThroughPrefixResolver: AWSS3PluginPrefixResolver {
/// AWSS3StoragePlugin default logic
///
/// - Tag: StorageAccessLevelAwarePrefixResolver
+@available(*, deprecated)
struct StorageAccessLevelAwarePrefixResolver {
let authService: AWSAuthServiceBehavior
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3StoragePluginConfiguration.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3StoragePluginConfiguration.swift
index 0cc1ee3996..63ec95ba5e 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3StoragePluginConfiguration.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3StoragePluginConfiguration.swift
@@ -13,6 +13,7 @@ import Foundation
public struct AWSS3StoragePluginConfiguration {
/// - Tag: AWSS3StoragePluginConfiguration.prefixResolver
+ @available(*, deprecated)
public let prefixResolver: AWSS3PluginPrefixResolver?
/// - Tag: AWSS3StoragePluginConfiguration.init
@@ -21,6 +22,7 @@ public struct AWSS3StoragePluginConfiguration {
}
/// - Tag: AWSS3StoragePluginConfiguration.prefixResolverFunc
+ @available(*, deprecated, message: "Use `StoragePath` instead")
public static func prefixResolver(
_ prefixResolver: AWSS3PluginPrefixResolver) -> AWSS3StoragePluginConfiguration {
.init(prefixResolver: prefixResolver)
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Error/AWSS3+StorageErrorConvertible.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Error/AWSS3+StorageErrorConvertible.swift
index 7b4a08ab76..8b1e7e316e 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Error/AWSS3+StorageErrorConvertible.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Error/AWSS3+StorageErrorConvertible.swift
@@ -14,7 +14,7 @@ extension AWSS3.NoSuchBucket: StorageErrorConvertible {
var storageError: StorageError {
.service(
"The specific bucket does not exist",
- "",
+ "Make sure the bucket exists",
self
)
}
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageDownloadDataOperation.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageDownloadDataOperation.swift
index 4df8672f9d..2e56183048 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageDownloadDataOperation.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageDownloadDataOperation.swift
@@ -84,13 +84,18 @@ class AWSS3StorageDownloadDataOperation: AmplifyInProcessReportingOperation<
return
}
- let prefixResolver = storageConfiguration.prefixResolver ??
- StorageAccessLevelAwarePrefixResolver(authService: authService)
-
Task {
do {
- let prefix = try await prefixResolver.resolvePrefix(for: request.options.accessLevel, targetIdentityId: request.options.targetIdentityId)
- let serviceKey = prefix + request.key
+ let serviceKey: String
+ if let path = request.path {
+ serviceKey = try await path.resolvePath(authService: self.authService)
+ } else {
+ let prefixResolver = storageConfiguration.prefixResolver ??
+ StorageAccessLevelAwarePrefixResolver(authService: authService)
+ let prefix = try await prefixResolver.resolvePrefix(for: request.options.accessLevel, targetIdentityId: request.options.targetIdentityId)
+ serviceKey = prefix + request.key
+ }
+
let accelerate = try AWSS3PluginOptions.accelerateValue(pluginOptions: request.options.pluginOptions)
storageService.download(serviceKey: serviceKey, fileURL: nil, accelerate: accelerate) { [weak self] event in
self?.onServiceEvent(event: event)
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageDownloadFileOperation.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageDownloadFileOperation.swift
index 88616953c1..86d75956b6 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageDownloadFileOperation.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageDownloadFileOperation.swift
@@ -27,7 +27,6 @@ class AWSS3StorageDownloadFileOperation: AmplifyInProcessReportingOperation<
let storageConfiguration: AWSS3StoragePluginConfiguration
let storageService: AWSS3StorageServiceBehavior
let authService: AWSAuthServiceBehavior
-
var storageTaskReference: StorageTaskReference?
// Serial queue for synchronizing access to `storageTaskReference`.
@@ -38,7 +37,8 @@ class AWSS3StorageDownloadFileOperation: AmplifyInProcessReportingOperation<
storageService: AWSS3StorageServiceBehavior,
authService: AWSAuthServiceBehavior,
progressListener: InProcessListener? = nil,
- resultListener: ResultListener? = nil) {
+ resultListener: ResultListener? = nil
+ ) {
self.storageConfiguration = storageConfiguration
self.storageService = storageService
@@ -87,15 +87,23 @@ class AWSS3StorageDownloadFileOperation: AmplifyInProcessReportingOperation<
return
}
- let prefixResolver = storageConfiguration.prefixResolver ??
- StorageAccessLevelAwarePrefixResolver(authService: authService)
-
Task {
do {
- let prefix = try await prefixResolver.resolvePrefix(for: request.options.accessLevel, targetIdentityId: request.options.targetIdentityId)
- let serviceKey = prefix + request.key
+ let serviceKey: String
+ if let path = request.path {
+ serviceKey = try await path.resolvePath(authService: authService)
+ } else {
+ let prefixResolver = storageConfiguration.prefixResolver ??
+ StorageAccessLevelAwarePrefixResolver(authService: authService)
+ let prefix = try await prefixResolver.resolvePrefix(for: request.options.accessLevel, targetIdentityId: request.options.targetIdentityId)
+ serviceKey = prefix + request.key
+ }
let accelerate = try AWSS3PluginOptions.accelerateValue(pluginOptions: request.options.pluginOptions)
- storageService.download(serviceKey: serviceKey, fileURL: self.request.local, accelerate: accelerate) { [weak self] event in
+ storageService.download(
+ serviceKey: serviceKey,
+ fileURL: self.request.local,
+ accelerate: accelerate
+ ) { [weak self] event in
self?.onServiceEvent(event: event)
}
} catch {
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadDataOperation.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadDataOperation.swift
index cbfd7ee52e..7dd70d4f28 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadDataOperation.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadDataOperation.swift
@@ -26,7 +26,7 @@ class AWSS3StorageUploadDataOperation: AmplifyInProcessReportingOperation<
let authService: AWSAuthServiceBehavior
var storageTaskReference: StorageTaskReference?
-
+ private var resolvedPath: String?
/// Serial queue for synchronizing access to `storageTaskReference`.
private let storageTaskActionQueue = DispatchQueue(label: "com.amazonaws.amplify.StorageTaskActionQueue")
@@ -84,13 +84,22 @@ class AWSS3StorageUploadDataOperation: AmplifyInProcessReportingOperation<
return
}
- let prefixResolver = storageConfiguration.prefixResolver ??
- StorageAccessLevelAwarePrefixResolver(authService: authService)
+
Task {
do {
- let prefix = try await prefixResolver.resolvePrefix(for: request.options.accessLevel, targetIdentityId: request.options.targetIdentityId)
- let serviceKey = prefix + request.key
+
+ let serviceKey: String
+ if let path = request.path {
+ serviceKey = try await path.resolvePath(authService: self.authService)
+ resolvedPath = serviceKey
+ } else {
+ let prefixResolver = storageConfiguration.prefixResolver ??
+ StorageAccessLevelAwarePrefixResolver(authService: authService)
+ let prefix = try await prefixResolver.resolvePrefix(for: request.options.accessLevel, targetIdentityId: request.options.targetIdentityId)
+ serviceKey = prefix + request.key
+ }
+
let accelerate = try AWSS3PluginOptions.accelerateValue(pluginOptions: request.options.pluginOptions)
if request.data.count > StorageUploadDataRequest.Options.multiPartUploadSizeThreshold {
storageService.multiPartUpload(
@@ -134,7 +143,11 @@ class AWSS3StorageUploadDataOperation: AmplifyInProcessReportingOperation<
case .inProcess(let progress):
dispatch(progress)
case .completed:
- dispatch(request.key)
+ if let path = resolvedPath {
+ dispatch(path)
+ } else {
+ dispatch(request.key)
+ }
finish()
case .failed(let error):
dispatch(error)
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadFileOperation.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadFileOperation.swift
index 617602388a..db8ced505c 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadFileOperation.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadFileOperation.swift
@@ -26,7 +26,7 @@ class AWSS3StorageUploadFileOperation: AmplifyInProcessReportingOperation<
let authService: AWSAuthServiceBehavior
var storageTaskReference: StorageTaskReference?
-
+ private var resolvedPath: String?
/// Serial queue for synchronizing access to `storageTaskReference`.
private let storageTaskActionQueue = DispatchQueue(label: "com.amazonaws.amplify.StorageTaskActionQueue")
@@ -108,13 +108,20 @@ class AWSS3StorageUploadFileOperation: AmplifyInProcessReportingOperation<
return
}
- let prefixResolver = storageConfiguration.prefixResolver ??
- StorageAccessLevelAwarePrefixResolver(authService: authService)
-
Task {
do {
- let prefix = try await prefixResolver.resolvePrefix(for: request.options.accessLevel, targetIdentityId: request.options.targetIdentityId)
- let serviceKey = prefix + request.key
+
+ let serviceKey: String
+ if let path = request.path {
+ serviceKey = try await path.resolvePath(authService: self.authService)
+ resolvedPath = serviceKey
+ } else {
+ let prefixResolver = storageConfiguration.prefixResolver ??
+ StorageAccessLevelAwarePrefixResolver(authService: authService)
+ let prefix = try await prefixResolver.resolvePrefix(for: request.options.accessLevel, targetIdentityId: request.options.targetIdentityId)
+ serviceKey = prefix + request.key
+ }
+
let accelerate = try AWSS3PluginOptions.accelerateValue(pluginOptions: request.options.pluginOptions)
if uploadSize > StorageUploadFileRequest.Options.multiPartUploadSizeThreshold {
storageService.multiPartUpload(
@@ -159,7 +166,11 @@ class AWSS3StorageUploadFileOperation: AmplifyInProcessReportingOperation<
case .inProcess(let progress):
dispatch(progress)
case .completed:
- dispatch(request.key)
+ if let path = resolvedPath {
+ dispatch(path)
+ } else {
+ dispatch(request.key)
+ }
finish()
case .failed(let error):
dispatch(error)
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageDownloadDataRequest+Validate.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageDownloadDataRequest+Validate.swift
index ef1ac970dc..61a8861712 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageDownloadDataRequest+Validate.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageDownloadDataRequest+Validate.swift
@@ -11,6 +11,11 @@ import Amplify
extension StorageDownloadDataRequest {
/// Performs client side validation and returns a `StorageError` for any validation failures.
func validate() -> StorageError? {
+ guard path == nil else {
+ // return nil here StoragePath are validated
+ // at during execution of request operation where the path is resolved
+ return nil
+ }
if let error = StorageRequestUtils.validateTargetIdentityId(options.targetIdentityId,
accessLevel: options.accessLevel) {
return error
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageDownloadFileRequest+Validate.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageDownloadFileRequest+Validate.swift
index 92706e9c79..a6174b8521 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageDownloadFileRequest+Validate.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageDownloadFileRequest+Validate.swift
@@ -11,6 +11,11 @@ import Amplify
extension StorageDownloadFileRequest {
/// Performs client side validation and returns a `StorageError` for any validation failures.
func validate() -> StorageError? {
+ guard path == nil else {
+ // return nil here StoragePath are validated
+ // at during execution of request operation where the path is resolved
+ return nil
+ }
if let error = StorageRequestUtils.validateTargetIdentityId(options.targetIdentityId,
accessLevel: options.accessLevel) {
return error
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageUploadDataRequest+Validate.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageUploadDataRequest+Validate.swift
index 4412186824..28eb48093f 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageUploadDataRequest+Validate.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageUploadDataRequest+Validate.swift
@@ -11,6 +11,12 @@ import Amplify
extension StorageUploadDataRequest {
/// Performs client side validation and returns a `StorageError` for any validation failures.
func validate() -> StorageError? {
+ guard path == nil else {
+ // return nil here StoragePath are validated
+ // at during execution of request operation where the path is resolved
+ return nil
+ }
+
if let error = StorageRequestUtils.validateKey(key) {
return error
}
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageUploadFileRequest+Validate.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageUploadFileRequest+Validate.swift
index abb99f1120..04185a8472 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageUploadFileRequest+Validate.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Request/StorageUploadFileRequest+Validate.swift
@@ -11,6 +11,12 @@ import Amplify
extension StorageUploadFileRequest {
/// Performs client side validation and returns a `StorageError` for any validation failures.
func validate() -> StorageError? {
+ guard path == nil else {
+ // return nil here StoragePath are validated
+ // at during execution of request operation where the path is resolved
+ return nil
+ }
+
if let error = StorageRequestUtils.validateKey(key) {
return error
}
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+GetPreSignedURLBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+GetPreSignedURLBehavior.swift
index fc3eb40699..755b2416cf 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+GetPreSignedURLBehavior.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+GetPreSignedURLBehavior.swift
@@ -21,7 +21,7 @@ extension AWSS3StorageService {
key: serviceKey,
signingOperation: signingOperation,
metadata: metadata,
- accelerate: nil,
+ accelerate: accelerate,
expires: Int64(expires)
)
}
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageServiceBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageServiceBehavior.swift
index 21ae5df171..6491546d8d 100644
--- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageServiceBehavior.swift
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageServiceBehavior.swift
@@ -33,6 +33,12 @@ protocol AWSS3StorageServiceBehavior {
typealias StorageServiceMultiPartUploadEvent =
StorageEvent
+
+ /// - Tag: AWSS3StorageService.client
+ var client: S3ClientProtocol { get }
+
+ var bucket: String! { get }
+
func reset()
func getEscapeHatch() -> S3Client
@@ -64,9 +70,11 @@ protocol AWSS3StorageServiceBehavior {
accelerate: Bool?,
onEvent: @escaping StorageServiceMultiPartUploadEventHandler)
+ @available(*, deprecated, message: "Use `AWSS3StorageListObjectsTask` instead")
func list(prefix: String,
options: StorageListRequest.Options) async throws -> StorageListResult
+ @available(*, deprecated, message: "Use `AWSS3StorageRemoveTask` instead")
func delete(serviceKey: String,
onEvent: @escaping StorageServiceDeleteEventHandler)
}
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift
new file mode 100644
index 0000000000..95c464cff2
--- /dev/null
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift
@@ -0,0 +1,50 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import Foundation
+import Amplify
+import AWSPluginsCore
+
+extension StoragePath {
+ func resolvePath(authService: AWSAuthServiceBehavior? = nil) async throws -> String {
+ if self is IdentityIDStoragePath {
+ let authService = authService ?? AWSAuthService()
+ guard let identityId = try await authService.getIdentityID() as? Input else {
+ throw StorageError.configuration(
+ "Unable to resolve identity id as a storage path input type",
+ "Please verify that storage is configured correctly",
+ nil
+ )
+ }
+ let path = resolve(identityId).trimmingCharacters(in: .whitespaces)
+ try validate(path)
+ return path
+ } else if self is StringStoragePath {
+ guard let input = "" as? Input else {
+ throw StorageError.unknown(
+ "Unable to resolve StringStoragePath resolver input",
+ nil
+ )
+ }
+ let path = resolve(input).trimmingCharacters(in: .whitespaces)
+ try validate(path)
+ return path
+ } else {
+ let errorDescription = "The StoragePath specified is not supported"
+ let recoverySuggestion = "Please specify a StoragePath from string or from identityID."
+ throw StorageError.validation("path", errorDescription, recoverySuggestion, nil)
+ }
+ }
+
+ func validate(_ path: String) throws {
+ if path.isEmpty || path.hasPrefix("/") {
+ let errorDescription = "Invalid StoragePath provided."
+ let recoverySuggestion = "StoragePath must not be empty or start with /"
+ throw StorageError.validation("path", errorDescription, recoverySuggestion, nil)
+ }
+ }
+}
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift
new file mode 100644
index 0000000000..2baadcf539
--- /dev/null
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift
@@ -0,0 +1,72 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import Amplify
+import Foundation
+import AWSS3
+import AWSPluginsCore
+
+protocol StorageListObjectsTask: AmplifyTaskExecution where Request == StorageListRequest, Success == StorageListResult, Failure == StorageError {}
+
+class AWSS3StorageListObjectsTask: StorageListObjectsTask, DefaultLogger {
+
+ let request: StorageListRequest
+ let storageConfiguration: AWSS3StoragePluginConfiguration
+ let storageBehaviour: AWSS3StorageServiceBehavior
+
+ init(_ request: StorageListRequest,
+ storageConfiguration: AWSS3StoragePluginConfiguration,
+ storageBehaviour: AWSS3StorageServiceBehavior) {
+ self.request = request
+ self.storageConfiguration = storageConfiguration
+ self.storageBehaviour = storageBehaviour
+ }
+
+ var eventName: HubPayloadEventName {
+ HubPayload.EventName.Storage.list
+ }
+
+ var eventNameCategoryType: CategoryType {
+ .storage
+ }
+
+ func execute() async throws -> StorageListResult {
+ guard let path = try await request.path?.resolvePath() else {
+ throw StorageError.validation(
+ "path",
+ "`path` is required for listing objects",
+ "Make sure that a valid `path` is passed for removing an object")
+ }
+ let input = ListObjectsV2Input(bucket: storageBehaviour.bucket,
+ continuationToken: request.options.nextToken,
+ delimiter: nil,
+ maxKeys: Int(request.options.pageSize),
+ prefix: path,
+ startAfter: nil)
+ do {
+ let response = try await storageBehaviour.client.listObjectsV2(input: input)
+ let contents: S3BucketContents = response.contents ?? []
+ let items = try contents.map { s3Object in
+ guard let path = s3Object.key else {
+ throw StorageError.unknown("Missing key in response")
+ }
+ return StorageListResult.Item(
+ path: path,
+ eTag: s3Object.eTag,
+ lastModified: s3Object.lastModified)
+ }
+ return StorageListResult(items: items, nextToken: response.nextContinuationToken)
+ } catch let error as StorageErrorConvertible {
+ throw error.storageError
+ } catch {
+ throw StorageError.service(
+ "Service error occurred.",
+ "Please inspect the underlying error for more details.",
+ error)
+ }
+ }
+}
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageRemoveTask.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageRemoveTask.swift
new file mode 100644
index 0000000000..33b4319db3
--- /dev/null
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageRemoveTask.swift
@@ -0,0 +1,59 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import Amplify
+import Foundation
+import AWSS3
+import AWSPluginsCore
+
+protocol StorageRemoveTask: AmplifyTaskExecution where Request == AWSS3DeleteObjectRequest, Success == String, Failure == StorageError {}
+
+class AWSS3StorageRemoveTask: StorageRemoveTask, DefaultLogger {
+
+ let request: StorageRemoveRequest
+ let storageConfiguration: AWSS3StoragePluginConfiguration
+ let storageBehaviour: AWSS3StorageServiceBehavior
+
+ init(_ request: StorageRemoveRequest,
+ storageConfiguration: AWSS3StoragePluginConfiguration,
+ storageBehaviour: AWSS3StorageServiceBehavior) {
+ self.request = request
+ self.storageConfiguration = storageConfiguration
+ self.storageBehaviour = storageBehaviour
+ }
+
+ var eventName: HubPayloadEventName {
+ HubPayload.EventName.Storage.remove
+ }
+
+ var eventNameCategoryType: CategoryType {
+ .storage
+ }
+
+ func execute() async throws -> String {
+ guard let serviceKey = try await request.path?.resolvePath() else {
+ throw StorageError.validation(
+ "path",
+ "`path` is required for removing an object",
+ "Make sure that a valid `path` is passed for removing an object")
+ }
+ let input = DeleteObjectInput(
+ bucket: storageBehaviour.bucket,
+ key: serviceKey)
+ do {
+ _ = try await storageBehaviour.client.deleteObject(input: input)
+ } catch let error as StorageErrorConvertible {
+ throw error.storageError
+ } catch let error {
+ throw StorageError.service(
+ "Service error occurred.",
+ "Please inspect the underlying error for more details.",
+ error)
+ }
+ return serviceKey
+ }
+}
diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3torageGetURLTask.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3torageGetURLTask.swift
new file mode 100644
index 0000000000..df803a3b71
--- /dev/null
+++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3torageGetURLTask.swift
@@ -0,0 +1,68 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import Amplify
+import Foundation
+import AWSS3
+import AWSPluginsCore
+
+protocol StorageGetURLTask: AmplifyTaskExecution where Request == StorageGetURLRequest, Success == URL, Failure == StorageError {}
+
+class AWSS3StorageGetURLTask: StorageGetURLTask, DefaultLogger {
+
+ let request: StorageGetURLRequest
+ let storageBehaviour: AWSS3StorageServiceBehavior
+
+ init(_ request: StorageGetURLRequest,
+ storageBehaviour: AWSS3StorageServiceBehavior) {
+ self.request = request
+ self.storageBehaviour = storageBehaviour
+ }
+
+ var eventName: HubPayloadEventName {
+ HubPayload.EventName.Storage.getURL
+ }
+
+ var eventNameCategoryType: CategoryType {
+ .storage
+ }
+
+ func execute() async throws -> URL {
+ guard let serviceKey = try await request.path?.resolvePath() else {
+ throw StorageError.validation(
+ "path",
+ "`path` is required field",
+ "Make sure that a valid `path` is passed for removing an object")
+ }
+
+ // Validate object if needed
+ if let pluginOptions = request.options.pluginOptions as? AWSStorageGetURLOptions, pluginOptions.validateObjectExistence {
+ try await storageBehaviour.validateObjectExistence(serviceKey: serviceKey)
+ }
+
+ let accelerate = try AWSS3PluginOptions.accelerateValue(
+ pluginOptions: request.options.pluginOptions)
+
+ do {
+ return try await storageBehaviour.getPreSignedURL(
+ serviceKey: serviceKey,
+ signingOperation: .getObject,
+ metadata: nil,
+ accelerate: accelerate,
+ expires: request.options.expires
+ )
+ } catch let error as StorageErrorConvertible {
+ throw error.storageError
+ } catch let error {
+ throw StorageError.service(
+ "Service error occurred.",
+ "Please inspect the underlying error for more details.",
+ error)
+ }
+
+ }
+}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockAWSS3StorageService.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockAWSS3StorageService.swift
index 2b70e3bc90..9249596f04 100644
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockAWSS3StorageService.swift
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockAWSS3StorageService.swift
@@ -55,6 +55,10 @@ public class MockAWSS3StorageService: AWSS3StorageServiceBehavior {
}
*/
+ public var client: any S3ClientProtocol = MockS3Client()
+
+ public var bucket: String! = "bucket"
+
public func reset() {
interactions.append(#function)
}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockS3Client.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockS3Client.swift
index a8c8f9f4b1..5fb4f71451 100644
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockS3Client.swift
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Mocks/MockS3Client.swift
@@ -28,6 +28,8 @@ final class MockS3Client {
var listObjectsV2Handler: (ListObjectsV2Input) async throws -> ListObjectsV2Output = { _ in throw ClientError.missingResult }
var headObjectHandler: (HeadObjectInput) async throws -> HeadObjectOutput = { _ in return HeadObjectOutput() }
+
+ var deleteObjectHandler: ((DeleteObjectInput) async throws -> DeleteObjectOutput)? = nil
}
extension MockS3Client: S3ClientProtocol {
@@ -111,7 +113,10 @@ extension MockS3Client: S3ClientProtocol {
}
func deleteObject(input: AWSS3.DeleteObjectInput) async throws -> AWSS3.DeleteObjectOutput {
- throw ClientError.missingImplementation
+ guard let deleteObjectHandler = deleteObjectHandler else {
+ throw ClientError.missingImplementation
+ }
+ return try await deleteObjectHandler(input)
}
func deleteObjects(input: AWSS3.DeleteObjectsInput) async throws -> AWSS3.DeleteObjectsOutput {
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift
index b87d1cd939..9b1b6b6d65 100644
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift
@@ -182,5 +182,212 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase {
mockStorageService.verifyDownload(serviceKey: expectedServiceKey, fileURL: url)
}
+ /// Given: Storage Download File Operation
+ /// When: The operation is executed with a request that has an invalid StringStoragePath
+ /// Then: The operation will fail with a validation error
+ func testDownloadFileOperationStringStoragePathValidationError() {
+ let path = StringStoragePath(resolve: { _ in return "/my/path" })
+ let request = StorageDownloadFileRequest(path: path,
+ local: testURL,
+ options: StorageDownloadFileRequest.Options())
+
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageDownloadFileOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Download File Operation
+ /// When: The operation is executed with a request that has an invalid StringStoragePath
+ /// Then: The operation will fail with a validation error
+ func testDownloadFileOperationEmptyStoragePathValidationError() {
+ let path = StringStoragePath(resolve: { _ in return " " })
+ let request = StorageDownloadFileRequest(path: path,
+ local: testURL,
+ options: StorageDownloadFileRequest.Options())
+
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageDownloadFileOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Download File Operation
+ /// When: The operation is executed with a request that has an invalid IdentityIDStoragePath
+ /// Then: The operation will fail with a validation error
+ func testDownloadFileOperationIdentityIDStoragePathValidationError() {
+ let path = IdentityIDStoragePath(resolve: { _ in return "/my/path" })
+ let request = StorageDownloadFileRequest(path: path,
+ local: testURL,
+ options: StorageDownloadFileRequest.Options())
+
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageDownloadFileOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Download File Operation
+ /// When: The operation is executed with a request that has an a custom implementation of StoragePath
+ /// Then: The operation will fail with a validation error
+ func testDownloadFileOperationCustomStoragePathValidationError() {
+ let path = InvalidCustomStoragePath(resolve: { _ in return "my/path" })
+ let request = StorageDownloadFileRequest(path: path,
+ local: testURL,
+ options: StorageDownloadFileRequest.Options())
+
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageDownloadFileOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Download File Operation
+ /// When: The operation is executed with a request that has an valid StringStoragePath
+ /// Then: The operation will succeed
+ func testDownloadFileOperationWithStringStoragePathSucceeds() async throws {
+ let path = StringStoragePath(resolve: { _ in return "public/\(self.testKey)" })
+ let task = StorageTransferTask(transferType: .download(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceDownloadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completed(nil)]
+ let url = URL(fileURLWithPath: "path")
+ let request = StorageDownloadFileRequest(path: path,
+ local: testURL,
+ options: StorageDownloadFileRequest.Options())
+ let inProcessInvoked = expectation(description: "inProgress was invoked on operation")
+ let completeInvoked = expectation(description: "complete was invoked on operation")
+ let operation = AWSS3StorageDownloadFileOperation(
+ request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: { _ in
+ inProcessInvoked.fulfill()
+ }, resultListener: { result in
+ switch result {
+ case .success:
+ completeInvoked.fulfill()
+ case .failure(let error):
+ XCTFail("Unexpected error on operation: \(error)")
+ }
+ })
+
+ operation.start()
+
+ await waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ mockStorageService.verifyDownload(serviceKey: "public/\(self.testKey)", fileURL: url)
+ }
+
+ /// Given: Storage Download File Operation
+ /// When: The operation is executed with a request that has an valid IdentityIDStoragePath
+ /// Then: The operation will succeed
+ func testDownloadFileOperationWithIdentityIDStoragePathSucceeds() async throws {
+ mockAuthService.identityId = testIdentityId
+ let path = IdentityIDStoragePath(resolve: { id in return "public/\(id)/\(self.testKey)" })
+ let task = StorageTransferTask(transferType: .download(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceDownloadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completed(nil)]
+ let url = URL(fileURLWithPath: "path")
+ let request = StorageDownloadFileRequest(path: path,
+ local: testURL,
+ options: StorageDownloadFileRequest.Options())
+ let inProcessInvoked = expectation(description: "inProgress was invoked on operation")
+ let completeInvoked = expectation(description: "complete was invoked on operation")
+ let operation = AWSS3StorageDownloadFileOperation(
+ request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: { _ in
+ inProcessInvoked.fulfill()
+ }, resultListener: { result in
+ switch result {
+ case .success:
+ completeInvoked.fulfill()
+ case .failure(let error):
+ XCTFail("Unexpected error on operation: \(error)")
+ }
+ })
+
+ operation.start()
+
+ await waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ mockStorageService.verifyDownload(serviceKey: "public/\(testIdentityId)/\(self.testKey)", fileURL: url)
+ }
+
// TODO: missing unit tests for pause resume and cancel. do we create a mock of the StorageTaskReference?
}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift
index 5acea18c20..6a7c7a5485 100644
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift
@@ -174,5 +174,202 @@ class AWSS3StorageDownloadDataOperationTests: AWSS3StorageOperationTestBase {
mockStorageService.verifyDownload(serviceKey: expectedServiceKey, fileURL: nil)
}
+ /// Given: Storage Download Data Operation
+ /// When: The operation is executed with a request that has an invalid StringStoragePath
+ /// Then: The operation will fail with a validation error
+ func testDownloadDataOperationStringStoragePathValidationError() {
+ let path = StringStoragePath(resolve: { _ in return "/my/path" })
+ let request = StorageDownloadDataRequest(path: path, options: StorageDownloadDataRequest.Options())
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageDownloadDataOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil
+ ) { event in
+ switch event {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Download Data Operation
+ /// When: The operation is executed with a request that has an invalid StringStoragePath
+ /// Then: The operation will fail with a validation error
+ func testDownloadDataOperationEmptyStoragePathValidationError() {
+ let path = StringStoragePath(resolve: { _ in return " " })
+ let request = StorageDownloadDataRequest(path: path, options: StorageDownloadDataRequest.Options())
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageDownloadDataOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil
+ ) { event in
+ switch event {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Download Data Operation
+ /// When: The operation is executed with a request that has an invalid IdentityIDStoragePath
+ /// Then: The operation will fail with a validation error
+ func testDownloadDataOperationIdentityIdStoragePathValidationError() {
+ let path = IdentityIDStoragePath(resolve: { _ in return "/my/path" })
+ let request = StorageDownloadDataRequest(path: path, options: StorageDownloadDataRequest.Options())
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageDownloadDataOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil) { event in
+ switch event {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Download Data Operation
+ /// When: The operation is executed with a request that has an a custom implementation of StoragePath
+ /// Then: The operation will fail with a validation error
+ func testDownloadDataOperationCustomStoragePathValidationError() {
+ let path = InvalidCustomStoragePath(resolve: { _ in return "my/path" })
+ let request = StorageDownloadDataRequest(path: path, options: StorageDownloadDataRequest.Options())
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageDownloadDataOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil) { event in
+ switch event {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Download Data Operation
+ /// When: The operation is executed with a request that has an valid StringStoragePath
+ /// Then: The operation will succeed
+ func testDownloadDataOperationWithStringStoragePathSucceeds() async throws {
+ let task = StorageTransferTask(transferType: .download(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceDownloadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completed(Data())]
+ let path = StringStoragePath(resolve: { _ in return "public/\(self.testKey)" })
+ let request = StorageDownloadDataRequest(path: path, options: StorageDownloadDataRequest.Options())
+
+ let inProcessInvoked = expectation(description: "inProgress was invoked on operation")
+ let completeInvoked = expectation(description: "complete was invoked on operation")
+ let operation = AWSS3StorageDownloadDataOperation(
+ request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: { _ in
+ inProcessInvoked.fulfill()
+ }, resultListener: { result in
+ switch result {
+ case .success:
+ completeInvoked.fulfill()
+ case .failure(let error):
+ XCTFail("Unexpected event invoked on operation: \(error)")
+ }
+ })
+
+ operation.start()
+
+ await fulfillment(of: [inProcessInvoked, completeInvoked], timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ mockStorageService.verifyDownload(serviceKey: "public/\(self.testKey)", fileURL: nil)
+ }
+
+ /// Given: Storage Download Data Operation
+ /// When: The operation is executed with a request that has an valid IdentityIDStoragePath
+ /// Then: The operation will succeed
+ func testDownloadDataOperationWithIdentityIDStoragePathSucceeds() async throws {
+ mockAuthService.identityId = testIdentityId
+ let task = StorageTransferTask(transferType: .download(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceDownloadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completed(Data())]
+ let path = IdentityIDStoragePath(resolve: { id in return "public/\(id)/\(self.testKey)" })
+ let request = StorageDownloadDataRequest(path: path, options: StorageDownloadDataRequest.Options())
+
+ let inProcessInvoked = expectation(description: "inProgress was invoked on operation")
+ let completeInvoked = expectation(description: "complete was invoked on operation")
+ let operation = AWSS3StorageDownloadDataOperation(
+ request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: { _ in
+ inProcessInvoked.fulfill()
+ }, resultListener: { result in
+ switch result {
+ case .success:
+ completeInvoked.fulfill()
+ case .failure(let error):
+ XCTFail("Unexpected event invoked on operation: \(error)")
+ }
+ })
+
+ operation.start()
+
+ await fulfillment(of: [inProcessInvoked, completeInvoked], timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ mockStorageService.verifyDownload(serviceKey: "public/\(testIdentityId)/\(self.testKey)", fileURL: nil)
+ }
+
// TODO: missing unit tets for pause resume and cancel. do we create a mock of the StorageTaskReference?
}
+
+struct InvalidCustomStoragePath: StoragePath {
+ var resolve: (String) -> String
+}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift
index c93b6975f3..5934e419d3 100644
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift
@@ -219,5 +219,227 @@ class AWSS3StorageUploadDataOperationTests: AWSS3StorageOperationTestBase {
metadata: metadata)
}
+ /// Given: Storage Upload Data Operation
+ /// When: The operation is executed with a request that has an invalid StringStoragePath
+ /// Then: The operation will fail with a validation error
+ func testUploadDataOperationStringStoragePathValidationError() {
+ let path = StringStoragePath(resolve: { _ in return "/my/path" })
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let options = StorageUploadDataRequest.Options(accessLevel: .protected)
+ let request = StorageUploadDataRequest(path: path, data: testData, options: options)
+ let operation = AWSS3StorageUploadDataOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil
+ ) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Upload Data Operation
+ /// When: The operation is executed with a request that has an invalid StringStoragePath
+ /// Then: The operation will fail with a validation error
+ func testUploadDataOperationEmptyStoragePathValidationError() {
+ let path = StringStoragePath(resolve: { _ in return " " })
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let options = StorageUploadDataRequest.Options(accessLevel: .protected)
+ let request = StorageUploadDataRequest(path: path, data: testData, options: options)
+ let operation = AWSS3StorageUploadDataOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil
+ ) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Upload Data Operation
+ /// When: The operation is executed with a request that has an invalid IdentityIDStoragePath
+ /// Then: The operation will fail with a validation error
+ func testUploadDataOperationIdentityIDStoragePathValidationError() {
+ let path = IdentityIDStoragePath(resolve: { _ in return "/my/path" })
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let options = StorageUploadDataRequest.Options(accessLevel: .protected)
+ let request = StorageUploadDataRequest(path: path, data: testData, options: options)
+ let operation = AWSS3StorageUploadDataOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil
+ ) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Upload Data Operation
+ /// When: The operation is executed with a request that has an a custom implementation of StoragePath
+ /// Then: The operation will fail with a validation error
+ func testUploadDataOperationCustomStoragePathValidationError() {
+ let path = InvalidCustomStoragePath(resolve: { _ in return "my/path" })
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let options = StorageUploadDataRequest.Options(accessLevel: .protected)
+ let request = StorageUploadDataRequest(path: path, data: testData, options: options)
+ let operation = AWSS3StorageUploadDataOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil
+ ) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Upload Data Operation
+ /// When: The operation is executed with a request that has an valid StringStoragePath
+ /// Then: The operation will succeed
+ func testUploadDataOperationWithStringStoragePathSucceeds() async throws {
+ let path = StringStoragePath(resolve: { _ in return "public/\(self.testKey)" })
+ let task = StorageTransferTask(transferType: .upload(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceUploadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completedVoid]
+
+ let expectedUploadSource = UploadSource.data(testData)
+ let metadata = ["mykey": "Value"]
+
+ let options = StorageUploadDataRequest.Options(accessLevel: .protected,
+ metadata: metadata,
+ contentType: testContentType)
+ let request = StorageUploadDataRequest(path: path, data: testData, options: options)
+
+ let inProcessInvoked = expectation(description: "inProgress was invoked on operation")
+ let completeInvoked = expectation(description: "complete was invoked on operation")
+ let operation = AWSS3StorageUploadDataOperation(
+ request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: { _ in
+ inProcessInvoked.fulfill()
+ }, resultListener: { result in
+ switch result {
+ case .success:
+ completeInvoked.fulfill()
+ default:
+ XCTFail("Should have received completed event")
+ }
+ })
+
+ operation.start()
+
+ await waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ XCTAssertEqual(mockStorageService.uploadCalled, 1)
+ mockStorageService.verifyUpload(serviceKey: "public/\(self.testKey)",
+ key: testKey,
+ uploadSource: expectedUploadSource,
+ contentType: testContentType,
+ metadata: metadata)
+ }
+
+ /// Given: Storage UploadData Operation
+ /// When: The operation is executed with a request that has an valid IdentityIDStoragePath
+ /// Then: The operation will succeed
+ func testUploadDataOperationWithIdentityIDStoragePathSucceeds() async throws {
+ mockAuthService.identityId = testIdentityId
+ let path = IdentityIDStoragePath(resolve: { id in return "public/\(id)/\(self.testKey)" })
+ let task = StorageTransferTask(transferType: .upload(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceUploadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completedVoid]
+
+ let expectedUploadSource = UploadSource.data(testData)
+ let metadata = ["mykey": "Value"]
+
+ let options = StorageUploadDataRequest.Options(accessLevel: .protected,
+ metadata: metadata,
+ contentType: testContentType)
+ let request = StorageUploadDataRequest(path: path, data: testData, options: options)
+ let inProcessInvoked = expectation(description: "inProgress was invoked on operation")
+ let completeInvoked = expectation(description: "complete was invoked on operation")
+ let operation = AWSS3StorageUploadDataOperation(
+ request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: { _ in
+ inProcessInvoked.fulfill()
+ }, resultListener: { result in
+ switch result {
+ case .success:
+ completeInvoked.fulfill()
+ default:
+ XCTFail("Should have received completed event")
+ }
+ })
+
+ operation.start()
+
+ await waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ XCTAssertEqual(mockStorageService.uploadCalled, 1)
+ mockStorageService.verifyUpload(serviceKey: "public/\(testIdentityId)/\(testKey)",
+ key: testKey,
+ uploadSource: expectedUploadSource,
+ contentType: testContentType,
+ metadata: metadata)
+ }
+
// TODO: test pause, resume, canel, etc.
}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift
index 52800e958e..12374173bb 100644
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift
@@ -255,5 +255,294 @@ class AWSS3StorageUploadFileOperationTests: AWSS3StorageOperationTestBase {
metadata: metadata)
}
+ /// Given: Storage Upload File Operation
+ /// When: The operation is executed with a request that has an invalid StringStoragePath
+ /// Then: The operation will fail with a validation error
+ func testUploadFileOperationStringStoragePathValidationError() {
+ let path = StringStoragePath(resolve: { _ in return "/my/path" })
+ mockAuthService.identityId = testIdentityId
+ let task = StorageTransferTask(transferType: .upload(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceUploadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completedVoid]
+
+ let filePath = NSTemporaryDirectory() + UUID().uuidString + ".tmp"
+ let fileURL = URL(fileURLWithPath: filePath)
+ FileManager.default.createFile(atPath: filePath, contents: testData, attributes: nil)
+ let expectedUploadSource = UploadSource.local(fileURL)
+ let metadata = ["mykey": "Value"]
+
+ let options = StorageUploadFileRequest.Options(accessLevel: .protected,
+ metadata: metadata,
+ contentType: testContentType)
+ let request = StorageUploadFileRequest(path: path, local: fileURL, options: options)
+
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageUploadFileOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Upload File Operation
+ /// When: The operation is executed with a request that has an invalid StringStoragePath
+ /// Then: The operation will fail with a validation error
+ func testUploadFileOperationEmptyStoragePathValidationError() {
+ let path = StringStoragePath(resolve: { _ in return " " })
+ mockAuthService.identityId = testIdentityId
+ let task = StorageTransferTask(transferType: .upload(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceUploadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completedVoid]
+
+ let filePath = NSTemporaryDirectory() + UUID().uuidString + ".tmp"
+ let fileURL = URL(fileURLWithPath: filePath)
+ FileManager.default.createFile(atPath: filePath, contents: testData, attributes: nil)
+ let expectedUploadSource = UploadSource.local(fileURL)
+ let metadata = ["mykey": "Value"]
+
+ let options = StorageUploadFileRequest.Options(accessLevel: .protected,
+ metadata: metadata,
+ contentType: testContentType)
+ let request = StorageUploadFileRequest(path: path, local: fileURL, options: options)
+
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageUploadFileOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Upload File Operation
+ /// When: The operation is executed with a request that has an invalid IdentityIDStoragePath
+ /// Then: The operation will fail with a validation error
+ func testUploadFileOperationIdentityIDStoragePathValidationError() {
+ let path = IdentityIDStoragePath(resolve: { _ in return "/my/path" })
+ mockAuthService.identityId = testIdentityId
+ let task = StorageTransferTask(transferType: .upload(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceUploadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completedVoid]
+
+ let filePath = NSTemporaryDirectory() + UUID().uuidString + ".tmp"
+ let fileURL = URL(fileURLWithPath: filePath)
+ FileManager.default.createFile(atPath: filePath, contents: testData, attributes: nil)
+ let expectedUploadSource = UploadSource.local(fileURL)
+ let metadata = ["mykey": "Value"]
+
+ let options = StorageUploadFileRequest.Options(accessLevel: .protected,
+ metadata: metadata,
+ contentType: testContentType)
+ let request = StorageUploadFileRequest(path: path, local: fileURL, options: options)
+
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageUploadFileOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Download File Operation
+ /// When: The operation is executed with a request that has an a custom implementation of StoragePath
+ /// Then: The operation will fail with a validation error
+ func testUploadFileOperationCustomStoragePathValidationError() {
+ let path = InvalidCustomStoragePath(resolve: { _ in return "my/path" })
+ mockAuthService.identityId = testIdentityId
+ let task = StorageTransferTask(transferType: .upload(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceUploadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completedVoid]
+
+ let filePath = NSTemporaryDirectory() + UUID().uuidString + ".tmp"
+ let fileURL = URL(fileURLWithPath: filePath)
+ FileManager.default.createFile(atPath: filePath, contents: testData, attributes: nil)
+ let expectedUploadSource = UploadSource.local(fileURL)
+ let metadata = ["mykey": "Value"]
+
+ let options = StorageUploadFileRequest.Options(accessLevel: .protected,
+ metadata: metadata,
+ contentType: testContentType)
+ let request = StorageUploadFileRequest(path: path, local: fileURL, options: options)
+
+ let failedInvoked = expectation(description: "failed was invoked on operation")
+ let operation = AWSS3StorageUploadFileOperation(request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: nil) { result in
+ switch result {
+ case .failure(let error):
+ guard case .validation = error else {
+ XCTFail("Should have failed with validation error")
+ return
+ }
+ failedInvoked.fulfill()
+ default:
+ XCTFail("Should have received failed event")
+ }
+ }
+
+ operation.start()
+ waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ }
+
+ /// Given: Storage Download File Operation
+ /// When: The operation is executed with a request that has an valid StringStoragePath
+ /// Then: The operation will succeed
+ func testUploadFileOperationWithStringStoragePathSucceeds() async throws {
+ let path = StringStoragePath(resolve: { _ in return "public/\(self.testKey)" })
+ mockAuthService.identityId = testIdentityId
+ let task = StorageTransferTask(transferType: .upload(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceUploadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completedVoid]
+
+ let filePath = NSTemporaryDirectory() + UUID().uuidString + ".tmp"
+ let fileURL = URL(fileURLWithPath: filePath)
+ FileManager.default.createFile(atPath: filePath, contents: testData, attributes: nil)
+ let expectedUploadSource = UploadSource.local(fileURL)
+ let metadata = ["mykey": "Value"]
+
+ let options = StorageUploadFileRequest.Options(accessLevel: .protected,
+ metadata: metadata,
+ contentType: testContentType)
+ let request = StorageUploadFileRequest(path: path, local: fileURL, options: options)
+
+ let inProcessInvoked = expectation(description: "inProgress was invoked on operation")
+ let completeInvoked = expectation(description: "complete was invoked on operation")
+ let operation = AWSS3StorageUploadFileOperation(
+ request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: { _ in
+ inProcessInvoked.fulfill()
+ }, resultListener: { result in
+ switch result {
+ case .success:
+ completeInvoked.fulfill()
+ default:
+ XCTFail("Should have received completed event")
+ }
+ })
+
+ operation.start()
+
+ await waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ XCTAssertEqual(mockStorageService.uploadCalled, 1)
+ mockStorageService.verifyUpload(serviceKey: "public/\(testKey)",
+ key: testKey,
+ uploadSource: expectedUploadSource,
+ contentType: testContentType,
+ metadata: metadata)
+ }
+
+ /// Given: Storage Upload File Operation
+ /// When: The operation is executed with a request that has an valid IdentityIDStoragePath
+ /// Then: The operation will succeed
+ func testUploadFileOperationWithIdentityIDStoragePathSucceeds() async throws {
+ let path = IdentityIDStoragePath(resolve: { id in return "public/\(id)/\(self.testKey)" })
+ mockAuthService.identityId = testIdentityId
+ let task = StorageTransferTask(transferType: .upload(onEvent: { _ in }), bucket: "bucket", key: "key")
+ mockStorageService.storageServiceUploadEvents = [
+ StorageEvent.initiated(StorageTaskReference(task)),
+ StorageEvent.inProcess(Progress()),
+ StorageEvent.completedVoid]
+
+ let filePath = NSTemporaryDirectory() + UUID().uuidString + ".tmp"
+ let fileURL = URL(fileURLWithPath: filePath)
+ FileManager.default.createFile(atPath: filePath, contents: testData, attributes: nil)
+ let expectedUploadSource = UploadSource.local(fileURL)
+ let metadata = ["mykey": "Value"]
+
+ let options = StorageUploadFileRequest.Options(accessLevel: .protected,
+ metadata: metadata,
+ contentType: testContentType)
+ let request = StorageUploadFileRequest(path: path, local: fileURL, options: options)
+ let inProcessInvoked = expectation(description: "inProgress was invoked on operation")
+ let completeInvoked = expectation(description: "complete was invoked on operation")
+ let operation = AWSS3StorageUploadFileOperation(
+ request,
+ storageConfiguration: testStorageConfiguration,
+ storageService: mockStorageService,
+ authService: mockAuthService,
+ progressListener: { _ in
+ inProcessInvoked.fulfill()
+ }, resultListener: { result in
+ switch result {
+ case .success:
+ completeInvoked.fulfill()
+ default:
+ XCTFail("Should have received completed event")
+ }
+ })
+
+ operation.start()
+
+ await waitForExpectations(timeout: 1)
+ XCTAssertTrue(operation.isFinished)
+ XCTAssertEqual(mockStorageService.uploadCalled, 1)
+ mockStorageService.verifyUpload(serviceKey: "public/\(testIdentityId)/\(testKey)",
+ key: testKey,
+ uploadSource: expectedUploadSource,
+ contentType: testContentType,
+ metadata: metadata)
+ }
+
// TODO: test pause, resume, canel, etc.
}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageDownloadFileRequestTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageDownloadFileRequestTests.swift
index d04e4d466d..f66b40c2f9 100644
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageDownloadFileRequestTests.swift
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageDownloadFileRequestTests.swift
@@ -6,7 +6,7 @@
//
import XCTest
-import Amplify
+@testable import Amplify
@testable import AWSS3StoragePlugin
class StorageDownloadFileRequestTests: XCTestCase {
@@ -95,4 +95,20 @@ class StorageDownloadFileRequestTests: XCTestCase {
XCTAssertEqual(description, StorageErrorConstants.keyIsEmpty.errorDescription)
XCTAssertEqual(recovery, StorageErrorConstants.keyIsEmpty.recoverySuggestion)
}
+
+ /// Given: StorageDownloadFileRequest with an invalid StringStoragePath
+ /// When: Request validation is executed
+ /// Then: There is no error returned even though the storage path is invalid
+ /// There is no error because the path validation is done at operation execution time and not part of the request
+ func testValidateWithStoragePath() {
+ let options = StorageDownloadFileRequest.Options(accessLevel: .private,
+ targetIdentityId: "",
+ pluginOptions: testPluginOptions)
+ let path = StringStoragePath(resolve: {_ in "my/path"})
+ let request = StorageDownloadFileRequest(path: path, local: testURL, options: options)
+
+ let storageErrorOptional = request.validate()
+
+ XCTAssertNil(storageErrorOptional)
+ }
}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageGetDataRequestTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageGetDataRequestTests.swift
index 1aee240bb7..b09a29cea4 100644
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageGetDataRequestTests.swift
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageGetDataRequestTests.swift
@@ -6,7 +6,7 @@
//
import XCTest
-import Amplify
+@testable import Amplify
@testable import AWSS3StoragePlugin
class StorageDownloadDataRequestTests: XCTestCase {
@@ -94,4 +94,20 @@ class StorageDownloadDataRequestTests: XCTestCase {
XCTAssertEqual(description, StorageErrorConstants.keyIsEmpty.errorDescription)
XCTAssertEqual(recovery, StorageErrorConstants.keyIsEmpty.recoverySuggestion)
}
+
+ /// Given: StorageDownloadDataRequest with an invalid StringStoragePath
+ /// When: Request validation is executed
+ /// Then: There is no error returned even though the storage path is invalid
+ /// There is no error because the path validation is done at operation execution time and not part of the request
+ func testValidateWithStoragePath() {
+ let options = StorageDownloadDataRequest.Options(accessLevel: .private,
+ targetIdentityId: "",
+ pluginOptions: testPluginOptions)
+ let path = StringStoragePath(resolve: { input in return "my/path/"})
+ let request = StorageDownloadDataRequest(path: path, options: options)
+
+ let storageErrorOptional = request.validate()
+
+ XCTAssertNil(storageErrorOptional)
+ }
}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StoragePutDataRequestTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StoragePutDataRequestTests.swift
index 5fadb24165..377ca07deb 100644
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StoragePutDataRequestTests.swift
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StoragePutDataRequestTests.swift
@@ -6,7 +6,7 @@
//
import XCTest
-import Amplify
+@testable import Amplify
@testable import AWSS3StoragePlugin
class AWSS3StorageUploadDataRequestTests: XCTestCase {
@@ -103,6 +103,23 @@ class AWSS3StorageUploadDataRequestTests: XCTestCase {
XCTAssertEqual(recovery, StorageErrorConstants.metadataKeysInvalid.recoverySuggestion)
}
+ /// Given: StorageUploadDataRequest with an invalid StringStoragePath
+ /// When: Request validation is executed
+ /// Then: There is no error returned even though the storage path is invalid
+ /// There is no error because the path validation is done at operation execution time and not part of the request
+ func testValidateWithStoragePath() {
+ let path = StringStoragePath(resolve: {_ in "my/path"})
+ let options = StorageUploadDataRequest.Options(accessLevel: .protected,
+ metadata: testMetadata,
+ contentType: testContentType,
+ pluginOptions: testPluginOptions)
+ let request = StorageUploadDataRequest(path: path, data: testData, options: options)
+
+ let storageErrorOptional = request.validate()
+
+ XCTAssertNil(storageErrorOptional)
+ }
+
// TODO: testValidateMetadataValuesTooLarge
// func testValidateMetadataValuesTooLarge() {
//
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageUploadFileRequestTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageUploadFileRequestTests.swift
index 16ee59e0af..57bcd34447 100644
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageUploadFileRequestTests.swift
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageUploadFileRequestTests.swift
@@ -6,7 +6,7 @@
//
import XCTest
-import Amplify
+@testable import Amplify
@testable import AWSS3StoragePlugin
class AWSS3StorageUploadFileRequestTests: XCTestCase {
@@ -115,6 +115,26 @@ class AWSS3StorageUploadFileRequestTests: XCTestCase {
XCTAssertEqual(recovery, StorageErrorConstants.metadataKeysInvalid.recoverySuggestion)
}
+ /// Given: StorageUploadFileRequest with an invalid StringStoragePath
+ /// When: Request validation is executed
+ /// Then: There is no error returned even though the storage path is invalid
+ /// There is no error because the path validation is done at operation execution time and not part of the request
+ func testValidateWithStoragePath() {
+ let path = StringStoragePath(resolve: {_ in "my/path"})
+ let filePath = NSTemporaryDirectory() + UUID().uuidString + ".tmp"
+ let fileURL = URL(fileURLWithPath: filePath)
+ FileManager.default.createFile(atPath: filePath, contents: testData, attributes: nil)
+ let options = StorageUploadFileRequest.Options(accessLevel: .protected,
+ metadata: testMetadata,
+ contentType: testContentType,
+ pluginOptions: testPluginOptions)
+ let request = StorageUploadFileRequest(path: path, local: fileURL, options: options)
+
+ let storageErrorOptional = request.validate()
+
+ XCTAssertNil(storageErrorOptional)
+ }
+
// TODO: testValidateMetadataValuesTooLarge
// func testValidateMetadataValuesTooLarge() {
//
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceConfigureTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceConfigureTests.swift
deleted file mode 100644
index 8cea4937f5..0000000000
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceConfigureTests.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-//
-// Copyright Amazon.com Inc. or its affiliates.
-// All Rights Reserved.
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-
-import XCTest
-
-class AWSS3StorageServiceConfigureTests: AWSS3StorageServiceTestBase {
- func testClassMustNotBeEmpty() {
- // Swift format crashes if a test class is empty
- }
-}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceDeleteBehaviorTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceDeleteBehaviorTests.swift
deleted file mode 100644
index 06c8a65ac3..0000000000
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceDeleteBehaviorTests.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-//
-// Copyright Amazon.com Inc. or its affiliates.
-// All Rights Reserved.
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-
-import XCTest
-
-class AWSS3StorageServiceDeleteBehaviorTests: AWSS3StorageServiceTestBase {
- func testClassMustNotBeEmpty() {
- // Swift format crashes if a test class is empty
- }
-}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceEscapeHatchBehaviorTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceEscapeHatchBehaviorTests.swift
deleted file mode 100644
index d47a94cf89..0000000000
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceEscapeHatchBehaviorTests.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-//
-// Copyright Amazon.com Inc. or its affiliates.
-// All Rights Reserved.
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-
-import XCTest
-
-// swiftlint:disable:next type_name
-class AWSS3StorageServiceEscapeHatchBehaviorTests: AWSS3StorageServiceTestBase {
- func testClassMustNotBeEmpty() {
- // Swift format crashes if a test class is empty
- }
-}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceListBehaviorTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceListBehaviorTests.swift
deleted file mode 100644
index 8cfa49ca95..0000000000
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceListBehaviorTests.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-//
-// Copyright Amazon.com Inc. or its affiliates.
-// All Rights Reserved.
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-
-import XCTest
-
-class AWSS3StorageServiceListBehaviorTests: AWSS3StorageServiceTestBase {
- func testClassMustNotBeEmpty() {
- // Swift format crashes if a test class is empty
- }
-}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceMultiPartUploadBehaviorTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceMultiPartUploadBehaviorTests.swift
deleted file mode 100644
index 4bc442d657..0000000000
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceMultiPartUploadBehaviorTests.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-//
-// Copyright Amazon.com Inc. or its affiliates.
-// All Rights Reserved.
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-
-import XCTest
-
-// swiftlint:disable:next type_name
-class AWSS3StorageServiceMultiPartUploadBehaviorTests: AWSS3StorageServiceTestBase {
- func testClassMustNotBeEmpty() {
- // Swift format crashes if a test class is empty
- }
-}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceTestBase.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceTestBase.swift
deleted file mode 100644
index d1085d922c..0000000000
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceTestBase.swift
+++ /dev/null
@@ -1,41 +0,0 @@
-//
-// Copyright Amazon.com Inc. or its affiliates.
-// All Rights Reserved.
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-
-import XCTest
-@testable import AWSS3StoragePlugin
-@testable import AmplifyTestCommon
-
-class AWSS3StorageServiceTestBase: XCTestCase {
- /*
- var mockTransferUtility: MockAWSS3TransferUtility!
- var mockPreSignedURLBuilder: MockAWSS3PreSignedURLBuilder!
- var mockS3: MockS3!
-
- var storageService: AWSS3StorageService!
-
- var bucket = "bucket"
- var identifier = "identifier"
-
- override func setUp() {
- mockTransferUtility = MockAWSS3TransferUtility()
- mockPreSignedURLBuilder = MockAWSS3PreSignedURLBuilder()
- mockS3 = MockS3()
- storageService = AWSS3StorageService(transferUtility: mockTransferUtility,
- preSignedURLBuilder: mockPreSignedURLBuilder,
- awsS3: mockS3,
- bucket: bucket,
- identifier: identifier)
- }
-
- func testConfigure() {
-
- }
-
- func testReset() async {
- }
- */
-}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceUploadBehaviorTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceUploadBehaviorTests.swift
deleted file mode 100644
index 323ae435e5..0000000000
--- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceUploadBehaviorTests.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-//
-// Copyright Amazon.com Inc. or its affiliates.
-// All Rights Reserved.
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-
-import XCTest
-
-class AWSS3StorageServiceUploadBehaviorTests: AWSS3StorageServiceTestBase {
- func testClassMustNotBeEmpty() {
- // Swift format crashes if a test class is empty
- }
-}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift
new file mode 100644
index 0000000000..71f5ea6ea1
--- /dev/null
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift
@@ -0,0 +1,138 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import XCTest
+@testable import Amplify
+@testable import AmplifyTestCommon
+@testable import AWSPluginsCore
+@testable import AWSS3StoragePlugin
+@testable import AWSPluginsTestCommon
+import AWSS3
+
+class AWSS3StorageGetURLTaskTests: XCTestCase {
+
+
+ /// - Given: A configured Storage GetURL Task with mocked service
+ /// - When: AWSS3StorageGetURLTask value is invoked
+ /// - Then: A URL should be returned.
+ func testGetURLTaskSuccess() async throws {
+
+ let somePath = "path"
+ let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
+
+ let serviceMock = MockAWSS3StorageService()
+ serviceMock.getPreSignedURLHandler = { path, _, _ in
+ XCTAssertEqual(somePath, path)
+ return tempURL
+ }
+
+ let request = StorageGetURLRequest(
+ path: StringStoragePath.fromString(somePath), options: .init())
+ let task = AWSS3StorageGetURLTask(
+ request,
+ storageBehaviour: serviceMock)
+ let value = try await task.value
+ XCTAssertEqual(value, tempURL)
+ }
+
+ /// - Given: A configured Storage GetURL Task with mocked service, throwing `NotFound` exception
+ /// - When: AWSS3StorageGetURLTask value is invoked
+ /// - Then: A storage service error should be returned, with an underlying service error
+ func testGetURLTaskNoBucket() async throws {
+ let somePath = "path"
+
+ let serviceMock = MockAWSS3StorageService()
+ serviceMock.getPreSignedURLHandler = { _, _, _ in
+ throw AWSS3.NotFound()
+ }
+
+ let request = StorageGetURLRequest(
+ path: StringStoragePath.fromString(somePath), options: .init())
+ let task = AWSS3StorageGetURLTask(
+ request,
+ storageBehaviour: serviceMock)
+ do {
+ _ = try await task.value
+ XCTFail("Task should throw an exception")
+ }
+ catch {
+ guard let storageError = error as? StorageError,
+ case .service(_, _, let underlyingError) = storageError else {
+ XCTFail("Should throw a Storage service error, instead threw \(error)")
+ return
+ }
+ XCTAssertTrue(underlyingError is AWSS3.NotFound,
+ "Underlying error should be NoSuchKey, instead got \(String(describing: underlyingError))")
+ }
+ }
+
+ /// - Given: A configured Storage GetURL Task with invalid path
+ /// - When: AWSS3StorageGetURLTask value is invoked
+ /// - Then: A storage validation error should be returned
+ func testGetURLTaskWithInvalidPath() async throws {
+ let somePath = "/path"
+ let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
+
+ let serviceMock = MockAWSS3StorageService()
+ serviceMock.getPreSignedURLHandler = { path, _, _ in
+ XCTAssertEqual(somePath, path)
+ return tempURL
+ }
+
+ let request = StorageGetURLRequest(
+ path: StringStoragePath.fromString(somePath), options: .init())
+ let task = AWSS3StorageGetURLTask(
+ request,
+ storageBehaviour: serviceMock)
+ do {
+ _ = try await task.value
+ XCTFail("Task should throw an exception")
+ }
+ catch {
+ guard let storageError = error as? StorageError,
+ case .validation(let field, _, _, _) = storageError else {
+ XCTFail("Should throw a storage validation error, instead threw \(error)")
+ return
+ }
+
+ XCTAssertEqual(field, "path", "Field in error should be `path`")
+ }
+ }
+
+ /// - Given: A configured Storage GetURL Task with invalid path
+ /// - When: AWSS3StorageGetURLTask value is invoked
+ /// - Then: A storage validation error should be returned
+ func testGetURLTaskWithInvalidEmptyPath() async throws {
+ let emptyPath = " "
+ let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
+
+ let serviceMock = MockAWSS3StorageService()
+ serviceMock.getPreSignedURLHandler = { path, _, _ in
+ XCTAssertEqual(emptyPath, path)
+ return tempURL
+ }
+
+ let request = StorageGetURLRequest(
+ path: StringStoragePath.fromString(emptyPath), options: .init())
+ let task = AWSS3StorageGetURLTask(
+ request,
+ storageBehaviour: serviceMock)
+ do {
+ _ = try await task.value
+ XCTFail("Task should throw an exception")
+ }
+ catch {
+ guard let storageError = error as? StorageError,
+ case .validation(let field, _, _, _) = storageError else {
+ XCTFail("Should throw a storage validation error, instead threw \(error)")
+ return
+ }
+
+ XCTAssertEqual(field, "path", "Field in error should be `path`")
+ }
+ }
+}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift
new file mode 100644
index 0000000000..922f29974c
--- /dev/null
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift
@@ -0,0 +1,133 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import XCTest
+@testable import Amplify
+@testable import AmplifyTestCommon
+@testable import AWSPluginsCore
+@testable import AWSS3StoragePlugin
+@testable import AWSPluginsTestCommon
+import AWSS3
+
+class AWSS3StorageListObjectsTaskTests: XCTestCase {
+
+ /// - Given: A configured Storage List Objects Task with mocked service
+ /// - When: AWSS3StorageListObjectsTask value is invoked
+ /// - Then: A list of keys should be returned.
+ func testListObjectsTaskSuccess() async throws {
+ let serviceMock = MockAWSS3StorageService()
+ let client = serviceMock.client as! MockS3Client
+ client.listObjectsV2Handler = { input in
+ return .init(
+ contents: [
+ .init(eTag: "tag", key: "key", lastModified: Date()),
+ .init(eTag: "tag", key: "key", lastModified: Date())],
+ nextContinuationToken: "continuationToken"
+ )
+ }
+
+ let request = StorageListRequest(
+ path: StringStoragePath.fromString("path"), options: .init())
+ let task = AWSS3StorageListObjectsTask(
+ request,
+ storageConfiguration: AWSS3StoragePluginConfiguration(),
+ storageBehaviour: serviceMock)
+ let value = try await task.value
+ XCTAssertEqual(value.items.count, 2)
+ XCTAssertEqual(value.nextToken, "continuationToken")
+ XCTAssertEqual(value.items[0].eTag, "tag")
+ XCTAssertEqual(value.items[0].key, "key")
+ XCTAssertEqual(value.items[0].path, "key")
+ XCTAssertNotNil(value.items[0].lastModified)
+
+ }
+
+ /// - Given: A configured ListObjects Remove Task with mocked service, throwing `NoSuchKey` exception
+ /// - When: AWSS3StorageListObjectsTask value is invoked
+ /// - Then: A storage service error should be returned, with an underlying service error
+ func testListObjectsTaskNoBucket() async throws {
+ let serviceMock = MockAWSS3StorageService()
+ let client = serviceMock.client as! MockS3Client
+ client.listObjectsV2Handler = { input in
+ throw AWSS3.NoSuchKey()
+ }
+
+ let request = StorageListRequest(
+ path: StringStoragePath.fromString("path"), options: .init())
+ let task = AWSS3StorageListObjectsTask(
+ request,
+ storageConfiguration: AWSS3StoragePluginConfiguration(),
+ storageBehaviour: serviceMock)
+ do {
+ _ = try await task.value
+ XCTFail("Task should throw an exception")
+ }
+ catch {
+ guard let storageError = error as? StorageError,
+ case .service(_, _, let underlyingError) = storageError else {
+ XCTFail("Should throw a Storage service error, instead threw \(error)")
+ return
+ }
+ XCTAssertTrue(underlyingError is AWSS3.NoSuchKey,
+ "Underlying error should be NoSuchKey, instead got \(String(describing: underlyingError))")
+ }
+ }
+
+ /// - Given: A configured Storage ListObjects Task with invalid path
+ /// - When: AWSS3StorageListObjectsTask value is invoked
+ /// - Then: A storage validation error should be returned
+ func testListObjectsTaskWithInvalidPath() async throws {
+ let serviceMock = MockAWSS3StorageService()
+
+ let request = StorageListRequest(
+ path: StringStoragePath.fromString("/path"), options: .init())
+ let task = AWSS3StorageListObjectsTask(
+ request,
+ storageConfiguration: AWSS3StoragePluginConfiguration(),
+ storageBehaviour: serviceMock)
+ do {
+ _ = try await task.value
+ XCTFail("Task should throw an exception")
+ }
+ catch {
+ guard let storageError = error as? StorageError,
+ case .validation(let field, _, _, _) = storageError else {
+ XCTFail("Should throw a storage validation error, instead threw \(error)")
+ return
+ }
+
+ XCTAssertEqual(field, "path", "Field in error should be `path`")
+ }
+ }
+
+ /// - Given: A configured Storage ListObjects Task with invalid path
+ /// - When: AWSS3StorageListObjectsTask value is invoked
+ /// - Then: A storage validation error should be returned
+ func testListObjectsTaskWithInvalidEmptyPath() async throws {
+ let serviceMock = MockAWSS3StorageService()
+
+ let request = StorageListRequest(
+ path: StringStoragePath.fromString(" "), options: .init())
+ let task = AWSS3StorageListObjectsTask(
+ request,
+ storageConfiguration: AWSS3StoragePluginConfiguration(),
+ storageBehaviour: serviceMock)
+ do {
+ _ = try await task.value
+ XCTFail("Task should throw an exception")
+ }
+ catch {
+ guard let storageError = error as? StorageError,
+ case .validation(let field, _, _, _) = storageError else {
+ XCTFail("Should throw a storage validation error, instead threw \(error)")
+ return
+ }
+
+ XCTAssertEqual(field, "path", "Field in error should be `path`")
+ }
+ }
+}
diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift
new file mode 100644
index 0000000000..06f4ecf809
--- /dev/null
+++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift
@@ -0,0 +1,123 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import XCTest
+@testable import Amplify
+@testable import AmplifyTestCommon
+@testable import AWSPluginsCore
+@testable import AWSS3StoragePlugin
+@testable import AWSPluginsTestCommon
+import AWSS3
+
+class AWSS3StorageRemoveTaskTests: XCTestCase {
+
+
+ /// - Given: A configured Storage Remove Task with mocked service
+ /// - When: AWSS3StorageRemoveTask value is invoked
+ /// - Then: A key should be returned, that has been removed without any errors.
+ func testRemoveTaskSuccess() async throws {
+ let serviceMock = MockAWSS3StorageService()
+ let client = serviceMock.client as! MockS3Client
+ client.deleteObjectHandler = { input in
+ return .init()
+ }
+
+ let request = StorageRemoveRequest(
+ path: StringStoragePath.fromString("path"), options: .init())
+ let task = AWSS3StorageRemoveTask(
+ request,
+ storageConfiguration: AWSS3StoragePluginConfiguration(),
+ storageBehaviour: serviceMock)
+ let value = try await task.value
+ XCTAssertEqual(value, "path")
+ }
+
+ /// - Given: A configured Storage Remove Task with mocked service, throwing `NoSuchKey` exception
+ /// - When: AWSS3StorageRemoveTask value is invoked
+ /// - Then: A storage service error should be returned, with an underlying service error
+ func testRemoveTaskNoBucket() async throws {
+ let serviceMock = MockAWSS3StorageService()
+ let client = serviceMock.client as! MockS3Client
+ client.deleteObjectHandler = { input in
+ throw AWSS3.NoSuchKey()
+ }
+
+ let request = StorageRemoveRequest(
+ path: StringStoragePath.fromString("path"), options: .init())
+ let task = AWSS3StorageRemoveTask(
+ request,
+ storageConfiguration: AWSS3StoragePluginConfiguration(),
+ storageBehaviour: serviceMock)
+ do {
+ _ = try await task.value
+ XCTFail("Task should throw an exception")
+ }
+ catch {
+ guard let storageError = error as? StorageError,
+ case .service(_, _, let underlyingError) = storageError else {
+ XCTFail("Should throw a Storage service error, instead threw \(error)")
+ return
+ }
+ XCTAssertTrue(underlyingError is AWSS3.NoSuchKey,
+ "Underlying error should be NoSuchKey, instead got \(String(describing: underlyingError))")
+ }
+ }
+
+ /// - Given: A configured Storage Remove Task with invalid path
+ /// - When: AWSS3StorageRemoveTask value is invoked
+ /// - Then: A storage validation error should be returned
+ func testRemoveTaskWithInvalidPath() async throws {
+ let serviceMock = MockAWSS3StorageService()
+
+ let request = StorageRemoveRequest(
+ path: StringStoragePath.fromString("/path"), options: .init())
+ let task = AWSS3StorageRemoveTask(
+ request,
+ storageConfiguration: AWSS3StoragePluginConfiguration(),
+ storageBehaviour: serviceMock)
+ do {
+ _ = try await task.value
+ XCTFail("Task should throw an exception")
+ }
+ catch {
+ guard let storageError = error as? StorageError,
+ case .validation(let field, _, _, _) = storageError else {
+ XCTFail("Should throw a storage validation error, instead threw \(error)")
+ return
+ }
+
+ XCTAssertEqual(field, "path", "Field in error should be `path`")
+ }
+ }
+
+ /// - Given: A configured Storage Remove Task with invalid path
+ /// - When: AWSS3StorageRemoveTask value is invoked
+ /// - Then: A storage validation error should be returned
+ func testRemoveTaskWithInvalidEmptyPath() async throws {
+ let serviceMock = MockAWSS3StorageService()
+
+ let request = StorageRemoveRequest(
+ path: StringStoragePath.fromString(" "), options: .init())
+ let task = AWSS3StorageRemoveTask(
+ request,
+ storageConfiguration: AWSS3StoragePluginConfiguration(),
+ storageBehaviour: serviceMock)
+ do {
+ _ = try await task.value
+ XCTFail("Task should throw an exception")
+ }
+ catch {
+ guard let storageError = error as? StorageError,
+ case .validation(let field, _, _, _) = storageError else {
+ XCTFail("Should throw a storage validation error, instead threw \(error)")
+ return
+ }
+
+ XCTAssertEqual(field, "path", "Field in error should be `path`")
+ }
+ }
+}
diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginDownloadIntegrationTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginDownloadIntegrationTests.swift
new file mode 100644
index 0000000000..d58a879123
--- /dev/null
+++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginDownloadIntegrationTests.swift
@@ -0,0 +1,61 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+@testable import Amplify
+
+import AWSS3StoragePlugin
+import ClientRuntime
+import CryptoKit
+import XCTest
+
+class AWSS3StoragePluginDownloadIntegrationTests: AWSS3StoragePluginTestBase {
+ /// Given: An object in storage
+ /// When: Call the downloadData API
+ /// Then: The operation completes successfully with the data retrieved
+ func testDownloadDataToMemory() async throws {
+ let key = UUID().uuidString
+ try await uploadData(key: key, data: Data(key.utf8))
+ _ = try await Amplify.Storage.downloadData(path: .fromString("public/\(key)"), options: .init()).value
+ _ = try await Amplify.Storage.remove(path: .fromString("public/\(key)"))
+ }
+ /// Given: An object in storage
+ /// When: Call the downloadFile API
+ /// Then: The operation completes successfully the local file containing the data from the object
+ func testDownloadFile() async throws {
+ let key = UUID().uuidString
+ let timestamp = String(Date().timeIntervalSince1970)
+ let timestampData = Data(timestamp.utf8)
+ try await uploadData(key: key, data: timestampData)
+ let filePath = NSTemporaryDirectory() + key + ".tmp"
+ let fileURL = URL(fileURLWithPath: filePath)
+ removeIfExists(fileURL)
+
+ _ = try await Amplify.Storage.downloadFile(path: .fromString("public/\(key)"), local: fileURL, options: .init()).value
+
+ let fileExists = FileManager.default.fileExists(atPath: fileURL.path)
+ XCTAssertTrue(fileExists)
+ do {
+ let result = try String(contentsOf: fileURL, encoding: .utf8)
+ XCTAssertEqual(result, timestamp)
+ } catch {
+ XCTFail("Failed to read file that has been downloaded to")
+ }
+ removeIfExists(fileURL)
+ _ = try await Amplify.Storage.remove(key: key)
+ }
+
+ func removeIfExists(_ fileURL: URL) {
+ let fileExists = FileManager.default.fileExists(atPath: fileURL.path)
+ if fileExists {
+ do {
+ try FileManager.default.removeItem(at: fileURL)
+ } catch {
+ XCTFail("Failed to delete file at \(fileURL)")
+ }
+ }
+ }
+}
diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift
new file mode 100644
index 0000000000..d8a4496e82
--- /dev/null
+++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift
@@ -0,0 +1,85 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+@testable import Amplify
+
+import AWSS3StoragePlugin
+import ClientRuntime
+import AWSClientRuntime
+import CryptoKit
+import XCTest
+import AWSS3
+
+class AWSS3StoragePluginGetURLIntegrationTests: AWSS3StoragePluginTestBase {
+
+ /// Given: An object in storage
+ /// When: Call the getURL API
+ /// Then: The operation completes successfully with the URL retrieved
+ func testGetRemoteURL() async throws {
+ let key = "public/" + UUID().uuidString
+ try await uploadData(key: key, dataString: key)
+ _ = try await Amplify.Storage.uploadData(
+ path: .fromString(key),
+ data: Data(key.utf8),
+ options: .init())
+
+ let remoteURL = try await Amplify.Storage.getURL(path: .fromString(key))
+
+ // The presigned URL generation does not result in an SDK or HTTP call.
+ XCTAssertEqual(requestRecorder.sdkRequests.map { $0.method} , [])
+
+ let (data, response) = try await URLSession.shared.data(from: remoteURL)
+ let httpResponse = try XCTUnwrap(response as? HTTPURLResponse)
+ XCTAssertEqual(httpResponse.statusCode, 200)
+
+ let dataString = try XCTUnwrap(String(data: data, encoding: .utf8))
+ XCTAssertEqual(dataString, key)
+
+ _ = try await Amplify.Storage.remove(path: .fromString(key))
+ }
+
+ /// - Given: A key for a non-existent S3 object
+ /// - When: A pre-signed URL is requested for that key with `validateObjectExistence = true`
+ /// - Then: A StorageError.keyNotFound error is thrown
+ func testGetURLForUnknownKeyWithValidation() async throws {
+ let unknownKey = "public/" + UUID().uuidString
+ do {
+ let url = try await Amplify.Storage.getURL(
+ path: .fromString(unknownKey),
+ options: .init(
+ pluginOptions: AWSStorageGetURLOptions(validateObjectExistence: true)
+ )
+ )
+ XCTFail("Expecting failure but got url: \(url)")
+ } catch StorageError.keyNotFound(let key, _, _, _) {
+ XCTAssertTrue(key.contains(unknownKey))
+ }
+
+ // A S3 HeadObject call is expected
+ XCTAssert(requestRecorder.sdkRequests.map(\.method).allSatisfy { $0 == .head })
+
+ XCTAssertEqual(requestRecorder.urlRequests.map { $0.httpMethod }, [])
+ }
+
+ /// - Given: A key for a non-existent S3 object
+ /// - When: A pre-signed URL is requested for that key with `validateObjectExistence = false`
+ /// - Then: A pre-signed URL is returned
+ func testGetURLForUnknownKeyWithoutValidation() async throws {
+ let unknownKey = UUID().uuidString
+ let url = try await Amplify.Storage.getURL(
+ path: .fromString(unknownKey),
+ options: .init(
+ pluginOptions: AWSStorageGetURLOptions(validateObjectExistence: false)
+ )
+ )
+ XCTAssertNotNil(url)
+
+ // No SDK or URLRequest calls expected
+ XCTAssertEqual(requestRecorder.sdkRequests.map { $0.method} , [])
+ XCTAssertEqual(requestRecorder.urlRequests.map { $0.httpMethod }, [])
+ }
+}
diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift
new file mode 100644
index 0000000000..83ad2ce017
--- /dev/null
+++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift
@@ -0,0 +1,192 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+@testable import Amplify
+
+import AWSS3StoragePlugin
+import ClientRuntime
+import AWSClientRuntime
+import CryptoKit
+import XCTest
+import AWSS3
+
+class AWSS3StoragePluginListObjectsIntegrationTests: AWSS3StoragePluginTestBase {
+
+ /// Given: Multiple data object which is uploaded to a public path
+ /// When: `Amplify.Storage.list` is run
+ /// Then: The API should execute successfully and list objects for path
+ func testListObjectsUploadedPublicData() async throws {
+ let key = UUID().uuidString
+ let data = Data(key.utf8)
+ let uniqueStringPath = "public/\(key)"
+
+ _ = try await Amplify.Storage.uploadData(path: .fromString(uniqueStringPath + "/test1"), data: data, options: nil).value
+
+ let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(firstListResult.items.filter({ $0.path.contains(uniqueStringPath)
+ }).count, 1)
+
+ _ = try await Amplify.Storage.uploadData(path: .fromString(uniqueStringPath + "/test2"), data: data, options: nil).value
+
+ let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(secondListResult.items.filter({ $0.path.contains(uniqueStringPath)
+ }).count, 2)
+
+ // Clean up
+ _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "/test1"))
+ _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "/test2"))
+ }
+
+ /// Given: Multiple data object which is uploaded to a protected path
+ /// When: `Amplify.Storage.list` is run
+ /// Then: The API should execute successfully and list objects for path
+ func testListObjectsUploadedProtectedData() async throws {
+ let key = UUID().uuidString
+ let data = Data(key.utf8)
+ var uniqueStringPath = ""
+
+ // Sign in
+ _ = try await Amplify.Auth.signIn(username: Self.user1, password: Self.password)
+
+ _ = try await Amplify.Storage.uploadData(
+ path: .fromIdentityID({ identityId in
+ uniqueStringPath = "protected/\(identityId)/\(key)"
+ return uniqueStringPath + "test1"
+ }),
+ data: data,
+ options: nil).value
+
+ let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(firstListResult.items.filter({ $0.path.contains(uniqueStringPath)
+ }).count, 1)
+
+ _ = try await Amplify.Storage.uploadData(
+ path: .fromIdentityID({ identityId in
+ uniqueStringPath = "protected/\(identityId)/\(key)"
+ return uniqueStringPath + "test2"
+ }),
+ data: data,
+ options: nil).value
+
+ let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(secondListResult.items.filter({ $0.path.contains(uniqueStringPath)
+ }).count, 2)
+
+ // clean up
+ _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "test1"))
+ _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "test2"))
+
+ }
+
+ /// Given: Multiple data object which is uploaded to a private path
+ /// When: `Amplify.Storage.list` is run
+ /// Then: The API should execute successfully and list objects for path
+ func testListObjectsUploadedPrivateData() async throws {
+ let key = UUID().uuidString
+ let data = Data(key.utf8)
+ var uniqueStringPath = ""
+
+ // Sign in
+ _ = try await Amplify.Auth.signIn(username: Self.user1, password: Self.password)
+
+ _ = try await Amplify.Storage.uploadData(
+ path: .fromIdentityID({ identityId in
+ uniqueStringPath = "private/\(identityId)/\(key)"
+ return uniqueStringPath + "test1"
+ }),
+ data: data,
+ options: nil).value
+
+ let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(firstListResult.items.filter({ $0.path.contains(uniqueStringPath)
+ }).count, 1)
+
+ _ = try await Amplify.Storage.uploadData(
+ path: .fromIdentityID({ identityId in
+ uniqueStringPath = "private/\(identityId)/\(key)"
+ return uniqueStringPath + "test2"
+ }),
+ data: data,
+ options: nil).value
+
+ let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(secondListResult.items.filter({ $0.path.contains(uniqueStringPath)
+ }).count, 2)
+
+ // clean up
+ _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "test1"))
+ _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "test2"))
+
+ }
+
+ /// Given: Give a unique key that does not exist
+ /// When: `Amplify.Storage.list` is run
+ /// Then: The API should execute and throw an error
+ func testRemoveKeyDoesNotExist() async throws {
+ let key = UUID().uuidString
+ let uniqueStringPath = "public/\(key)"
+
+ do {
+ _ = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+ }
+ catch {
+ guard let storageError = error as? StorageError else {
+ XCTFail("Error should be of type StorageError but got \(error)")
+ return
+ }
+ guard case .keyNotFound(_, _, _, let underlyingError) = storageError else {
+ XCTFail("Error should be of type keyNotFound but got \(error)")
+ return
+ }
+
+ guard underlyingError is AWSS3.NotFound else {
+ XCTFail("Underlying error should be of type AWSS3.NotFound but got \(error)")
+ return
+ }
+ }
+ }
+
+ /// Given: Give a unique key where is user is NOT logged in
+ /// When: `Amplify.Storage.list` is run
+ /// Then: The API should execute and throw an error
+ func testRemoveKeyWhenNotSignedInForPrivateKey() async throws {
+ let key = UUID().uuidString
+ let uniqueStringPath = "private/\(key)"
+
+ do {
+ _ = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+ }
+ catch {
+ guard let storageError = error as? StorageError else {
+ XCTFail("Error should be of type StorageError but got \(error)")
+ return
+ }
+ guard case .accessDenied(_, _, let underlyingError) = storageError else {
+ XCTFail("Error should be of type keyNotFound but got \(error)")
+ return
+ }
+
+ guard underlyingError is UnknownAWSHTTPServiceError else {
+ XCTFail("Underlying error should be of type UnknownAWSHTTPServiceError but got \(error)")
+ return
+ }
+ }
+ }
+
+}
diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginRemoveIntegrationTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginRemoveIntegrationTests.swift
new file mode 100644
index 0000000000..561c1504ba
--- /dev/null
+++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginRemoveIntegrationTests.swift
@@ -0,0 +1,166 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+@testable import Amplify
+
+import AWSS3StoragePlugin
+import ClientRuntime
+import AWSClientRuntime
+import CryptoKit
+import XCTest
+import AWSS3
+
+class AWSS3StoragePluginRemoveIntegrationTests: AWSS3StoragePluginTestBase {
+
+ /// Given: A data object which is uploaded to a public path
+ /// When: `Amplify.Storage.remove` is run
+ /// Then: The API should execute successfully and remove the object
+ func testRemoveUploadedPublicData() async throws {
+ let key = UUID().uuidString
+ let data = Data(key.utf8)
+ let uniqueStringPath = "public/\(key)"
+
+ _ = try await Amplify.Storage.uploadData(path: .fromString(uniqueStringPath), data: data, options: nil).value
+
+ let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(firstListResult.items.filter({ $0.key == uniqueStringPath}).count, 1)
+
+ // Validate
+ _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath))
+
+ let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(secondListResult.items.filter({ $0.key == uniqueStringPath}).count, 0)
+
+ }
+
+ /// Given: A data object which is uploaded to a protected path
+ /// When: `Amplify.Storage.remove` is run
+ /// Then: The API should execute successfully and remove the object
+ func testRemoveUploadedProtectedData() async throws {
+ let key = UUID().uuidString
+ let data = Data(key.utf8)
+ var uniqueStringPath = ""
+
+ // Sign in
+ _ = try await Amplify.Auth.signIn(username: Self.user1, password: Self.password)
+
+ _ = try await Amplify.Storage.uploadData(
+ path: .fromIdentityID({ identityId in
+ uniqueStringPath = "protected/\(identityId)/\(key)"
+ return uniqueStringPath
+ }),
+ data: data,
+ options: nil).value
+
+ let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(firstListResult.items.filter({ $0.key == uniqueStringPath}).count, 1)
+
+ // Validate
+ _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath))
+
+ let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(secondListResult.items.filter({ $0.key == uniqueStringPath}).count, 0)
+
+ }
+
+ /// Given: A data object which is uploaded to a private path
+ /// When: `Amplify.Storage.remove` is run
+ /// Then: The API should execute successfully and remove the object
+ func testRemoveUploadedPrivateData() async throws {
+ let key = UUID().uuidString
+ let data = Data(key.utf8)
+ var uniqueStringPath = ""
+
+ // Sign in
+ _ = try await Amplify.Auth.signIn(username: Self.user1, password: Self.password)
+
+ _ = try await Amplify.Storage.uploadData(
+ path: .fromIdentityID({ identityId in
+ uniqueStringPath = "private/\(identityId)/\(key)"
+ return uniqueStringPath
+ }),
+ data: data,
+ options: nil).value
+
+ let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(firstListResult.items.filter({ $0.key == uniqueStringPath}).count, 1)
+
+ // Validate
+ _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath))
+
+ let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath))
+
+ // Validate the item was uploaded.
+ XCTAssertEqual(secondListResult.items.filter({ $0.key == uniqueStringPath}).count, 0)
+
+ }
+
+ /// Given: Give a unique key that does not exist
+ /// When: `Amplify.Storage.remove` is run
+ /// Then: The API should execute and throw an error
+ func testRemoveKeyDoesNotExist() async throws {
+ let key = UUID().uuidString
+ let uniqueStringPath = "public/\(key)"
+
+ do {
+ _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath))
+ }
+ catch {
+ guard let storageError = error as? StorageError else {
+ XCTFail("Error should be of type StorageError but got \(error)")
+ return
+ }
+ guard case .keyNotFound(_, _, _, let underlyingError) = storageError else {
+ XCTFail("Error should be of type keyNotFound but got \(error)")
+ return
+ }
+
+ guard underlyingError is AWSS3.NotFound else {
+ XCTFail("Underlying error should be of type AWSS3.NotFound but got \(error)")
+ return
+ }
+ }
+ }
+
+ /// Given: Give a unique key where is user is NOT logged in
+ /// When: `Amplify.Storage.remove` is run
+ /// Then: The API should execute and throw an error
+ func testRemoveKeyWhenNotSignedInForPrivateKey() async throws {
+ let key = UUID().uuidString
+ let uniqueStringPath = "private/\(key)"
+
+ do {
+ _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath))
+ }
+ catch {
+ guard let storageError = error as? StorageError else {
+ XCTFail("Error should be of type StorageError but got \(error)")
+ return
+ }
+ guard case .accessDenied(_, _, let underlyingError) = storageError else {
+ XCTFail("Error should be of type keyNotFound but got \(error)")
+ return
+ }
+
+ guard underlyingError is UnknownAWSHTTPServiceError else {
+ XCTFail("Underlying error should be of type UnknownAWSHTTPServiceError but got \(error)")
+ return
+ }
+ }
+ }
+
+}
diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginUploadIntegrationTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginUploadIntegrationTests.swift
new file mode 100644
index 0000000000..565bee8746
--- /dev/null
+++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginUploadIntegrationTests.swift
@@ -0,0 +1,201 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+@testable import Amplify
+
+import AWSS3StoragePlugin
+import ClientRuntime
+import CryptoKit
+import XCTest
+
+class AWSS3StoragePluginUploadIntegrationTests: AWSS3StoragePluginTestBase {
+
+ var uploadedKeys: [String]!
+
+ /// Represents expected pieces of the User-Agent header of an SDK http request.
+ ///
+ /// Example SDK User-Agent:
+ /// ```
+ /// User-Agent: aws-sdk-swift/1.0 api/s3/1.0 os/iOS/16.4.0 lang/swift/5.8
+ /// ```
+ /// - Tag: SdkUserAgentComponent
+ private enum SdkUserAgentComponent: String, CaseIterable {
+ case api = "api/s3"
+ case lang = "lang/swift"
+ case os = "os/"
+ case sdk = "aws-sdk-swift/"
+ }
+
+ /// Represents expected pieces of the User-Agent header of an URLRequest used for uploading or
+ /// downloading.
+ ///
+ /// Example SDK User-Agent:
+ /// ```
+ /// User-Agent: lib/amplify-swift
+ /// ```
+ /// - Tag: SdkUserAgentComponent
+ private enum URLUserAgentComponent: String, CaseIterable {
+ case lib = "lib/amplify-swift"
+ case os = "os/"
+ }
+
+ override func setUp() async throws {
+ try await super.setUp()
+ uploadedKeys = []
+ }
+
+ override func tearDown() async throws {
+ for key in uploadedKeys {
+ _ = try await Amplify.Storage.remove(path: .fromString("public/\(key)"))
+ }
+ uploadedKeys = nil
+ try await super.tearDown()
+ }
+
+ /// Given: An data object
+ /// When: Upload the data
+ /// Then: The operation completes successfully
+ func testUploadData() async throws {
+ let key = UUID().uuidString
+ let data = Data(key.utf8)
+
+ _ = try await Amplify.Storage.uploadData(path: .fromString("public/\(key)"), data: data, options: nil).value
+ _ = try await Amplify.Storage.remove(path: .fromString("public/\(key)"))
+
+ // Only the remove operation results in an SDK request
+ XCTAssertEqual(requestRecorder.sdkRequests.map { $0.method } , [.delete])
+ try assertUserAgentComponents(sdkRequests: requestRecorder.sdkRequests)
+
+ XCTAssertEqual(requestRecorder.urlRequests.map { $0.httpMethod }, ["PUT"])
+ try assertUserAgentComponents(urlRequests: requestRecorder.urlRequests)
+ }
+
+ /// Given: A empty data object
+ /// When: Upload the data
+ /// Then: The operation completes successfully
+ func testUploadEmptyData() async throws {
+ let key = UUID().uuidString
+ let data = Data("".utf8)
+ _ = try await Amplify.Storage.uploadData(path: .fromString("public/\(key)"), data: data, options: nil).value
+ _ = try await Amplify.Storage.remove(path: .fromString("public/\(key)"))
+
+ XCTAssertEqual(requestRecorder.urlRequests.map { $0.httpMethod }, ["PUT"])
+ try assertUserAgentComponents(urlRequests: requestRecorder.urlRequests)
+ }
+
+ /// Given: A file with contents
+ /// When: Upload the file
+ /// Then: The operation completes successfully and all URLSession and SDK requests include a user agent
+ func testUploadFile() async throws {
+ let key = UUID().uuidString
+ let filePath = NSTemporaryDirectory() + key + ".tmp"
+
+ let fileURL = URL(fileURLWithPath: filePath)
+ FileManager.default.createFile(atPath: filePath, contents: Data(key.utf8), attributes: nil)
+
+ _ = try await Amplify.Storage.uploadFile(path: .fromString("public/\(key)"), local: fileURL, options: nil).value
+ _ = try await Amplify.Storage.remove(path: .fromString("public/\(key)"))
+
+ // Only the remove operation results in an SDK request
+ XCTAssertEqual(requestRecorder.sdkRequests.map { $0.method} , [.delete])
+ try assertUserAgentComponents(sdkRequests: requestRecorder.sdkRequests)
+
+ XCTAssertEqual(requestRecorder.urlRequests.map { $0.httpMethod }, ["PUT"])
+ try assertUserAgentComponents(urlRequests: requestRecorder.urlRequests)
+ }
+
+ /// Given: A file with empty contents
+ /// When: Upload the file
+ /// Then: The operation completes successfully
+ func testUploadFileEmptyData() async throws {
+ let key = UUID().uuidString
+ let filePath = NSTemporaryDirectory() + key + ".tmp"
+ let fileURL = URL(fileURLWithPath: filePath)
+ FileManager.default.createFile(atPath: filePath, contents: Data("".utf8), attributes: nil)
+
+ _ = try await Amplify.Storage.uploadFile(path: .fromString("public/\(key)"), local: fileURL, options: nil).value
+ _ = try await Amplify.Storage.remove(path: .fromString("public/\(key)"))
+
+ XCTAssertEqual(requestRecorder.urlRequests.map { $0.httpMethod }, ["PUT"])
+ try assertUserAgentComponents(urlRequests: requestRecorder.urlRequests)
+ }
+
+ /// Given: A large data object
+ /// When: Upload the data
+ /// Then: The operation completes successfully
+ func testUploadLargeData() async throws {
+ let key = "public/" + UUID().uuidString
+
+ let uploadKey = try await Amplify.Storage.uploadData(path: .fromString(key),
+ data: AWSS3StoragePluginTestBase.largeDataObject,
+ options: nil).value
+ XCTAssertEqual(uploadKey, key)
+
+ try await Amplify.Storage.remove(path: .fromString(key))
+
+ let userAgents = requestRecorder.urlRequests.compactMap { $0.allHTTPHeaderFields?["User-Agent"] }
+ XCTAssertGreaterThan(userAgents.count, 1)
+ for userAgent in userAgents {
+ let expectedComponent = "MultiPart/UploadPart"
+ XCTAssertTrue(userAgent.contains(expectedComponent), "\(userAgent) does not contain \(expectedComponent)")
+ }
+ }
+
+ /// Given: A large file
+ /// When: Upload the file
+ /// Then: The operation completes successfully
+ func testUploadLargeFile() async throws {
+ let key = UUID().uuidString
+ let filePath = NSTemporaryDirectory() + key + ".tmp"
+ let fileURL = URL(fileURLWithPath: filePath)
+
+ FileManager.default.createFile(atPath: filePath,
+ contents: AWSS3StoragePluginTestBase.largeDataObject,
+ attributes: nil)
+
+ _ = try await Amplify.Storage.uploadFile(path: .fromString("public/\(key)"), local: fileURL, options: nil).value
+ _ = try await Amplify.Storage.remove(path: .fromString("public/\(key)"))
+
+ let userAgents = requestRecorder.urlRequests.compactMap { $0.allHTTPHeaderFields?["User-Agent"] }
+ XCTAssertGreaterThan(userAgents.count, 1)
+ for userAgent in userAgents {
+ let expectedComponent = "MultiPart/UploadPart"
+ XCTAssertTrue(userAgent.contains(expectedComponent), "\(userAgent) does not contain \(expectedComponent)")
+ }
+ }
+
+ func removeIfExists(_ fileURL: URL) {
+ let fileExists = FileManager.default.fileExists(atPath: fileURL.path)
+ if fileExists {
+ do {
+ try FileManager.default.removeItem(at: fileURL)
+ } catch {
+ XCTFail("Failed to delete file at \(fileURL)")
+ }
+ }
+ }
+
+ private func assertUserAgentComponents(sdkRequests: [SdkHttpRequest], file: StaticString = #filePath, line: UInt = #line) throws {
+ for request in sdkRequests {
+ let headers = request.headers.dictionary
+ let userAgent = try XCTUnwrap(headers["User-Agent"]?.joined(separator:","))
+ for component in SdkUserAgentComponent.allCases {
+ XCTAssertTrue(userAgent.contains(component.rawValue), "\(userAgent.description) does not contain \(component)", file: file, line: line)
+ }
+ }
+ }
+
+ private func assertUserAgentComponents(urlRequests: [URLRequest], file: StaticString = #filePath, line: UInt = #line) throws {
+ for request in urlRequests {
+ let headers = try XCTUnwrap(request.allHTTPHeaderFields)
+ let userAgent = try XCTUnwrap(headers["User-Agent"])
+ for component in URLUserAgentComponent.allCases {
+ XCTAssertTrue(userAgent.contains(component.rawValue), "\(userAgent.description) does not contain \(component)", file: file, line: line)
+ }
+ }
+ }
+}
diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj
index 9ead18462e..f3330c56ef 100644
--- a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj
+++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj
@@ -9,6 +9,9 @@
/* Begin PBXBuildFile section */
0311113528EBED6500D58441 /* Tests.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 0311113428EBED6500D58441 /* Tests.xcconfig */; };
031BC3F328EC9B2C0047B2E8 /* AppIcon.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 031BC3F228EC9B2C0047B2E8 /* AppIcon.xcassets */; };
+ 488C2A732BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 488C2A722BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift */; };
+ 488C2A752BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 488C2A742BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift */; };
+ 488C2A772BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 488C2A762BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift */; };
21D165C32BBEF329001E3D4B /* amplify_outputs.json in Resources */ = {isa = PBXBuildFile; fileRef = 21D165C22BBEF329001E3D4B /* amplify_outputs.json */; };
21D165C42BBEF329001E3D4B /* amplify_outputs.json in Resources */ = {isa = PBXBuildFile; fileRef = 21D165C22BBEF329001E3D4B /* amplify_outputs.json */; };
21F7630D2BD6B8640048845A /* AWSS3StoragePluginAccelerateIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565DF16F2953BAEA000DCCF7 /* AWSS3StoragePluginAccelerateIntegrationTests.swift */; };
@@ -81,6 +84,8 @@
68828E4628C2736C006E7C0A /* AWSS3StoragePluginProgressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08C28BEAF8E00C8A6EB /* AWSS3StoragePluginProgressTests.swift */; };
68828E4728C27745006E7C0A /* AWSS3StoragePluginPutDataResumabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08828BEAF8E00C8A6EB /* AWSS3StoragePluginPutDataResumabilityTests.swift */; };
68828E4828C2AAA6006E7C0A /* AWSS3StoragePluginGetDataResumabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08B28BEAF8E00C8A6EB /* AWSS3StoragePluginGetDataResumabilityTests.swift */; };
+ 734605222BACB5CC0039F0EB /* AWSS3StoragePluginUploadIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 734605212BACB5CC0039F0EB /* AWSS3StoragePluginUploadIntegrationTests.swift */; };
+ 734605242BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 734605232BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift */; };
901AB3E92AE2C2DC000F825B /* AWSS3StoragePluginUploadMetadataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901AB3E82AE2C2DC000F825B /* AWSS3StoragePluginUploadMetadataTestCase.swift */; };
97914BA32955798D002000EA /* AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFEAF28E748270000C36A /* AsyncTesting.swift */; };
97914BA52955798D002000EA /* AsyncExpectation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFEB028E748270000C36A /* AsyncExpectation.swift */; };
@@ -130,6 +135,9 @@
0311113428EBED6500D58441 /* Tests.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Tests.xcconfig; sourceTree = ""; };
0311113828EBEEA700D58441 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = ""; };
031BC3F228EC9B2C0047B2E8 /* AppIcon.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = AppIcon.xcassets; sourceTree = ""; };
+ 488C2A722BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginRemoveIntegrationTests.swift; sourceTree = ""; };
+ 488C2A742BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginListObjectsIntegrationTests.swift; sourceTree = ""; };
+ 488C2A762BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginGetURLIntegrationTests.swift; sourceTree = ""; };
21D165C02BBEDF0A001E3D4B /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
21D165C22BBEF329001E3D4B /* amplify_outputs.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = amplify_outputs.json; sourceTree = ""; };
21F763262BD6B8640048845A /* AWSS3StoragePluginGen2IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSS3StoragePluginGen2IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -161,6 +169,8 @@
684FB0A928BEB07200C8A6EB /* AWSS3StoragePluginIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSS3StoragePluginIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
684FB0C228BEB45600C8A6EB /* AuthSignInHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthSignInHelper.swift; sourceTree = ""; };
684FB0C528BEB84800C8A6EB /* StorageHostApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = StorageHostApp.entitlements; sourceTree = ""; };
+ 734605212BACB5CC0039F0EB /* AWSS3StoragePluginUploadIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginUploadIntegrationTests.swift; sourceTree = ""; };
+ 734605232BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginDownloadIntegrationTests.swift; sourceTree = ""; };
901AB3E82AE2C2DC000F825B /* AWSS3StoragePluginUploadMetadataTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginUploadMetadataTestCase.swift; sourceTree = ""; };
97914B972955797E002000EA /* StorageStressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageStressTests.swift; sourceTree = ""; };
97914BB92955798D002000EA /* StorageStressTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StorageStressTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -312,6 +322,11 @@
562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */,
901AB3E82AE2C2DC000F825B /* AWSS3StoragePluginUploadMetadataTestCase.swift */,
684FB08728BEAF8E00C8A6EB /* ResumabilityTests */,
+ 734605212BACB5CC0039F0EB /* AWSS3StoragePluginUploadIntegrationTests.swift */,
+ 734605232BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift */,
+ 488C2A722BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift */,
+ 488C2A742BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift */,
+ 488C2A762BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift */,
);
path = AWSS3StoragePluginIntegrationTests;
sourceTree = "";
@@ -706,22 +721,27 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 488C2A772BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift in Sources */,
565DF1702953BAEA000DCCF7 /* AWSS3StoragePluginAccelerateIntegrationTests.swift in Sources */,
684FB0C328BEB45600C8A6EB /* AuthSignInHelper.swift in Sources */,
681DFEB228E748270000C36A /* AsyncTesting.swift in Sources */,
68828E4828C2AAA6006E7C0A /* AWSS3StoragePluginGetDataResumabilityTests.swift in Sources */,
901AB3E92AE2C2DC000F825B /* AWSS3StoragePluginUploadMetadataTestCase.swift in Sources */,
681DFEB328E748270000C36A /* AsyncExpectation.swift in Sources */,
+ 488C2A732BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift in Sources */,
68828E4628C2736C006E7C0A /* AWSS3StoragePluginProgressTests.swift in Sources */,
684FB0B528BEB08900C8A6EB /* AWSS3StoragePluginAccessLevelTests.swift in Sources */,
68828E4028C1549E006E7C0A /* AWSS3StoragePluginDownloadFileResumabilityTests.swift in Sources */,
+ 734605242BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift in Sources */,
68828E4528C26D2D006E7C0A /* AWSS3StoragePluginPrefixKeyResolverTests.swift in Sources */,
684FB0B328BEB08900C8A6EB /* AWSS3StoragePluginTestBase.swift in Sources */,
+ 488C2A752BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift in Sources */,
68828E3F28C1549B006E7C0A /* AWSS3StoragePluginUploadFileResumabilityTests.swift in Sources */,
562B9AA42A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift in Sources */,
68828E3E28C1546F006E7C0A /* AWSS3StoragePluginConfigurationTests.swift in Sources */,
68828E4728C27745006E7C0A /* AWSS3StoragePluginPutDataResumabilityTests.swift in Sources */,
68828E4128C154E5006E7C0A /* AWSS3StoragePluginNegativeTests.swift in Sources */,
+ 734605222BACB5CC0039F0EB /* AWSS3StoragePluginUploadIntegrationTests.swift in Sources */,
68828E3D28C136EB006E7C0A /* AWSS3StoragePluginBasicIntegrationTests.swift in Sources */,
681DFEB428E748270000C36A /* XCTestCase+AsyncTesting.swift in Sources */,
68828E4228C15B8B006E7C0A /* AWSS3StoragePluginOptionsUsabilityTests.swift in Sources */,
diff --git a/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift b/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift
index b8e9acf55e..bc6255567f 100644
--- a/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift
+++ b/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift
@@ -180,6 +180,68 @@ class MockStorageCategoryPlugin: MessageReporter, StorageCategoryPlugin {
false
}
+ func getURL(path: any StoragePath, options: StorageGetURLRequest.Options?) async throws -> URL {
+ notify("getURL")
+ let options = options ?? StorageGetURLRequest.Options()
+ let request = StorageGetURLRequest(key: key, options: options)
+ let operation = MockStorageGetURLOperation(request: request)
+ let taskAdapter = AmplifyOperationTaskAdapter(operation: operation)
+ return try await taskAdapter.value
+ }
+
+ func downloadData(path: any StoragePath, options: StorageDownloadDataRequest.Options?) -> StorageDownloadDataTask {
+ notify("downloadData")
+ let options = options ?? StorageDownloadDataRequest.Options()
+ let request = StorageDownloadDataRequest(path: path, options: options)
+ let operation = MockStorageDownloadDataOperation(request: request)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ return taskAdapter
+ }
+
+ func downloadFile(path: any StoragePath, local: URL, options: StorageDownloadFileRequest.Options?) -> StorageDownloadFileTask {
+ notify("downloadFile")
+ let options = options ?? StorageDownloadFileRequest.Options()
+ let request = StorageDownloadFileRequest(path: path, local: local, options: options)
+ let operation = MockStorageDownloadFileOperation(request: request)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ return taskAdapter
+ }
+
+ func uploadData(path: any StoragePath, data: Data, options: StorageUploadDataRequest.Options?) -> StorageUploadDataTask {
+ notify("uploadData")
+ let options = options ?? StorageUploadDataRequest.Options()
+ let request = StorageUploadDataRequest(key: key, data: data, options: options)
+ let operation = MockStorageUploadDataOperation(request: request)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ return taskAdapter
+ }
+
+ func uploadFile(path: any StoragePath, local: URL, options: StorageUploadFileRequest.Options?) -> StorageUploadFileTask {
+ notify("uploadFile")
+ let options = options ?? StorageUploadFileRequest.Options()
+ let request = StorageUploadFileRequest(key: key, local: local, options: options)
+ let operation = MockStorageUploadFileOperation(request: request)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ return taskAdapter
+ }
+
+ func remove(path: any StoragePath, options: StorageRemoveRequest.Options?) async throws -> String {
+ notify("remove")
+ let options = options ?? StorageRemoveRequest.Options()
+ let request = StorageRemoveRequest(key: key, options: options)
+ let operation = MockStorageRemoveOperation(request: request)
+ let taskAdapter = AmplifyOperationTaskAdapter(operation: operation)
+ return try await taskAdapter.value
+ }
+
+ func list(path: any StoragePath, options: StorageListRequest.Options?) async throws -> StorageListResult {
+ notify("list")
+ let options = options ?? StorageListRequest.Options()
+ let request = StorageListRequest(options: options)
+ let operation = MockStorageListOperation(request: request)
+ let taskAdapter = AmplifyOperationTaskAdapter(operation: operation)
+ return try await taskAdapter.value
+ }
}
class MockSecondStorageCategoryPlugin: MockStorageCategoryPlugin {
diff --git a/AmplifyTests/CategoryTests/Hub/AmplifyOperationHubTests.swift b/AmplifyTests/CategoryTests/Hub/AmplifyOperationHubTests.swift
index baa6629239..0a5b3a01d1 100644
--- a/AmplifyTests/CategoryTests/Hub/AmplifyOperationHubTests.swift
+++ b/AmplifyTests/CategoryTests/Hub/AmplifyOperationHubTests.swift
@@ -299,6 +299,93 @@ class MockDispatchingStoragePlugin: StorageCategoryPlugin {
return try await taskAdapter.value
}
+ @discardableResult
+ func getURL(
+ path: any StoragePath,
+ options: StorageGetURLOperation.Request.Options?
+ ) async throws -> URL {
+ let options = options ?? StorageGetURLRequest.Options()
+ let request = StorageGetURLRequest(key: key, options: options)
+ let operation = MockStorageGetURLOperation(request: request)
+ let taskAdapter = AmplifyOperationTaskAdapter(operation: operation)
+ return try await taskAdapter.value
+ }
+
+ @discardableResult
+ public func downloadData(
+ path: any StoragePath,
+ options: StorageDownloadDataOperation.Request.Options? = nil
+ ) -> StorageDownloadDataTask {
+ let options = options ?? StorageDownloadDataRequest.Options()
+ let request = StorageDownloadDataRequest(key: key, options: options)
+ let operation = MockDispatchingStorageDownloadDataOperation(request: request)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ return taskAdapter
+ }
+
+ @discardableResult
+ public func downloadFile(
+ path: any StoragePath,
+ local: URL,
+ options: StorageDownloadFileOperation.Request.Options?
+ ) -> StorageDownloadFileTask {
+ let options = options ?? StorageDownloadFileRequest.Options()
+ let request = StorageDownloadFileRequest(key: key, local: local, options: options)
+ let operation = MockDispatchingStorageDownloadFileOperation(request: request)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ return taskAdapter
+ }
+
+ @discardableResult
+ public func uploadData(
+ path: any StoragePath,
+ data: Data,
+ options: StorageUploadDataOperation.Request.Options?
+ ) -> StorageUploadDataTask {
+ let options = options ?? StorageUploadDataRequest.Options()
+ let request = StorageUploadDataRequest(key: key, data: data, options: options)
+ let operation = MockDispatchingStorageUploadDataOperation(request: request)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ return taskAdapter
+ }
+
+ @discardableResult
+ public func uploadFile(
+ path: any StoragePath,
+ local: URL,
+ options: StorageUploadFileOperation.Request.Options?
+ ) -> StorageUploadFileTask {
+ let options = options ?? StorageUploadFileRequest.Options()
+ let request = StorageUploadFileRequest(key: key, local: local, options: options)
+ let operation = MockDispatchingStorageUploadFileOperation(request: request)
+ let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation)
+ return taskAdapter
+ }
+
+ @discardableResult
+ public func remove(
+ path: any StoragePath,
+ options: StorageRemoveRequest.Options? = nil
+ ) async throws -> String {
+ let options = options ?? StorageRemoveRequest.Options()
+ let request = StorageRemoveRequest(key: key, options: options)
+ let operation = MockDispatchingStorageRemoveOperation(request: request)
+ let taskAdapter = AmplifyOperationTaskAdapter(operation: operation)
+ return try await taskAdapter.value
+ }
+
+ @discardableResult
+ func list(
+ path: any StoragePath,
+ options: StorageListOperation.Request.Options?
+ ) async throws -> StorageListResult {
+ let options = options ?? StorageListRequest.Options()
+ let request = StorageListRequest(options: options)
+ let operation = MockDispatchingStorageListOperation(request: request)
+ let taskAdapter = AmplifyOperationTaskAdapter(operation: operation)
+ return try await taskAdapter.value
+ }
+
}
// swiftlint:disable:next type_name
diff --git a/AmplifyTests/CategoryTests/Storage/StoragePathTests.swift b/AmplifyTests/CategoryTests/Storage/StoragePathTests.swift
new file mode 100644
index 0000000000..6dc34ed703
--- /dev/null
+++ b/AmplifyTests/CategoryTests/Storage/StoragePathTests.swift
@@ -0,0 +1,35 @@
+//
+// Copyright Amazon.com Inc. or its affiliates.
+// All Rights Reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import XCTest
+
+@testable import Amplify
+@testable import AmplifyTestCommon
+
+class StoragePathTests: XCTestCase {
+
+ /// Given: StringStoragePath object
+ /// When: resolve is called
+ /// Then: a string storage path is returned
+ func testResolveStringStoragePath() {
+ let expectedResult = "/my/path"
+ let path = StringStoragePath(resolve: { input in return expectedResult})
+ let result = path.resolve("input")
+ XCTAssertEqual(result, expectedResult)
+ }
+
+ /// Given: IdentityIDStoragePath object
+ /// When: resolve is called
+ /// Then: a string storage path is returned with the identity id included in the path
+ func testResolveIdentityIDStoragePath() {
+ let identityID = "123"
+ let expectedResult = "/my/\(identityID)/path"
+ let path = IdentityIDStoragePath(resolve: { id in return "/my/\(id)/path"})
+ let result = path.resolve(identityID)
+ XCTAssertEqual(result, expectedResult)
+ }
+}