From 0d13d9f3408f3e779899f7a0c3a15ffe6b57baff Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:33:40 -0500 Subject: [PATCH 01/26] feat(storage): add new storage gen2 APIs (#3559) --- .../Request/StorageDownloadDataRequest.swift | 2 + .../Request/StorageDownloadFileRequest.swift | 2 + .../Request/StorageGetURLRequest.swift | 2 + .../Request/StorageListRequest.swift | 3 + .../Request/StorageRemoveRequest.swift | 1 + .../Request/StorageUploadDataRequest.swift | 2 + .../Request/StorageUploadFileRequest.swift | 2 + .../Storage/Result/StorageListResult.swift | 35 ++++- .../Storage/StorageAccessLevel.swift | 1 + .../StorageCategory+ClientBehavior.swift | 59 +++++++ .../Storage/StorageCategoryBehavior.swift | 147 ++++++++++++++++-- Amplify/Categories/Storage/StoragePath.swift | 44 ++++++ ...SS3StoragePlugin+AsyncClientBehavior.swift | 139 +++++++++++++++++ .../AWSS3StoragePlugin.swift | 1 + .../AWSS3PluginPrefixResolver.swift | 3 + .../AWSS3StoragePluginConfiguration.swift | 2 + .../Mocks/MockStorageCategoryPlugin.swift | 62 ++++++++ .../Hub/AmplifyOperationHubTests.swift | 87 +++++++++++ 18 files changed, 575 insertions(+), 19 deletions(-) create mode 100644 Amplify/Categories/Storage/StoragePath.swift diff --git a/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift index f4f7ee0b83..2c550b6b6d 100644 --- a/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift +++ b/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift @@ -40,11 +40,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 diff --git a/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift index 78d7be6ffd..4a665444dc 100644 --- a/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift +++ b/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift @@ -46,11 +46,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 diff --git a/Amplify/Categories/Storage/Operation/Request/StorageGetURLRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageGetURLRequest.swift index 25c4563098..453bbf847a 100644 --- a/Amplify/Categories/Storage/Operation/Request/StorageGetURLRequest.swift +++ b/Amplify/Categories/Storage/Operation/Request/StorageGetURLRequest.swift @@ -43,11 +43,13 @@ public extension StorageGetURLRequest { /// 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? /// Number of seconds before the URL expires. Defaults to diff --git a/Amplify/Categories/Storage/Operation/Request/StorageListRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageListRequest.swift index d82d4f1718..5689e6b47c 100644 --- a/Amplify/Categories/Storage/Operation/Request/StorageListRequest.swift +++ b/Amplify/Categories/Storage/Operation/Request/StorageListRequest.swift @@ -32,16 +32,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..ace12f1218 100644 --- a/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift +++ b/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift @@ -38,6 +38,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..522f34192b 100644 --- a/Amplify/Categories/Storage/Operation/Request/StorageUploadDataRequest.swift +++ b/Amplify/Categories/Storage/Operation/Request/StorageUploadDataRequest.swift @@ -46,11 +46,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 diff --git a/Amplify/Categories/Storage/Operation/Request/StorageUploadFileRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageUploadFileRequest.swift index 9382776c5b..43b786cf2c 100644 --- a/Amplify/Categories/Storage/Operation/Request/StorageUploadFileRequest.swift +++ b/Amplify/Categories/Storage/Operation/Request/StorageUploadFileRequest.swift @@ -43,11 +43,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 diff --git a/Amplify/Categories/Storage/Result/StorageListResult.swift b/Amplify/Categories/Storage/Result/StorageListResult.swift index f01a517c44..45777ae086 100644 --- a/Amplify/Categories/Storage/Result/StorageListResult.swift +++ b/Amplify/Categories/Storage/Result/StorageListResult.swift @@ -42,6 +42,11 @@ extension StorageListResult { /// - Tag: StorageListResultItem public struct Item { + /// The path of the object in storage. + /// + /// - Tag: StorageListResultItem.path + public let path: StoragePath + /// The unique identifier of the object in storage. /// /// - Tag: StorageListResultItem.key @@ -72,11 +77,31 @@ 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:key: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 = StringStoragePath(pathResolver: { _ in return "" }) + } + + public init( + path: StoragePath, + key: String, + size: Int? = nil, + eTag: String? = nil, + lastModified: Date? = nil, + pluginResults: Any? = nil + ) { + self.path = path self.key = key self.size = size self.eTag = eTag 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..22a76ea0ca 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: 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: 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: 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: 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: 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: 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: 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..1d519cd898 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: 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: 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: 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: 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: 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: 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: 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..d011e3e110 --- /dev/null +++ b/Amplify/Categories/Storage/StoragePath.swift @@ -0,0 +1,44 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +public typealias StoragePathResolver = (String) -> String + +/// Protocol that provides a closure to resolve the storage path. +/// +/// - Tag: StoragePath +public protocol StoragePath { + var pathResolver: StoragePathResolver { get } +} + +public extension StoragePath where Self == StringStoragePath { + static func fromString(_ path: String) -> Self { + return StringStoragePath(pathResolver: { _ in return path }) + } +} + +public extension StoragePath where Self == IdentityIdStoragePath { + static func fromIdentityId(_ identityIdPathResolver: @escaping StoragePathResolver) -> Self { + return IdentityIdStoragePath(pathResolver: identityIdPathResolver) + } +} + +/// Conforms to StoragePath protocol. Provides a storage path based on a string storage path. +/// +/// - Tag: StringStoragePath +public struct StringStoragePath: StoragePath { + public let pathResolver: StoragePathResolver +} + +/// Conforms to StoragePath protocol. +/// Provides a storage path constructed from an unique identity identifer. +/// +/// - Tag: IdentityStoragePath +public struct IdentityIdStoragePath: StoragePath { + public let pathResolver: StoragePathResolver +} diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift index 80167b691d..fde6e7ccf4 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift @@ -45,6 +45,55 @@ extension AWSS3StoragePlugin { return result } + public func getURL( + path: StoragePath, + options: StorageGetURLOperation.Request.Options? = nil + ) async throws -> URL { + let options = options ?? StorageGetURLRequest.Options() + let path = "" //TODO: resolve path + let request = StorageGetURLRequest(key: path, options: options) + if let error = request.validate() { + throw error + } + let prefixResolver = storageConfiguration.prefixResolver ?? StorageAccessLevelAwarePrefixResolver(authService: authService) + let prefix = try await prefixResolver.resolvePrefix(for: options.accessLevel, + targetIdentityId: options.targetIdentityId) + let serviceKey = prefix + request.key + if let pluginOptions = options.pluginOptions as? AWSStorageGetURLOptions, pluginOptions.validateObjectExistence { + try await storageService.validateObjectExistence(serviceKey: serviceKey) + } + let accelerate = try AWSS3PluginOptions.accelerateValue( + pluginOptions: options.pluginOptions) + let result = try await storageService.getPreSignedURL( + serviceKey: serviceKey, + signingOperation: .getObject, + metadata: nil, + accelerate: accelerate, + expires: options.expires) + + let channel = HubChannel(from: categoryType) + let payload = HubPayload(eventName: HubPayload.EventName.Storage.getURL, context: options, data: result) + Amplify.Hub.dispatch(to: channel, payload: payload) + return result + } + + public func downloadData( + path: StoragePath, + options: StorageDownloadDataOperation.Request.Options? = nil + ) -> StorageDownloadDataTask { + let options = options ?? StorageDownloadDataRequest.Options() + let path = "" //TODO: resolve path + let request = StorageDownloadDataRequest(key: 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 +129,25 @@ extension AWSS3StoragePlugin { return taskAdapter } + @discardableResult + public func downloadFile( + path: StoragePath, + local: URL, + options: StorageDownloadFileOperation.Request.Options? = nil + ) -> StorageDownloadFileTask { + let options = options ?? StorageDownloadFileRequest.Options() + let path = "" //TODO: resolve path + let request = StorageDownloadFileRequest(key: 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 +166,25 @@ extension AWSS3StoragePlugin { return taskAdapter } + @discardableResult + public func uploadData( + path: StoragePath, + data: Data, + options: StorageUploadDataOperation.Request.Options? = nil + ) -> StorageUploadDataTask { + let options = options ?? StorageUploadDataRequest.Options() + let path = "" //TODO: resolve path + let request = StorageUploadDataRequest(key: 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 +203,25 @@ extension AWSS3StoragePlugin { return taskAdapter } + @discardableResult + public func uploadFile( + path: StoragePath, + local: URL, + options: StorageUploadFileOperation.Request.Options? = nil + ) -> StorageUploadFileTask { + let options = options ?? StorageUploadFileRequest.Options() + let path = "" //TODO: resolve path + let request = StorageUploadFileRequest(key: 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 +239,24 @@ extension AWSS3StoragePlugin { return try await taskAdapter.value } + @discardableResult + public func remove( + path: StoragePath, + options: StorageRemoveOperation.Request.Options? = nil + ) async throws -> String { + let options = options ?? StorageRemoveRequest.Options() + let path = "" //TODO: resolve path + let request = StorageRemoveRequest(key: path, options: options) + let operation = AWSS3StorageRemoveOperation(request, + storageConfiguration: storageConfiguration, + storageService: storageService, + authService: authService) + let taskAdapter = AmplifyOperationTaskAdapter(operation: operation) + queue.addOperation(operation) + + return try await taskAdapter.value + } + public func list( options: StorageListRequest.Options? = nil ) async throws -> StorageListResult { @@ -148,6 +272,21 @@ extension AWSS3StoragePlugin { return result } + public func list( + path: StoragePath, + options: StorageListRequest.Options? = nil + ) async throws -> StorageListResult { + let options = options ?? StorageListRequest.Options() + let prefix = "" //TODO: resolve path + let result = try await storageService.list(prefix: prefix, options: options) + + let channel = HubChannel(from: categoryType) + let payload = HubPayload(eventName: HubPayload.EventName.Storage.list, context: options, data: result) + Amplify.Hub.dispatch(to: channel, payload: payload) + + return result + } + 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 66fb690978..989d6528a3 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..a9ad565410 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, message: "Use `StoragePath` instead") 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, message: "Use `StoragePath` instead") 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, message: "Use `StoragePath` instead") 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/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift b/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift index b8e9acf55e..cccfabe016 100644 --- a/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift +++ b/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift @@ -180,6 +180,68 @@ class MockStorageCategoryPlugin: MessageReporter, StorageCategoryPlugin { false } + func getURL(path: 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: StoragePath, options: StorageDownloadDataRequest.Options?) -> StorageDownloadDataTask { + notify("downloadData") + let options = options ?? StorageDownloadDataRequest.Options() + let request = StorageDownloadDataRequest(key: key, options: options) + let operation = MockStorageDownloadDataOperation(request: request) + let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation) + return taskAdapter + } + + func downloadFile(path: StoragePath, local: URL, options: StorageDownloadFileRequest.Options?) -> StorageDownloadFileTask { + notify("downloadFile") + let options = options ?? StorageDownloadFileRequest.Options() + let request = StorageDownloadFileRequest(key: key, local: local, options: options) + let operation = MockStorageDownloadFileOperation(request: request) + let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation) + return taskAdapter + } + + func uploadData(path: 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: 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: 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: 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..6a81984fcb 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: 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: 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: 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: 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: 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: 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: 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 From 3a0c0cbcf04214567dbec8199c6fb7d4f1f86084 Mon Sep 17 00:00:00 2001 From: Harsh <6162866+harsh62@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:44:22 -0400 Subject: [PATCH 02/26] feat(storage): refactor storage remove api by including path (#3571) * feat(storage): refactor storage remove api by including path * updated tests --- .../Request/StorageRemoveRequest.swift | 15 ++++ .../Core/Support/AmplifyTaskExecution.swift | 71 +++++++++++++++++++ ...SS3StoragePlugin+AsyncClientBehavior.swift | 16 ++--- .../Error/AWSS3+StorageErrorConvertible.swift | 2 +- .../Storage/AWSS3StorageServiceBehavior.swift | 7 ++ .../Internal/StoragePath+Extensions.swift | 34 +++++++++ .../Tasks/AWSS3StorageRemoveTask.swift | 59 +++++++++++++++ .../Mocks/MockAWSS3StorageService.swift | 4 ++ .../Mocks/MockS3Client.swift | 7 +- .../AWSS3StorageRemoveRequestTests.swift | 1 + .../AWSS3StorageServiceConfigureTests.swift | 14 ---- ...SS3StorageServiceDeleteBehaviorTests.swift | 14 ---- ...orageServiceEscapeHatchBehaviorTests.swift | 15 ---- ...AWSS3StorageServiceListBehaviorTests.swift | 14 ---- ...eServiceMultiPartUploadBehaviorTests.swift | 15 ---- .../Storage/AWSS3StorageServiceTestBase.swift | 41 ----------- ...SS3StorageServiceUploadBehaviorTests.swift | 14 ---- .../Tasks/AWSS3StorageRemoveTaskTests.swift | 70 ++++++++++++++++++ 18 files changed, 274 insertions(+), 139 deletions(-) create mode 100644 Amplify/Core/Support/AmplifyTaskExecution.swift create mode 100644 AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift create mode 100644 AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageRemoveTask.swift delete mode 100644 AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceConfigureTests.swift delete mode 100644 AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceDeleteBehaviorTests.swift delete mode 100644 AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceEscapeHatchBehaviorTests.swift delete mode 100644 AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceListBehaviorTests.swift delete mode 100644 AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceMultiPartUploadBehaviorTests.swift delete mode 100644 AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceTestBase.swift delete mode 100644 AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Service/Storage/AWSS3StorageServiceUploadBehaviorTests.swift create mode 100644 AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift diff --git a/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift index ace12f1218..f0e0310265 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: 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: StoragePath, options: Options) { + self.key = "" + self.options = options + self.path = path } } 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/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift index fde6e7ccf4..fede94948d 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift @@ -245,16 +245,12 @@ extension AWSS3StoragePlugin { options: StorageRemoveOperation.Request.Options? = nil ) async throws -> String { let options = options ?? StorageRemoveRequest.Options() - let path = "" //TODO: resolve path - let request = StorageRemoveRequest(key: path, options: options) - let operation = AWSS3StorageRemoveOperation(request, - storageConfiguration: storageConfiguration, - storageService: storageService, - authService: authService) - let taskAdapter = AmplifyOperationTaskAdapter(operation: operation) - queue.addOperation(operation) - - return try await taskAdapter.value + let request = StorageRemoveRequest(path: path, options: options) + let task = AWSS3StorageRemoveTask( + request, + storageConfiguration: storageConfiguration, + storageBehaviour: storageService) + return try await task.value } public func list( 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/Service/Storage/AWSS3StorageServiceBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageServiceBehavior.swift index 21ae5df171..b1e274a609 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 @@ -67,6 +73,7 @@ protocol AWSS3StorageServiceBehavior { 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..94bd595354 --- /dev/null +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift @@ -0,0 +1,34 @@ +// +// 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() async throws -> String { + if self is IdentityIdStoragePath { + let authService = AWSAuthService() + let identityId = try await authService.getIdentityID() + let path = pathResolver(identityId) + try validate(path) + return path + } else { + let path = pathResolver("") + try validate(path) + return path + } + } + + func validate(_ path: String) throws { + if !path.hasPrefix("/") { + let errorDescription = "Invalid StoragePath specified." + let recoverySuggestion = "Please specify a valid StoragePath that contains the prefix /." + throw StorageError.validation(path, errorDescription, recoverySuggestion, nil) + } + } +} 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/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/Request/AWSS3StorageRemoveRequestTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageRemoveRequestTests.swift index 76d7cfbff9..f5a741846c 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageRemoveRequestTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageRemoveRequestTests.swift @@ -9,6 +9,7 @@ import XCTest import Amplify @testable import AWSS3StoragePlugin +// TODO: [HS] Add path validation test cases once storage path extension is merged. class AWSS3StorageRemoveRequestTests: XCTestCase { let testTargetIdentityId = "TestTargetIdentityId" 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/AWSS3StorageRemoveTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift new file mode 100644 index 0000000000..22a96d6de9 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift @@ -0,0 +1,70 @@ +// +// 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))") + } + } + +} From 326ba60447b010bf1bf533e8a32062cfdf487357 Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Mon, 18 Mar 2024 16:15:06 -0500 Subject: [PATCH 03/26] feat(storage): update storage download api (#3561) --- .../Request/StorageDownloadDataRequest.swift | 14 ++ .../Request/StorageDownloadFileRequest.swift | 15 ++ .../Request/StorageRemoveRequest.swift | 4 +- .../Storage/Result/StorageListResult.swift | 6 +- .../StorageCategory+ClientBehavior.swift | 14 +- .../Storage/StorageCategoryBehavior.swift | 14 +- Amplify/Categories/Storage/StoragePath.swift | 19 +- ...SS3StoragePlugin+AsyncClientBehavior.swift | 20 +- .../AWSS3PluginPrefixResolver.swift | 6 +- .../AWSS3StorageDownloadDataOperation.swift | 15 +- .../AWSS3StorageDownloadFileOperation.swift | 24 ++- .../StorageDownloadDataRequest+Validate.swift | 5 + .../StorageDownloadFileRequest+Validate.swift | 5 + .../Internal/StoragePath+Extensions.swift | 34 +++- ...SS3StorageDownloadFileOperationTests.swift | 174 ++++++++++++++++++ .../AWSS3StorageGetDataOperationTests.swift | 165 +++++++++++++++++ ...AWSS3StorageDownloadFileRequestTests.swift | 18 +- .../AWSS3StorageGetDataRequestTests.swift | 18 +- .../Mocks/MockStorageCategoryPlugin.swift | 18 +- .../Hub/AmplifyOperationHubTests.swift | 14 +- .../Storage/StoragePathTests.swift | 35 ++++ 21 files changed, 555 insertions(+), 82 deletions(-) create mode 100644 AmplifyTests/CategoryTests/Storage/StoragePathTests.swift diff --git a/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift index 2c550b6b6d..91a85e20b0 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 `StoragePath` instead") public let key: String /// Options to adjust the behavior of this request, including plugin-options @@ -24,9 +30,17 @@ public struct StorageDownloadDataRequest: AmplifyOperationRequest { public let options: Options /// - Tag: StorageDownloadDataRequest.key + @available(*, deprecated, message: "Use init(path:local:options)") public init(key: String, options: Options) { self.key = key self.options = options + self.path = nil + } + + public init(path: any StoragePath, options: Options) { + self.key = "" + self.options = options + self.path = path } } diff --git a/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift index 4a665444dc..1738ea0212 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 `StoragePath` instead") public let key: String /// The local file to download the object to @@ -29,10 +35,19 @@ 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 + } + + public init(path: any StoragePath, local: URL, options: Options) { + self.key = "" + self.local = local + self.options = options + self.path = path } } diff --git a/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift index f0e0310265..31ae1de4f3 100644 --- a/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift +++ b/Amplify/Categories/Storage/Operation/Request/StorageRemoveRequest.swift @@ -20,7 +20,7 @@ public struct StorageRemoveRequest: AmplifyOperationRequest { /// The unique path for the object in storage /// /// - Tag: StorageRemoveRequest.path - public let path: StoragePath? + public let path: (any StoragePath)? /// Options to adjust the behavior of this request, including plugin-options /// @@ -36,7 +36,7 @@ public struct StorageRemoveRequest: AmplifyOperationRequest { } /// - Tag: StorageRemoveRequest.init - public init(path: StoragePath, options: Options) { + public init(path: any StoragePath, options: Options) { self.key = "" self.options = options self.path = path diff --git a/Amplify/Categories/Storage/Result/StorageListResult.swift b/Amplify/Categories/Storage/Result/StorageListResult.swift index 45777ae086..7294945b9f 100644 --- a/Amplify/Categories/Storage/Result/StorageListResult.swift +++ b/Amplify/Categories/Storage/Result/StorageListResult.swift @@ -45,7 +45,7 @@ extension StorageListResult { /// The path of the object in storage. /// /// - Tag: StorageListResultItem.path - public let path: StoragePath + public let path: String /// The unique identifier of the object in storage. /// @@ -90,11 +90,11 @@ extension StorageListResult { self.eTag = eTag self.lastModified = lastModified self.pluginResults = pluginResults - self.path = StringStoragePath(pathResolver: { _ in return "" }) + self.path = "" } public init( - path: StoragePath, + path: String, key: String, size: Int? = nil, eTag: String? = nil, diff --git a/Amplify/Categories/Storage/StorageCategory+ClientBehavior.swift b/Amplify/Categories/Storage/StorageCategory+ClientBehavior.swift index 22a76ea0ca..55b69bbe43 100644 --- a/Amplify/Categories/Storage/StorageCategory+ClientBehavior.swift +++ b/Amplify/Categories/Storage/StorageCategory+ClientBehavior.swift @@ -19,7 +19,7 @@ extension StorageCategory: StorageCategoryBehavior { @discardableResult public func getURL( - path: StoragePath, + path: any StoragePath, options: StorageGetURLOperation.Request.Options? = nil ) async throws -> URL { try await plugin.getURL(path: path, options: options) @@ -35,7 +35,7 @@ extension StorageCategory: StorageCategoryBehavior { @discardableResult public func downloadData( - path: StoragePath, + path: any StoragePath, options: StorageDownloadDataOperation.Request.Options? = nil ) -> StorageDownloadDataTask { plugin.downloadData(path: path, options: options) @@ -52,7 +52,7 @@ extension StorageCategory: StorageCategoryBehavior { @discardableResult public func downloadFile( - path: StoragePath, + path: any StoragePath, local: URL, options: StorageDownloadFileOperation.Request.Options? = nil ) -> StorageDownloadFileTask { @@ -70,7 +70,7 @@ extension StorageCategory: StorageCategoryBehavior { @discardableResult public func uploadData( - path: StoragePath, + path: any StoragePath, data: Data, options: StorageUploadDataOperation.Request.Options? = nil ) -> StorageUploadDataTask { @@ -88,7 +88,7 @@ extension StorageCategory: StorageCategoryBehavior { @discardableResult public func uploadFile( - path: StoragePath, + path: any StoragePath, local: URL, options: StorageUploadFileOperation.Request.Options? = nil ) -> StorageUploadFileTask { @@ -105,7 +105,7 @@ extension StorageCategory: StorageCategoryBehavior { @discardableResult public func remove( - path: StoragePath, + path: any StoragePath, options: StorageRemoveRequest.Options? = nil ) async throws -> String { try await plugin.remove(path: path, options: options) @@ -120,7 +120,7 @@ extension StorageCategory: StorageCategoryBehavior { @discardableResult public func list( - path: StoragePath, + path: any StoragePath, options: StorageListOperation.Request.Options? = nil ) async throws -> StorageListResult { try await plugin.list(path: path, options: options) diff --git a/Amplify/Categories/Storage/StorageCategoryBehavior.swift b/Amplify/Categories/Storage/StorageCategoryBehavior.swift index 1d519cd898..0b933d4dfc 100644 --- a/Amplify/Categories/Storage/StorageCategoryBehavior.swift +++ b/Amplify/Categories/Storage/StorageCategoryBehavior.swift @@ -39,7 +39,7 @@ public protocol StorageCategoryBehavior { /// - Tag: StorageCategoryBehavior.getURL @discardableResult func getURL( - path: StoragePath, + path: any StoragePath, options: StorageGetURLOperation.Request.Options? ) async throws -> URL @@ -65,7 +65,7 @@ public protocol StorageCategoryBehavior { /// /// - Tag: StorageCategoryBehavior.downloadData func downloadData( - path: StoragePath, + path: any StoragePath, options: StorageDownloadDataOperation.Request.Options? ) -> StorageDownloadDataTask @@ -97,7 +97,7 @@ public protocol StorageCategoryBehavior { /// - Tag: StorageCategoryBehavior.downloadFile @discardableResult func downloadFile( - path: StoragePath, + path: any StoragePath, local: URL, options: StorageDownloadFileOperation.Request.Options? ) -> StorageDownloadFileTask @@ -130,7 +130,7 @@ public protocol StorageCategoryBehavior { /// - Tag: StorageCategoryBehavior.uploadData @discardableResult func uploadData( - path: StoragePath, + path: any StoragePath, data: Data, options: StorageUploadDataOperation.Request.Options? ) -> StorageUploadDataTask @@ -163,7 +163,7 @@ public protocol StorageCategoryBehavior { /// - Tag: StorageCategoryBehavior.uploadFile @discardableResult func uploadFile( - path: StoragePath, + path: any StoragePath, local: URL, options: StorageUploadFileOperation.Request.Options? ) -> StorageUploadFileTask @@ -193,7 +193,7 @@ public protocol StorageCategoryBehavior { /// - Tag: StorageCategoryBehavior.remove @discardableResult func remove( - path: StoragePath, + path: any StoragePath, options: StorageRemoveOperation.Request.Options? ) async throws -> String @@ -218,7 +218,7 @@ public protocol StorageCategoryBehavior { /// - Tag: StorageCategoryBehavior.list @discardableResult func list( - path: StoragePath, + path: any StoragePath, options: StorageListOperation.Request.Options? ) async throws -> StorageListResult diff --git a/Amplify/Categories/Storage/StoragePath.swift b/Amplify/Categories/Storage/StoragePath.swift index d011e3e110..b3cf55867a 100644 --- a/Amplify/Categories/Storage/StoragePath.swift +++ b/Amplify/Categories/Storage/StoragePath.swift @@ -7,24 +7,25 @@ import Foundation -public typealias StoragePathResolver = (String) -> String +public typealias IdentityIDPathResolver = (String) -> String /// Protocol that provides a closure to resolve the storage path. /// /// - Tag: StoragePath public protocol StoragePath { - var pathResolver: StoragePathResolver { get } + associatedtype Input + var resolve: (Input) -> String { get } } public extension StoragePath where Self == StringStoragePath { static func fromString(_ path: String) -> Self { - return StringStoragePath(pathResolver: { _ in return path }) + return StringStoragePath(resolve: { _ in return path }) } } -public extension StoragePath where Self == IdentityIdStoragePath { - static func fromIdentityId(_ identityIdPathResolver: @escaping StoragePathResolver) -> Self { - return IdentityIdStoragePath(pathResolver: identityIdPathResolver) +public extension StoragePath where Self == IdentityIDStoragePath { + static func fromIdentityID(_ identityIdPathResolver: @escaping IdentityIDPathResolver) -> Self { + return IdentityIDStoragePath(resolve: identityIdPathResolver) } } @@ -32,13 +33,13 @@ public extension StoragePath where Self == IdentityIdStoragePath { /// /// - Tag: StringStoragePath public struct StringStoragePath: StoragePath { - public let pathResolver: StoragePathResolver + 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 pathResolver: StoragePathResolver +public struct IdentityIDStoragePath: StoragePath { + public let resolve: IdentityIDPathResolver } diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift index fede94948d..8dd2bcb82e 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift @@ -46,7 +46,7 @@ extension AWSS3StoragePlugin { } public func getURL( - path: StoragePath, + path: any StoragePath, options: StorageGetURLOperation.Request.Options? = nil ) async throws -> URL { let options = options ?? StorageGetURLRequest.Options() @@ -78,12 +78,11 @@ extension AWSS3StoragePlugin { } public func downloadData( - path: StoragePath, + path: any StoragePath, options: StorageDownloadDataOperation.Request.Options? = nil ) -> StorageDownloadDataTask { let options = options ?? StorageDownloadDataRequest.Options() - let path = "" //TODO: resolve path - let request = StorageDownloadDataRequest(key: path, options: options) + let request = StorageDownloadDataRequest(path: path, options: options) let operation = AWSS3StorageDownloadDataOperation(request, storageConfiguration: storageConfiguration, storageService: storageService, @@ -131,13 +130,12 @@ extension AWSS3StoragePlugin { @discardableResult public func downloadFile( - path: StoragePath, + path: any StoragePath, local: URL, options: StorageDownloadFileOperation.Request.Options? = nil ) -> StorageDownloadFileTask { let options = options ?? StorageDownloadFileRequest.Options() - let path = "" //TODO: resolve path - let request = StorageDownloadFileRequest(key: path, local: local, options: options) + let request = StorageDownloadFileRequest(path: path, local: local, options: options) let operation = AWSS3StorageDownloadFileOperation(request, storageConfiguration: storageConfiguration, storageService: storageService, @@ -168,7 +166,7 @@ extension AWSS3StoragePlugin { @discardableResult public func uploadData( - path: StoragePath, + path: any StoragePath, data: Data, options: StorageUploadDataOperation.Request.Options? = nil ) -> StorageUploadDataTask { @@ -205,7 +203,7 @@ extension AWSS3StoragePlugin { @discardableResult public func uploadFile( - path: StoragePath, + path: any StoragePath, local: URL, options: StorageUploadFileOperation.Request.Options? = nil ) -> StorageUploadFileTask { @@ -241,7 +239,7 @@ extension AWSS3StoragePlugin { @discardableResult public func remove( - path: StoragePath, + path: any StoragePath, options: StorageRemoveOperation.Request.Options? = nil ) async throws -> String { let options = options ?? StorageRemoveRequest.Options() @@ -269,7 +267,7 @@ extension AWSS3StoragePlugin { } public func list( - path: StoragePath, + path: any StoragePath, options: StorageListRequest.Options? = nil ) async throws -> StorageListResult { let options = options ?? StorageListRequest.Options() diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3PluginPrefixResolver.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3PluginPrefixResolver.swift index a9ad565410..d7c970f15c 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3PluginPrefixResolver.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Configuration/AWSS3PluginPrefixResolver.swift @@ -12,7 +12,7 @@ import AWSPluginsCore /// Resolves the final prefix prepended to the S3 key for a given request. /// /// - Tag: AWSS3PluginPrefixResolver -@available(*, deprecated, message: "Use `StoragePath` instead") +@available(*, deprecated) public protocol AWSS3PluginPrefixResolver { /// - Tag: AWSS3PluginPrefixResolver.resolvePrefix func resolvePrefix(for accessLevel: StorageAccessLevel, @@ -22,7 +22,7 @@ public protocol AWSS3PluginPrefixResolver { /// Convenience resolver. Resolves the provided key as-is, with no manipulation /// /// - Tag: PassThroughPrefixResolver -@available(*, deprecated, message: "Use `StoragePath` instead") +@available(*, deprecated) public struct PassThroughPrefixResolver: AWSS3PluginPrefixResolver { public func resolvePrefix(for accessLevel: StorageAccessLevel, targetIdentityId: String?) async throws -> String { @@ -33,7 +33,7 @@ public struct PassThroughPrefixResolver: AWSS3PluginPrefixResolver { /// AWSS3StoragePlugin default logic /// /// - Tag: StorageAccessLevelAwarePrefixResolver -@available(*, deprecated, message: "Use `StoragePath` instead") +@available(*, deprecated) struct StorageAccessLevelAwarePrefixResolver { let authService: AWSAuthServiceBehavior 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/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/Support/Internal/StoragePath+Extensions.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift index 94bd595354..e8fe27bd60 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift @@ -10,25 +10,41 @@ import Amplify import AWSPluginsCore extension StoragePath { - func resolvePath() async throws -> String { - if self is IdentityIdStoragePath { - let authService = AWSAuthService() - let identityId = try await authService.getIdentityID() - let path = pathResolver(identityId) + 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) try validate(path) return path - } else { - let path = pathResolver("") + } 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) 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.hasPrefix("/") { let errorDescription = "Invalid StoragePath specified." - let recoverySuggestion = "Please specify a valid StoragePath that contains the prefix /." - throw StorageError.validation(path, errorDescription, recoverySuggestion, nil) + let recoverySuggestion = "Please specify a valid StoragePath that contains the prefix / " + throw StorageError.validation("path", errorDescription, recoverySuggestion, nil) } } } diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift index b87d1cd939..d514ef2f69 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift @@ -182,5 +182,179 @@ 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 testDownloadDataOperationStringStoragePathValidationError() { + 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 IdentityIDStoragePath + /// Then: The operation will fail with a validation error + func testDownloadDataOperationIdentityIDStoragePathValidationError() { + 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 testDownloadDataOperationCustomStoragePathValidationError() { + 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 testDownloadDataOperationWithIdentityIDStoragePathSucceeds() async throws { + let path = IdentityIDStoragePath(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) + } + // 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..d3ec457abc 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift @@ -174,5 +174,170 @@ 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 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 { + 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/\(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) + } + // 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/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/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift b/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift index cccfabe016..bc6255567f 100644 --- a/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift +++ b/AmplifyTestCommon/Mocks/MockStorageCategoryPlugin.swift @@ -180,7 +180,7 @@ class MockStorageCategoryPlugin: MessageReporter, StorageCategoryPlugin { false } - func getURL(path: StoragePath, options: StorageGetURLRequest.Options?) async throws -> URL { + 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) @@ -189,25 +189,25 @@ class MockStorageCategoryPlugin: MessageReporter, StorageCategoryPlugin { return try await taskAdapter.value } - func downloadData(path: StoragePath, options: StorageDownloadDataRequest.Options?) -> StorageDownloadDataTask { + func downloadData(path: any StoragePath, options: StorageDownloadDataRequest.Options?) -> StorageDownloadDataTask { notify("downloadData") let options = options ?? StorageDownloadDataRequest.Options() - let request = StorageDownloadDataRequest(key: key, options: options) + let request = StorageDownloadDataRequest(path: path, options: options) let operation = MockStorageDownloadDataOperation(request: request) let taskAdapter = AmplifyInProcessReportingOperationTaskAdapter(operation: operation) return taskAdapter } - func downloadFile(path: StoragePath, local: URL, options: StorageDownloadFileRequest.Options?) -> StorageDownloadFileTask { + func downloadFile(path: any StoragePath, local: URL, options: StorageDownloadFileRequest.Options?) -> StorageDownloadFileTask { notify("downloadFile") let options = options ?? StorageDownloadFileRequest.Options() - let request = StorageDownloadFileRequest(key: key, local: local, options: 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: StoragePath, data: Data, options: StorageUploadDataRequest.Options?) -> StorageUploadDataTask { + 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) @@ -216,7 +216,7 @@ class MockStorageCategoryPlugin: MessageReporter, StorageCategoryPlugin { return taskAdapter } - func uploadFile(path: StoragePath, local: URL, options: StorageUploadFileRequest.Options?) -> StorageUploadFileTask { + 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) @@ -225,7 +225,7 @@ class MockStorageCategoryPlugin: MessageReporter, StorageCategoryPlugin { return taskAdapter } - func remove(path: StoragePath, options: StorageRemoveRequest.Options?) async throws -> String { + 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) @@ -234,7 +234,7 @@ class MockStorageCategoryPlugin: MessageReporter, StorageCategoryPlugin { return try await taskAdapter.value } - func list(path: StoragePath, options: StorageListRequest.Options?) async throws -> StorageListResult { + func list(path: any StoragePath, options: StorageListRequest.Options?) async throws -> StorageListResult { notify("list") let options = options ?? StorageListRequest.Options() let request = StorageListRequest(options: options) diff --git a/AmplifyTests/CategoryTests/Hub/AmplifyOperationHubTests.swift b/AmplifyTests/CategoryTests/Hub/AmplifyOperationHubTests.swift index 6a81984fcb..0a5b3a01d1 100644 --- a/AmplifyTests/CategoryTests/Hub/AmplifyOperationHubTests.swift +++ b/AmplifyTests/CategoryTests/Hub/AmplifyOperationHubTests.swift @@ -301,7 +301,7 @@ class MockDispatchingStoragePlugin: StorageCategoryPlugin { @discardableResult func getURL( - path: StoragePath, + path: any StoragePath, options: StorageGetURLOperation.Request.Options? ) async throws -> URL { let options = options ?? StorageGetURLRequest.Options() @@ -313,7 +313,7 @@ class MockDispatchingStoragePlugin: StorageCategoryPlugin { @discardableResult public func downloadData( - path: StoragePath, + path: any StoragePath, options: StorageDownloadDataOperation.Request.Options? = nil ) -> StorageDownloadDataTask { let options = options ?? StorageDownloadDataRequest.Options() @@ -325,7 +325,7 @@ class MockDispatchingStoragePlugin: StorageCategoryPlugin { @discardableResult public func downloadFile( - path: StoragePath, + path: any StoragePath, local: URL, options: StorageDownloadFileOperation.Request.Options? ) -> StorageDownloadFileTask { @@ -338,7 +338,7 @@ class MockDispatchingStoragePlugin: StorageCategoryPlugin { @discardableResult public func uploadData( - path: StoragePath, + path: any StoragePath, data: Data, options: StorageUploadDataOperation.Request.Options? ) -> StorageUploadDataTask { @@ -351,7 +351,7 @@ class MockDispatchingStoragePlugin: StorageCategoryPlugin { @discardableResult public func uploadFile( - path: StoragePath, + path: any StoragePath, local: URL, options: StorageUploadFileOperation.Request.Options? ) -> StorageUploadFileTask { @@ -364,7 +364,7 @@ class MockDispatchingStoragePlugin: StorageCategoryPlugin { @discardableResult public func remove( - path: StoragePath, + path: any StoragePath, options: StorageRemoveRequest.Options? = nil ) async throws -> String { let options = options ?? StorageRemoveRequest.Options() @@ -376,7 +376,7 @@ class MockDispatchingStoragePlugin: StorageCategoryPlugin { @discardableResult func list( - path: StoragePath, + path: any StoragePath, options: StorageListOperation.Request.Options? ) async throws -> StorageListResult { let options = options ?? StorageListRequest.Options() 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) + } +} From c13dfbe4db827cc20d23949db605b2d95862f1d5 Mon Sep 17 00:00:00 2001 From: Harsh <6162866+harsh62@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:26:40 -0400 Subject: [PATCH 04/26] feat(Storage): Refactor GetURL API to include `path` (#3573) --- .../Request/StorageGetURLRequest.swift | 47 ++++++-- ...SS3StoragePlugin+AsyncClientBehavior.swift | 30 +---- ...orageService+GetPreSignedURLBehavior.swift | 2 +- .../Tasks/AWSS3torageGetURLTask.swift | 68 +++++++++++ .../AWSS3StorageRemoveRequestTests.swift | 1 - .../Tasks/AWSS3StorageGetURLTaskTests.swift | 106 ++++++++++++++++++ .../Tasks/AWSS3StorageRemoveTaskTests.swift | 27 +++++ 7 files changed, 243 insertions(+), 38 deletions(-) create mode 100644 AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3torageGetURLTask.swift create mode 100644 AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift diff --git a/Amplify/Categories/Storage/Operation/Request/StorageGetURLRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageGetURLRequest.swift index 453bbf847a..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,29 +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 @@ -64,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, @@ -77,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/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift index 8dd2bcb82e..8513bf98d6 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift @@ -50,31 +50,11 @@ extension AWSS3StoragePlugin { options: StorageGetURLOperation.Request.Options? = nil ) async throws -> URL { let options = options ?? StorageGetURLRequest.Options() - let path = "" //TODO: resolve path - let request = StorageGetURLRequest(key: path, options: options) - if let error = request.validate() { - throw error - } - let prefixResolver = storageConfiguration.prefixResolver ?? StorageAccessLevelAwarePrefixResolver(authService: authService) - let prefix = try await prefixResolver.resolvePrefix(for: options.accessLevel, - targetIdentityId: options.targetIdentityId) - let serviceKey = prefix + request.key - if let pluginOptions = options.pluginOptions as? AWSStorageGetURLOptions, pluginOptions.validateObjectExistence { - try await storageService.validateObjectExistence(serviceKey: serviceKey) - } - let accelerate = try AWSS3PluginOptions.accelerateValue( - pluginOptions: options.pluginOptions) - let result = try await storageService.getPreSignedURL( - serviceKey: serviceKey, - signingOperation: .getObject, - metadata: nil, - accelerate: accelerate, - expires: options.expires) - - let channel = HubChannel(from: categoryType) - let payload = HubPayload(eventName: HubPayload.EventName.Storage.getURL, context: options, data: result) - Amplify.Hub.dispatch(to: channel, payload: payload) - return result + let request = StorageGetURLRequest(path: path, options: options) + let task = AWSS3StorageGetURLTask( + request, + storageBehaviour: storageService) + return try await task.value } public func downloadData( 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/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/Request/AWSS3StorageRemoveRequestTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageRemoveRequestTests.swift index f5a741846c..76d7cfbff9 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageRemoveRequestTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Request/AWSS3StorageRemoveRequestTests.swift @@ -9,7 +9,6 @@ import XCTest import Amplify @testable import AWSS3StoragePlugin -// TODO: [HS] Add path validation test cases once storage path extension is merged. class AWSS3StorageRemoveRequestTests: XCTestCase { let testTargetIdentityId = "TestTargetIdentityId" diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift new file mode 100644 index 0000000000..575b2eaf49 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift @@ -0,0 +1,106 @@ +// +// 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 testRemoveTaskSuccess() 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 testRemoveTaskNoBucket() 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`") + } + } + +} diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift index 22a96d6de9..1ac8651b43 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift @@ -67,4 +67,31 @@ class AWSS3StorageRemoveTaskTests: XCTestCase { } } + /// - 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`") + } + } + } From 597473a1978feb7938e265d5f6ef353899bfe6e0 Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:58:35 -0500 Subject: [PATCH 05/26] feat(storage): update storage upload APIs to use storage path (#3574) --- .../Request/StorageDownloadDataRequest.swift | 14 +- .../Request/StorageDownloadFileRequest.swift | 12 +- .../Request/StorageUploadDataRequest.swift | 31 ++- .../Request/StorageUploadFileRequest.swift | 31 ++- .../AWSS3StorageUploadDataOperation.swift | 16 +- .../AWSS3StorageUploadFileOperation.swift | 16 +- .../StorageUploadDataRequest+Validate.swift | 6 + .../StorageUploadFileRequest+Validate.swift | 6 + ...SS3StorageDownloadFileOperationTests.swift | 13 +- .../AWSS3StorageGetDataOperationTests.swift | 28 +- .../AWSS3StoragePutDataOperationTests.swift | 191 ++++++++++++++ ...AWSS3StorageUploadFileOperationTests.swift | 243 ++++++++++++++++++ .../AWSS3StoragePutDataRequestTests.swift | 19 +- .../AWSS3StorageUploadFileRequestTests.swift | 22 +- 14 files changed, 613 insertions(+), 35 deletions(-) diff --git a/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift index 91a85e20b0..1a8ed260b9 100644 --- a/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift +++ b/Amplify/Categories/Storage/Operation/Request/StorageDownloadDataRequest.swift @@ -21,7 +21,7 @@ public struct StorageDownloadDataRequest: AmplifyOperationRequest { /// The unique identifier for the object in storage /// /// - Tag: StorageDownloadDataRequest.key - @available(*, deprecated, message: "Use `StoragePath` instead") + @available(*, deprecated, message: "Use `path` instead of `key`") public let key: String /// Options to adjust the behavior of this request, including plugin-options @@ -29,7 +29,7 @@ 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 @@ -37,6 +37,7 @@ public struct StorageDownloadDataRequest: AmplifyOperationRequest { self.path = nil } + /// - Tag: StorageDownloadDataRequest.init public init(path: any StoragePath, options: Options) { self.key = "" self.options = options @@ -89,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) { @@ -96,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 1738ea0212..7ec34c222f 100644 --- a/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift +++ b/Amplify/Categories/Storage/Operation/Request/StorageDownloadFileRequest.swift @@ -21,7 +21,7 @@ public struct StorageDownloadFileRequest: AmplifyOperationRequest { /// The unique identifier for the object in storage /// /// - Tag: StorageDownloadFileRequest.key - @available(*, deprecated, message: "Use `StoragePath` instead") + @available(*, deprecated, message: "Use `path` instead of `key`") public let key: String /// The local file to download the object to @@ -43,6 +43,7 @@ public struct StorageDownloadFileRequest: AmplifyOperationRequest { self.path = nil } + /// - Tag: StorageDownloadFileRequest.init public init(path: any StoragePath, local: URL, options: Options) { self.key = "" self.local = local @@ -78,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) { @@ -85,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/StorageUploadDataRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageUploadDataRequest.swift index 522f34192b..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 } } @@ -73,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 43b786cf2c..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 } } @@ -70,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/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadDataOperation.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadDataOperation.swift index cbfd7ee52e..6e4288445a 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadDataOperation.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadDataOperation.swift @@ -84,13 +84,21 @@ 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) + } 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( diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadFileOperation.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadFileOperation.swift index 617602388a..92e63b5b4d 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadFileOperation.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadFileOperation.swift @@ -108,13 +108,19 @@ 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) + } 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( 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/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift index d514ef2f69..4a54f12812 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift @@ -185,7 +185,7 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { /// 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 testDownloadDataOperationStringStoragePathValidationError() { + func testDownloadFileOperationStringStoragePathValidationError() { let path = StringStoragePath(resolve: { _ in return "my/path" }) let request = StorageDownloadFileRequest(path: path, local: testURL, @@ -217,7 +217,7 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { /// 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 testDownloadDataOperationIdentityIDStoragePathValidationError() { + func testDownloadFileOperationIdentityIDStoragePathValidationError() { let path = IdentityIDStoragePath(resolve: { _ in return "my/path" }) let request = StorageDownloadFileRequest(path: path, local: testURL, @@ -249,7 +249,7 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { /// 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 testDownloadDataOperationCustomStoragePathValidationError() { + func testDownloadFileOperationCustomStoragePathValidationError() { let path = InvalidCustomStoragePath(resolve: { _ in return "my/path" }) let request = StorageDownloadFileRequest(path: path, local: testURL, @@ -320,8 +320,9 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { /// Given: Storage Download File Operation /// When: The operation is executed with a request that has an valid IdentityIDStoragePath /// Then: The operation will succeed - func testDownloadDataOperationWithIdentityIDStoragePathSucceeds() async throws { - let path = IdentityIDStoragePath(resolve: { _ in return "/public/\(self.testKey)" }) + 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)), @@ -353,7 +354,7 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { await waitForExpectations(timeout: 1) XCTAssertTrue(operation.isFinished) - mockStorageService.verifyDownload(serviceKey: "/public/\(self.testKey)", fileURL: url) + 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 d3ec457abc..8f2fbb440b 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift @@ -185,17 +185,18 @@ class AWSS3StorageDownloadDataOperationTests: AWSS3StorageOperationTestBase { 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") - } + 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() @@ -302,12 +303,13 @@ class AWSS3StorageDownloadDataOperationTests: AWSS3StorageOperationTestBase { /// 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/\(self.testKey)" }) + 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") @@ -332,7 +334,7 @@ class AWSS3StorageDownloadDataOperationTests: AWSS3StorageOperationTestBase { await fulfillment(of: [inProcessInvoked, completeInvoked], timeout: 1) XCTAssertTrue(operation.isFinished) - mockStorageService.verifyDownload(serviceKey: "/public/\(self.testKey)", fileURL: nil) + 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? diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift index c93b6975f3..d500a4450e 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift @@ -219,5 +219,196 @@ 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 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..0105e8e721 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift @@ -255,5 +255,248 @@ 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 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/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() { // From 836441298d6629ea614726adfb263038e16faac2 Mon Sep 17 00:00:00 2001 From: Harsh <6162866+harsh62@users.noreply.github.com> Date: Thu, 21 Mar 2024 12:09:25 -0400 Subject: [PATCH 06/26] feat(Storage): Refactor list objects API to include `path` (#3580) * feat(Storage): Refactor list objects API to include `path` * working on review comments --- .../Request/StorageListRequest.swift | 13 +++ ...SS3StoragePlugin+AsyncClientBehavior.swift | 14 +-- .../Storage/AWSS3StorageServiceBehavior.swift | 1 + .../Tasks/AWSS3StorageListObjectsTask.swift | 73 ++++++++++++ .../Tasks/AWSS3StorageGetURLTaskTests.swift | 4 +- .../AWSS3StorageListObjectsTaskTests.swift | 106 ++++++++++++++++++ 6 files changed, 201 insertions(+), 10 deletions(-) create mode 100644 AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift create mode 100644 AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift diff --git a/Amplify/Categories/Storage/Operation/Request/StorageListRequest.swift b/Amplify/Categories/Storage/Operation/Request/StorageListRequest.swift index 5689e6b47c..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 } } diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift index 8513bf98d6..06f2db02a6 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift @@ -251,14 +251,12 @@ extension AWSS3StoragePlugin { options: StorageListRequest.Options? = nil ) async throws -> StorageListResult { let options = options ?? StorageListRequest.Options() - let prefix = "" //TODO: resolve path - let result = try await storageService.list(prefix: prefix, options: options) - - let channel = HubChannel(from: categoryType) - let payload = HubPayload(eventName: HubPayload.EventName.Storage.list, context: options, data: result) - Amplify.Hub.dispatch(to: channel, payload: payload) - - return result + 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 { diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageServiceBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageServiceBehavior.swift index b1e274a609..6491546d8d 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageServiceBehavior.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageServiceBehavior.swift @@ -70,6 +70,7 @@ protocol AWSS3StorageServiceBehavior { accelerate: Bool?, onEvent: @escaping StorageServiceMultiPartUploadEventHandler) + @available(*, deprecated, message: "Use `AWSS3StorageListObjectsTask` instead") func list(prefix: String, options: StorageListRequest.Options) async throws -> StorageListResult diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift new file mode 100644 index 0000000000..6da4ce23d0 --- /dev/null +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift @@ -0,0 +1,73 @@ +// +// 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 removing an object", + "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 key = s3Object.key else { + throw StorageError.unknown("Missing key in response") + } + return StorageListResult.Item( + path: path, + key: key, + 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/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift index 575b2eaf49..4b74e24f01 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift @@ -19,7 +19,7 @@ class AWSS3StorageGetURLTaskTests: XCTestCase { /// - Given: A configured Storage GetURL Task with mocked service /// - When: AWSS3StorageGetURLTask value is invoked /// - Then: A URL should be returned. - func testRemoveTaskSuccess() async throws { + func testGetURLTaskSuccess() async throws { let somePath = "/path" let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()) @@ -42,7 +42,7 @@ class AWSS3StorageGetURLTaskTests: XCTestCase { /// - 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 testRemoveTaskNoBucket() async throws { + func testGetURLTaskNoBucket() async throws { let somePath = "/path" let serviceMock = MockAWSS3StorageService() diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift new file mode 100644 index 0000000000..f58fe03e7a --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift @@ -0,0 +1,106 @@ +// +// 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") + 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`") + } + } + +} From b8ec8d86ee4c0b0dd308213cc178c36c7b085ab9 Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Thu, 21 Mar 2024 13:21:38 -0500 Subject: [PATCH 07/26] chore(storage): update storage path validation rule (#3579) --- .../Support/Internal/StoragePath+Extensions.swift | 4 ++-- .../AWSS3StorageDownloadFileOperationTests.swift | 12 ++++++------ .../AWSS3StorageGetDataOperationTests.swift | 12 ++++++------ .../AWSS3StoragePutDataOperationTests.swift | 12 ++++++------ .../AWSS3StorageUploadFileOperationTests.swift | 12 ++++++------ .../Tasks/AWSS3StorageGetURLTaskTests.swift | 6 +++--- .../Tasks/AWSS3StorageListObjectsTaskTests.swift | 6 +++--- .../Tasks/AWSS3StorageRemoveTaskTests.swift | 8 ++++---- 8 files changed, 36 insertions(+), 36 deletions(-) diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift index e8fe27bd60..bdc2ebece4 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift @@ -41,9 +41,9 @@ extension StoragePath { } func validate(_ path: String) throws { - if !path.hasPrefix("/") { + if path.hasPrefix("/") { let errorDescription = "Invalid StoragePath specified." - let recoverySuggestion = "Please specify a valid StoragePath that contains the prefix / " + let recoverySuggestion = "Please specify a valid StoragePath that does not contain the prefix / " throw StorageError.validation("path", errorDescription, recoverySuggestion, nil) } } diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift index 4a54f12812..98b15f5954 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift @@ -186,7 +186,7 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { /// 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 path = StringStoragePath(resolve: { _ in return "/my/path" }) let request = StorageDownloadFileRequest(path: path, local: testURL, options: StorageDownloadFileRequest.Options()) @@ -218,7 +218,7 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { /// 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 path = IdentityIDStoragePath(resolve: { _ in return "/my/path" }) let request = StorageDownloadFileRequest(path: path, local: testURL, options: StorageDownloadFileRequest.Options()) @@ -282,7 +282,7 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { /// 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 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)), @@ -314,7 +314,7 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { await waitForExpectations(timeout: 1) XCTAssertTrue(operation.isFinished) - mockStorageService.verifyDownload(serviceKey: "/public/\(self.testKey)", fileURL: url) + mockStorageService.verifyDownload(serviceKey: "public/\(self.testKey)", fileURL: url) } /// Given: Storage Download File Operation @@ -322,7 +322,7 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { /// Then: The operation will succeed func testDownloadFileOperationWithIdentityIDStoragePathSucceeds() async throws { mockAuthService.identityId = testIdentityId - let path = IdentityIDStoragePath(resolve: { id in return "/public/\(id)/\(self.testKey)" }) + 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)), @@ -354,7 +354,7 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { await waitForExpectations(timeout: 1) XCTAssertTrue(operation.isFinished) - mockStorageService.verifyDownload(serviceKey: "/public/\(testIdentityId)/\(self.testKey)", fileURL: url) + 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 8f2fbb440b..23a28ee935 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift @@ -178,7 +178,7 @@ class AWSS3StorageDownloadDataOperationTests: AWSS3StorageOperationTestBase { /// 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 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, @@ -208,7 +208,7 @@ class AWSS3StorageDownloadDataOperationTests: AWSS3StorageOperationTestBase { /// 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 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, @@ -271,7 +271,7 @@ class AWSS3StorageDownloadDataOperationTests: AWSS3StorageOperationTestBase { StorageEvent.initiated(StorageTaskReference(task)), StorageEvent.inProcess(Progress()), StorageEvent.completed(Data())] - let path = StringStoragePath(resolve: { _ in return "/public/\(self.testKey)" }) + 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") @@ -296,7 +296,7 @@ class AWSS3StorageDownloadDataOperationTests: AWSS3StorageOperationTestBase { await fulfillment(of: [inProcessInvoked, completeInvoked], timeout: 1) XCTAssertTrue(operation.isFinished) - mockStorageService.verifyDownload(serviceKey: "/public/\(self.testKey)", fileURL: nil) + mockStorageService.verifyDownload(serviceKey: "public/\(self.testKey)", fileURL: nil) } /// Given: Storage Download Data Operation @@ -309,7 +309,7 @@ class AWSS3StorageDownloadDataOperationTests: AWSS3StorageOperationTestBase { StorageEvent.initiated(StorageTaskReference(task)), StorageEvent.inProcess(Progress()), StorageEvent.completed(Data())] - let path = IdentityIDStoragePath(resolve: { id in return "/public/\(id)/\(self.testKey)" }) + 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") @@ -334,7 +334,7 @@ class AWSS3StorageDownloadDataOperationTests: AWSS3StorageOperationTestBase { await fulfillment(of: [inProcessInvoked, completeInvoked], timeout: 1) XCTAssertTrue(operation.isFinished) - mockStorageService.verifyDownload(serviceKey: "/public/\(testIdentityId)/\(self.testKey)", fileURL: nil) + 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? diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift index d500a4450e..faeb9fc862 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift @@ -223,7 +223,7 @@ class AWSS3StorageUploadDataOperationTests: AWSS3StorageOperationTestBase { /// 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 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) @@ -254,7 +254,7 @@ class AWSS3StorageUploadDataOperationTests: AWSS3StorageOperationTestBase { /// 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 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) @@ -316,7 +316,7 @@ class AWSS3StorageUploadDataOperationTests: AWSS3StorageOperationTestBase { /// 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 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)), @@ -354,7 +354,7 @@ class AWSS3StorageUploadDataOperationTests: AWSS3StorageOperationTestBase { await waitForExpectations(timeout: 1) XCTAssertTrue(operation.isFinished) XCTAssertEqual(mockStorageService.uploadCalled, 1) - mockStorageService.verifyUpload(serviceKey: "/public/\(self.testKey)", + mockStorageService.verifyUpload(serviceKey: "public/\(self.testKey)", key: testKey, uploadSource: expectedUploadSource, contentType: testContentType, @@ -366,7 +366,7 @@ class AWSS3StorageUploadDataOperationTests: AWSS3StorageOperationTestBase { /// Then: The operation will succeed func testUploadDataOperationWithIdentityIDStoragePathSucceeds() async throws { mockAuthService.identityId = testIdentityId - let path = IdentityIDStoragePath(resolve: { id in return "/public/\(id)/\(self.testKey)" }) + 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)), @@ -403,7 +403,7 @@ class AWSS3StorageUploadDataOperationTests: AWSS3StorageOperationTestBase { await waitForExpectations(timeout: 1) XCTAssertTrue(operation.isFinished) XCTAssertEqual(mockStorageService.uploadCalled, 1) - mockStorageService.verifyUpload(serviceKey: "/public/\(testIdentityId)/\(testKey)", + mockStorageService.verifyUpload(serviceKey: "public/\(testIdentityId)/\(testKey)", key: testKey, uploadSource: expectedUploadSource, contentType: testContentType, diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift index 0105e8e721..87183415e6 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift @@ -259,7 +259,7 @@ class AWSS3StorageUploadFileOperationTests: AWSS3StorageOperationTestBase { /// 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" }) + let path = StringStoragePath(resolve: { _ in return "/my/path" }) mockAuthService.identityId = testIdentityId let task = StorageTransferTask(transferType: .upload(onEvent: { _ in }), bucket: "bucket", key: "key") mockStorageService.storageServiceUploadEvents = [ @@ -305,7 +305,7 @@ class AWSS3StorageUploadFileOperationTests: AWSS3StorageOperationTestBase { /// 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" }) + let path = IdentityIDStoragePath(resolve: { _ in return "/my/path" }) mockAuthService.identityId = testIdentityId let task = StorageTransferTask(transferType: .upload(onEvent: { _ in }), bucket: "bucket", key: "key") mockStorageService.storageServiceUploadEvents = [ @@ -397,7 +397,7 @@ class AWSS3StorageUploadFileOperationTests: AWSS3StorageOperationTestBase { /// 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)" }) + 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 = [ @@ -439,7 +439,7 @@ class AWSS3StorageUploadFileOperationTests: AWSS3StorageOperationTestBase { await waitForExpectations(timeout: 1) XCTAssertTrue(operation.isFinished) XCTAssertEqual(mockStorageService.uploadCalled, 1) - mockStorageService.verifyUpload(serviceKey: "/public/\(testKey)", + mockStorageService.verifyUpload(serviceKey: "public/\(testKey)", key: testKey, uploadSource: expectedUploadSource, contentType: testContentType, @@ -450,7 +450,7 @@ class AWSS3StorageUploadFileOperationTests: AWSS3StorageOperationTestBase { /// 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)" }) + 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 = [ @@ -491,7 +491,7 @@ class AWSS3StorageUploadFileOperationTests: AWSS3StorageOperationTestBase { await waitForExpectations(timeout: 1) XCTAssertTrue(operation.isFinished) XCTAssertEqual(mockStorageService.uploadCalled, 1) - mockStorageService.verifyUpload(serviceKey: "/public/\(testIdentityId)/\(testKey)", + mockStorageService.verifyUpload(serviceKey: "public/\(testIdentityId)/\(testKey)", key: testKey, uploadSource: expectedUploadSource, contentType: testContentType, diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift index 4b74e24f01..ccd5212bc8 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift @@ -21,7 +21,7 @@ class AWSS3StorageGetURLTaskTests: XCTestCase { /// - Then: A URL should be returned. func testGetURLTaskSuccess() async throws { - let somePath = "/path" + let somePath = "path" let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()) let serviceMock = MockAWSS3StorageService() @@ -43,7 +43,7 @@ class AWSS3StorageGetURLTaskTests: XCTestCase { /// - 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 somePath = "path" let serviceMock = MockAWSS3StorageService() serviceMock.getPreSignedURLHandler = { _, _, _ in @@ -74,7 +74,7 @@ class AWSS3StorageGetURLTaskTests: XCTestCase { /// - When: AWSS3StorageGetURLTask value is invoked /// - Then: A storage validation error should be returned func testGetURLTaskWithInvalidPath() async throws { - let somePath = "path" + let somePath = "/path" let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()) let serviceMock = MockAWSS3StorageService() diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift index f58fe03e7a..4ba7cff47c 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift @@ -31,7 +31,7 @@ class AWSS3StorageListObjectsTaskTests: XCTestCase { } let request = StorageListRequest( - path: StringStoragePath.fromString("/path"), options: .init()) + path: StringStoragePath.fromString("path"), options: .init()) let task = AWSS3StorageListObjectsTask( request, storageConfiguration: AWSS3StoragePluginConfiguration(), @@ -56,7 +56,7 @@ class AWSS3StorageListObjectsTaskTests: XCTestCase { } let request = StorageListRequest( - path: StringStoragePath.fromString("/path"), options: .init()) + path: StringStoragePath.fromString("path"), options: .init()) let task = AWSS3StorageListObjectsTask( request, storageConfiguration: AWSS3StoragePluginConfiguration(), @@ -83,7 +83,7 @@ class AWSS3StorageListObjectsTaskTests: XCTestCase { let serviceMock = MockAWSS3StorageService() let request = StorageListRequest( - path: StringStoragePath.fromString("path"), options: .init()) + path: StringStoragePath.fromString("/path"), options: .init()) let task = AWSS3StorageListObjectsTask( request, storageConfiguration: AWSS3StoragePluginConfiguration(), diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift index 1ac8651b43..047f130036 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift @@ -27,13 +27,13 @@ class AWSS3StorageRemoveTaskTests: XCTestCase { } let request = StorageRemoveRequest( - path: StringStoragePath.fromString("/path"), options: .init()) + path: StringStoragePath.fromString("path"), options: .init()) let task = AWSS3StorageRemoveTask( request, storageConfiguration: AWSS3StoragePluginConfiguration(), storageBehaviour: serviceMock) let value = try await task.value - XCTAssertEqual(value, "/path") + XCTAssertEqual(value, "path") } /// - Given: A configured Storage Remove Task with mocked service, throwing `NoSuchKey` exception @@ -47,7 +47,7 @@ class AWSS3StorageRemoveTaskTests: XCTestCase { } let request = StorageRemoveRequest( - path: StringStoragePath.fromString("/path"), options: .init()) + path: StringStoragePath.fromString("path"), options: .init()) let task = AWSS3StorageRemoveTask( request, storageConfiguration: AWSS3StoragePluginConfiguration(), @@ -74,7 +74,7 @@ class AWSS3StorageRemoveTaskTests: XCTestCase { let serviceMock = MockAWSS3StorageService() let request = StorageRemoveRequest( - path: StringStoragePath.fromString("path"), options: .init()) + path: StringStoragePath.fromString("/path"), options: .init()) let task = AWSS3StorageRemoveTask( request, storageConfiguration: AWSS3StoragePluginConfiguration(), From 9e2bc8baef3deba67f2472294dfaa404a9da18b9 Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:21:52 -0500 Subject: [PATCH 08/26] chore(storage): add new upload and download integration tests (#3581) --- ...SS3StoragePlugin+AsyncClientBehavior.swift | 6 +- .../AWSS3StorageUploadDataOperation.swift | 9 +- .../AWSS3StorageUploadFileOperation.swift | 9 +- ...toragePluginDownloadIntegrationTests.swift | 61 ++++++ ...3StoragePluginUploadIntegrationTests.swift | 201 ++++++++++++++++++ .../StorageHostApp.xcodeproj/project.pbxproj | 8 + 6 files changed, 286 insertions(+), 8 deletions(-) create mode 100644 AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginDownloadIntegrationTests.swift create mode 100644 AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginUploadIntegrationTests.swift diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift index 06f2db02a6..a960ec1e07 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+AsyncClientBehavior.swift @@ -151,8 +151,7 @@ extension AWSS3StoragePlugin { options: StorageUploadDataOperation.Request.Options? = nil ) -> StorageUploadDataTask { let options = options ?? StorageUploadDataRequest.Options() - let path = "" //TODO: resolve path - let request = StorageUploadDataRequest(key: path, data: data, options: options) + let request = StorageUploadDataRequest(path: path, data: data, options: options) let operation = AWSS3StorageUploadDataOperation(request, storageConfiguration: storageConfiguration, storageService: storageService, @@ -188,8 +187,7 @@ extension AWSS3StoragePlugin { options: StorageUploadFileOperation.Request.Options? = nil ) -> StorageUploadFileTask { let options = options ?? StorageUploadFileRequest.Options() - let path = "" //TODO: resolve path - let request = StorageUploadFileRequest(key: path, local: local, options: options) + let request = StorageUploadFileRequest(path: path, local: local, options: options) let operation = AWSS3StorageUploadFileOperation(request, storageConfiguration: storageConfiguration, storageService: storageService, diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadDataOperation.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Operation/AWSS3StorageUploadDataOperation.swift index 6e4288445a..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") @@ -92,6 +92,7 @@ class AWSS3StorageUploadDataOperation: AmplifyInProcessReportingOperation< 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) @@ -142,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 92e63b5b4d..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") @@ -114,6 +114,7 @@ class AWSS3StorageUploadFileOperation: AmplifyInProcessReportingOperation< 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) @@ -165,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/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/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 da28c91c22..dbb60060a3 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj @@ -59,6 +59,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 */; }; @@ -128,6 +130,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; }; @@ -269,6 +273,8 @@ 562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */, 901AB3E82AE2C2DC000F825B /* AWSS3StoragePluginUploadMetadataTestCase.swift */, 684FB08728BEAF8E00C8A6EB /* ResumabilityTests */, + 734605212BACB5CC0039F0EB /* AWSS3StoragePluginUploadIntegrationTests.swift */, + 734605232BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift */, ); path = AWSS3StoragePluginIntegrationTests; sourceTree = ""; @@ -616,6 +622,7 @@ 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 */, 68828E3F28C1549B006E7C0A /* AWSS3StoragePluginUploadFileResumabilityTests.swift in Sources */, @@ -623,6 +630,7 @@ 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 */, From 540acc2ba64ae18ee9bf067189de337442b19466 Mon Sep 17 00:00:00 2001 From: Harsh <6162866+harsh62@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:58:12 -0400 Subject: [PATCH 09/26] feat(Storage): Adding integration tests for getURL, remove and list (#3584) --- ...3StoragePluginGetURLIntegrationTests.swift | 85 ++++++++ ...agePluginListObjectsIntegrationTests.swift | 186 ++++++++++++++++++ ...3StoragePluginRemoveIntegrationTests.swift | 166 ++++++++++++++++ .../StorageHostApp.xcodeproj/project.pbxproj | 12 ++ 4 files changed, 449 insertions(+) create mode 100644 AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift create mode 100644 AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift create mode 100644 AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginRemoveIntegrationTests.swift 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..6b05278a11 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift @@ -0,0 +1,186 @@ +// +// 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 == 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 == 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 == 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 == 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 == 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 == 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/StorageHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj index dbb60060a3..975f3dea20 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 */; }; 56043E9329FC4D33003E3424 /* amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = D5C0382101A0E23943FDF4CB /* amplifyconfiguration.json */; }; 562B9AA42A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */; }; 562B9AA52A0D734E00A96FC6 /* AWSS3StoragePluginRequestRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */; }; @@ -103,6 +106,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 = ""; }; 562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginRequestRecorder.swift; sourceTree = ""; }; 565DF16F2953BAEA000DCCF7 /* AWSS3StoragePluginAccelerateIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginAccelerateIntegrationTests.swift; sourceTree = ""; }; 681D7D392A42637700F7C310 /* StorageWatchApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StorageWatchApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -275,6 +281,9 @@ 684FB08728BEAF8E00C8A6EB /* ResumabilityTests */, 734605212BACB5CC0039F0EB /* AWSS3StoragePluginUploadIntegrationTests.swift */, 734605232BACB60E0039F0EB /* AWSS3StoragePluginDownloadIntegrationTests.swift */, + 488C2A722BAE04DC009AD2BA /* AWSS3StoragePluginRemoveIntegrationTests.swift */, + 488C2A742BAFCA7C009AD2BA /* AWSS3StoragePluginListObjectsIntegrationTests.swift */, + 488C2A762BAFD4B3009AD2BA /* AWSS3StoragePluginGetURLIntegrationTests.swift */, ); path = AWSS3StoragePluginIntegrationTests; sourceTree = ""; @@ -613,18 +622,21 @@ 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 */, From 8c85096ccea8248edc28a665d218867fdf19926c Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Thu, 28 Mar 2024 14:55:10 -0500 Subject: [PATCH 10/26] chore(storage): update storage path validation to include empty/white spaces (#3587) --- .../Internal/StoragePath+Extensions.swift | 10 +++- ...SS3StorageDownloadFileOperationTests.swift | 32 +++++++++++++ .../AWSS3StorageGetDataOperationTests.swift | 30 ++++++++++++ .../AWSS3StoragePutDataOperationTests.swift | 31 +++++++++++++ ...AWSS3StorageUploadFileOperationTests.swift | 46 +++++++++++++++++++ .../Tasks/AWSS3StorageGetURLTaskTests.swift | 32 +++++++++++++ .../AWSS3StorageListObjectsTaskTests.swift | 26 +++++++++++ .../Tasks/AWSS3StorageRemoveTaskTests.swift | 26 +++++++++++ 8 files changed, 231 insertions(+), 2 deletions(-) diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift index bdc2ebece4..482190be32 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift @@ -20,7 +20,7 @@ extension StoragePath { nil ) } - let path = resolve(identityId) + let path = resolve(identityId).trimmingCharacters(in: .whitespaces) try validate(path) return path } else if self is StringStoragePath { @@ -30,7 +30,7 @@ extension StoragePath { nil ) } - let path = resolve(input) + let path = resolve(input).trimmingCharacters(in: .whitespaces) try validate(path) return path } else { @@ -41,6 +41,12 @@ extension StoragePath { } func validate(_ path: String) throws { + guard !path.isEmpty else { + let errorDescription = "Invalid StoragePath specified." + let recoverySuggestion = "Please specify a valid StoragePath" + throw StorageError.validation("path", errorDescription, recoverySuggestion, nil) + } + if path.hasPrefix("/") { let errorDescription = "Invalid StoragePath specified." let recoverySuggestion = "Please specify a valid StoragePath that does not contain the prefix / " diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift index 98b15f5954..9b1b6b6d65 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageDownloadFileOperationTests.swift @@ -214,6 +214,38 @@ class AWSS3StorageDownloadFileOperationTests: AWSS3StorageOperationTestBase { 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 diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift index 23a28ee935..6a7c7a5485 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageGetDataOperationTests.swift @@ -204,6 +204,36 @@ class AWSS3StorageDownloadDataOperationTests: AWSS3StorageOperationTestBase { 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 diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift index faeb9fc862..5934e419d3 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StoragePutDataOperationTests.swift @@ -250,6 +250,37 @@ class AWSS3StorageUploadDataOperationTests: AWSS3StorageOperationTestBase { 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 diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift index 87183415e6..12374173bb 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Operation/AWSS3StorageUploadFileOperationTests.swift @@ -301,6 +301,52 @@ class AWSS3StorageUploadFileOperationTests: AWSS3StorageOperationTestBase { 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 diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift index ccd5212bc8..71f5ea6ea1 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageGetURLTaskTests.swift @@ -103,4 +103,36 @@ class AWSS3StorageGetURLTaskTests: XCTestCase { } } + /// - 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 index 4ba7cff47c..251111abfa 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift @@ -103,4 +103,30 @@ class AWSS3StorageListObjectsTaskTests: XCTestCase { } } + /// - 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 index 047f130036..06f4ecf809 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageRemoveTaskTests.swift @@ -94,4 +94,30 @@ class AWSS3StorageRemoveTaskTests: XCTestCase { } } + /// - 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`") + } + } } From 3169654f9b073f0934622feed31c44bb3d307ab9 Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:36:24 -0500 Subject: [PATCH 11/26] chore: deprecate key field in StorageListResult.Item (#3610) --- .../Storage/Result/StorageListResult.swift | 6 +++--- .../Tasks/AWSS3StorageListObjectsTask.swift | 5 ++--- .../AWSS3StorageListObjectsTaskTests.swift | 1 + ...ragePluginListObjectsIntegrationTests.swift | 18 ++++++++++++------ 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Amplify/Categories/Storage/Result/StorageListResult.swift b/Amplify/Categories/Storage/Result/StorageListResult.swift index 7294945b9f..057b9e177a 100644 --- a/Amplify/Categories/Storage/Result/StorageListResult.swift +++ b/Amplify/Categories/Storage/Result/StorageListResult.swift @@ -50,6 +50,7 @@ extension StorageListResult { /// 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 @@ -77,7 +78,7 @@ extension StorageListResult { /// [StorageCategoryBehavior.list](x-source-tag://StorageCategoryBehavior.list). /// /// - Tag: StorageListResultItem.init - @available(*, deprecated, message: "Use init(path:key:size:lastModifiedDate:eTag:pluginResults)") + @available(*, deprecated, message: "Use init(path:size:lastModifiedDate:eTag:pluginResults)") public init( key: String, size: Int? = nil, @@ -95,14 +96,13 @@ extension StorageListResult { public init( path: String, - key: String, size: Int? = nil, eTag: String? = nil, lastModified: Date? = nil, pluginResults: Any? = nil ) { self.path = path - self.key = key + self.key = path self.size = size self.eTag = eTag self.lastModified = lastModified diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift index 6da4ce23d0..2baadcf539 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Tasks/AWSS3StorageListObjectsTask.swift @@ -38,7 +38,7 @@ class AWSS3StorageListObjectsTask: StorageListObjectsTask, DefaultLogger { guard let path = try await request.path?.resolvePath() else { throw StorageError.validation( "path", - "`path` is required for removing an object", + "`path` is required for listing objects", "Make sure that a valid `path` is passed for removing an object") } let input = ListObjectsV2Input(bucket: storageBehaviour.bucket, @@ -51,12 +51,11 @@ class AWSS3StorageListObjectsTask: StorageListObjectsTask, DefaultLogger { let response = try await storageBehaviour.client.listObjectsV2(input: input) let contents: S3BucketContents = response.contents ?? [] let items = try contents.map { s3Object in - guard let key = s3Object.key else { + guard let path = s3Object.key else { throw StorageError.unknown("Missing key in response") } return StorageListResult.Item( path: path, - key: key, eTag: s3Object.eTag, lastModified: s3Object.lastModified) } diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift index 251111abfa..922f29974c 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Tasks/AWSS3StorageListObjectsTaskTests.swift @@ -41,6 +41,7 @@ class AWSS3StorageListObjectsTaskTests: XCTestCase { 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) } diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift index 6b05278a11..83ad2ce017 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginListObjectsIntegrationTests.swift @@ -29,14 +29,16 @@ class AWSS3StoragePluginListObjectsIntegrationTests: AWSS3StoragePluginTestBase let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) // Validate the item was uploaded. - XCTAssertEqual(firstListResult.items.filter({ $0.path == uniqueStringPath}).count, 1) + 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 == uniqueStringPath}).count, 2) + XCTAssertEqual(secondListResult.items.filter({ $0.path.contains(uniqueStringPath) + }).count, 2) // Clean up _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "/test1")) @@ -65,7 +67,8 @@ class AWSS3StoragePluginListObjectsIntegrationTests: AWSS3StoragePluginTestBase let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) // Validate the item was uploaded. - XCTAssertEqual(firstListResult.items.filter({ $0.path == uniqueStringPath}).count, 1) + XCTAssertEqual(firstListResult.items.filter({ $0.path.contains(uniqueStringPath) + }).count, 1) _ = try await Amplify.Storage.uploadData( path: .fromIdentityID({ identityId in @@ -78,7 +81,8 @@ class AWSS3StoragePluginListObjectsIntegrationTests: AWSS3StoragePluginTestBase let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) // Validate the item was uploaded. - XCTAssertEqual(secondListResult.items.filter({ $0.path == uniqueStringPath}).count, 2) + XCTAssertEqual(secondListResult.items.filter({ $0.path.contains(uniqueStringPath) + }).count, 2) // clean up _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "test1")) @@ -108,7 +112,8 @@ class AWSS3StoragePluginListObjectsIntegrationTests: AWSS3StoragePluginTestBase let firstListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) // Validate the item was uploaded. - XCTAssertEqual(firstListResult.items.filter({ $0.path == uniqueStringPath}).count, 1) + XCTAssertEqual(firstListResult.items.filter({ $0.path.contains(uniqueStringPath) + }).count, 1) _ = try await Amplify.Storage.uploadData( path: .fromIdentityID({ identityId in @@ -121,7 +126,8 @@ class AWSS3StoragePluginListObjectsIntegrationTests: AWSS3StoragePluginTestBase let secondListResult = try await Amplify.Storage.list(path: .fromString(uniqueStringPath)) // Validate the item was uploaded. - XCTAssertEqual(secondListResult.items.filter({ $0.path == uniqueStringPath}).count, 2) + XCTAssertEqual(secondListResult.items.filter({ $0.path.contains(uniqueStringPath) + }).count, 2) // clean up _ = try await Amplify.Storage.remove(path: .fromString(uniqueStringPath + "test1")) From 8e2bfa7d5dbeedad07c4c54387ff589151c0857e Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:05:00 -0500 Subject: [PATCH 12/26] chore(storage): align storage validation exception message with Amplify Android (#3612) --- .../Support/Internal/StoragePath+Extensions.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift index 482190be32..95c464cff2 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Internal/StoragePath+Extensions.swift @@ -41,15 +41,9 @@ extension StoragePath { } func validate(_ path: String) throws { - guard !path.isEmpty else { - let errorDescription = "Invalid StoragePath specified." - let recoverySuggestion = "Please specify a valid StoragePath" - throw StorageError.validation("path", errorDescription, recoverySuggestion, nil) - } - - if path.hasPrefix("/") { - let errorDescription = "Invalid StoragePath specified." - let recoverySuggestion = "Please specify a valid StoragePath that does not contain the prefix / " + 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) } } From f8b9885765099dcfd5013eca7b6e8f4cf8e14589 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:05:07 -0400 Subject: [PATCH 13/26] feat(all): Configure plugins with AmplifyOutputs (#3567) * feat: read and decode AmplifyConfigurationV2 * analytics * api * auth * geo * logging * notifications * storage * authenticator support * predictions and coreML * fixes - String to AWSAuthorizationType, AWSRegion, AmazonCognitoStandardAttributes * reconcile to latest changes * Add AWSAPIPluginGen2FunctionalTests * Revert "logging" This reverts commit 0df5c347b6bfc6e5f0e61becc0c93ff959a5a97a. * consolidate README * Auth Integration tests * fix apiplugin * storage integ tests * rename and refactor AmplifyOutputs * analytics integration tests * enable more storage tests * geo integration tests * logging integration test set up * AuthHostedUIApp testing with Gen2 set up * PushNotifications tests with Gen2 set up * Add Prediction integration test README * refactor analytics options under the plugin type * auth flow type mapping * fix storage behavior - revert to previous behavior * rename oauth domain to cognito domain and add custom domain override * unit test around AmplifyOutputsData * remove authFlowType, add internal init helpers, auth unit tests * analytics unit tests * api unit tests * geo unit tests * datastore unit tests * Amplify configure tests * no changes to InternalAWSPinpoint, removing added init * fix push notifications unit tests * predictions plugin does not configure with amplify outputs * coreMLpredictions unit tests * storage unit tests * fix concurrent config unit tests * update Auth usernameAttributes and userVerificationTypes to enum * rename analytics options file * update debug description * remove unneeded mapping from amplifyconfiguration.json * add unit test for translating back to json for auth config * Update Analytics Options properties to use TimeInterval * fix naming and method signatures * auth integ test - user username as username * fix analytics tests * fix api functional test target * fix auth integ test target * fix geo integ test target * fix logging integ test target * fix notifications integ test target * fix storage integ test target * update README * update analytics options to remove autoSessionTrackingInterval * fix commented out username checking * configure if-else refactor * update recovery message * swiftlint disable nesting AmplifyOutputsData * adding reasoning and link for swiftlint message * reanble at end of file swiftlint * feat(api): Expose a constant for the API name used by Gen2 data category (#3631) * feat(api): Expose a constant for the API name used by Gen2 data category * update casing on API * add public * finalize API --- .../APICategory+CategoryConfigurable.swift | 6 + ...alyticsCategory+CategoryConfigurable.swift | 6 + .../AuthCategory+CategoryConfigurable.swift | 7 + .../DataStoreCategory+Configurable.swift | 4 + .../GeoCategory+CategoryConfigurable.swift | 6 + .../HubCategory+CategoryConfigurable.swift | 15 + ...LoggingCategory+CategoryConfigurable.swift | 26 ++ ...cationsCategory+CategoryConfigurable.swift | 7 + ...ictionsCategory+CategoryConfigurable.swift | 7 + ...StorageCategory+CategoryConfigurable.swift | 6 + .../Configuration/AmplifyConfiguration.swift | 4 +- .../Configuration/AmplifyOutputsData.swift | 364 ++++++++++++++++++ .../Configuration/ConfigurationError.swift | 8 + .../Internal/Amplify+Resolve.swift | 1 - .../AmplifyConfigurationInitialization.swift | 70 ++++ .../Internal/Category+Configuration.swift | 1 - .../Internal/CategoryConfigurable.swift | 6 +- .../AWSAPIPlugin/AWSAPIPlugin+Configure.swift | 37 +- .../Sources/AWSAPIPlugin/AWSAPIPlugin.swift | 4 + ...ryPluginConfiguration+EndpointConfig.swift | 32 +- .../AWSAPICategoryPluginConfiguration.swift | 47 ++- .../APIHostApp.xcodeproj/project.pbxproj | 295 +++++++++++++- .../xcschemes/APIHostApp.xcscheme | 11 + .../AWSAPIPluginFunctionalTests.xcscheme | 6 + .../AWSAPIPluginGen2FunctionalTests.xcscheme | 58 +++ ...AWSAPIPluginGen2FunctionalTests.xctestplan | 39 ++ .../Base/TestConfigHelper.swift | 11 +- .../GraphQLModelBasedTests.swift | 20 +- .../AWSAPIPluginFunctionalTests/README.md | 112 +++++- .../AWSAPICategoryPlugin+ConfigureTests.swift | 65 +++- ...AWSPinpointAnalyticsPlugin+Configure.swift | 33 +- .../AWSPinpointAnalyticsPlugin+Options.swift | 36 ++ .../AWSPinpointAnalyticsPlugin+Reset.swift | 4 + .../AWSPinpointAnalyticsPlugin.swift | 8 +- ...PinpointAnalyticsPluginConfiguration.swift | 74 ++-- .../Constants/AnalyticsErrorConstants.swift | 10 + ...inpointAnalyticsPluginConfigureTests.swift | 99 ++++- .../AWSPinpointAnalyticsPluginTestBase.swift | 4 +- ...uginAmplifyOutputsConfigurationTests.swift | 87 +++++ ...intAnalyticsPluginConfigurationTests.swift | 38 +- ...yticsPluginGen2IntegrationTests.xctestplan | 28 ++ ...pointAnalyticsPluginIntegrationTests.swift | 15 +- .../README.md | 160 +++++++- .../project.pbxproj | 138 +++++++ ...alyticsPluginGen2IntegrationTests.xcscheme | 58 +++ .../xcschemes/AnalyticsHostApp.xcscheme | 2 +- .../AWSCognitoAuthPlugin+Configure.swift | 16 +- .../Data/UserPoolConfigurationData.swift | 154 +++++++- .../Support/Helpers/ConfigurationHelper.swift | 167 +++++++- ...oAuthPluginAmplifyOutputsConfigTests.swift | 126 ++++++ .../Support/ConfigurationHelperTests.swift | 292 ++++++++++++++ .../AuthHostApp.xcodeproj/project.pbxproj | 207 ++++++++++ .../AuthGen2IntegrationTests.xcscheme | 58 +++ .../AWSAuthBaseTest.swift | 29 +- .../AuthGen2IntegrationTests.xctestplan | 28 ++ .../Helpers/AuthSignInHelper.swift | 1 + .../AuthIntegrationTests/README.md | 126 +++++- .../SignInTests/AuthSRPSignInTests.swift | 12 +- .../SignUpTests/AuthSignUpTests.swift | 1 + .../AuthHostedUIApp.xcodeproj/project.pbxproj | 2 + .../AuthHostedUIApp/AuthHostedUIAppApp.swift | 18 +- .../AuthHostedUIGen2App.xctestplan | 28 ++ .../AuthHostedUIApp/ContentView.swift | 1 - .../Utils/ConfigurationHelper.swift | 2 +- .../AuthHostedUIAppUITests/UITestCase.swift | 3 + .../Core/AWSDataStorePluginTests.swift | 20 +- .../AWSLocationGeoPlugin+Configure.swift | 12 +- .../AWSLocationGeoPluginConfiguration.swift | 70 +++- .../Configuration/GeoPluginConfigError.swift | 2 +- .../AWSLocationGeoPluginConfigureTests.swift | 15 + ...uginAmplifyOutputsConfigurationTests.swift | 143 +++++++ ...SLocationGeoPluginConfigurationTests.swift | 13 - .../Constants/GeoPluginTestConfig.swift | 9 +- ...onGeoPluginGen2IntegrationTests.xctestplan | 28 ++ ...AWSLocationGeoPluginIntegrationTests.swift | 15 +- .../README.md | 181 ++++++++- .../GeoHostApp.xcodeproj/project.pbxproj | 149 +++++++ ...tionGeoPluginGen2IntegrationTests.xcscheme | 58 +++ ...LocationGeoPluginIntegrationTests.xcscheme | 2 +- .../AWSPinpointPluginConfiguration.swift | 6 +- ...ggingPluginGen2IntegrationTests.xctestplan | 33 ++ ...udWatchLoggingPluginIntegrationTests.swift | 16 +- .../README.md | 103 ++++- .../project.pbxproj | 153 ++++++++ ...LoggingPluginGen2IntegrationTests.xcscheme | 69 ++++ ...intPushNotificationsPlugin+Configure.swift | 19 +- ...ushNotificationsPluginErrorConstants.swift | 5 + ...ushNotificationsPluginConfigureTests.swift | 19 +- .../PushNotificationGen2HostApp.xctestplan | 34 ++ .../PushNotificationHostApp copy-Info.plist | 15 + .../project.pbxproj | 190 +++++++++ .../PushNotificationGen2HostApp.xcscheme | 82 ++++ .../PushNotificationHostApp copy.xcscheme | 77 ++++ .../PushNotificationHostApp.xcscheme | 90 +++++ .../PushNotificationHostApp/ContentView.swift | 16 +- .../PushNotificationHostAppUITests.swift | 5 + .../PushNotificationHostAppUITests/README.md | 17 +- .../AWSPredictionsPlugin+Configure.swift | 24 +- .../ErrorHandling/PluginErrorMessage.swift | 5 + .../CoreMLPredictionsPlugin+Configure.swift | 4 +- .../PredictionsPluginConfigurationTests.swift | 22 +- .../CoreMLPredictionsPluginConfigTests.swift | 13 +- .../README.md | 10 +- .../AWSS3StoragePlugin+Configure.swift | 102 +++-- .../AWSS3StoragePlugin.swift | 2 +- .../Constants/PluginErrorConstants.swift | 4 + ...uginAmplifyOutputsConfigurationTests.swift | 113 ++++++ .../AWSS3StoragePluginBaseConfigTests.swift | 2 +- .../Storage/Tests/StorageHostApp/.gitignore | 4 +- .../AWSS3StoragePluginAccessLevelTests.swift | 68 +++- ...oragePluginGen2IntegrationTests.xctestplan | 28 ++ .../AWSS3StoragePluginTestBase.swift | 64 ++- ...3StoragePluginUploadMetadataTestCase.swift | 13 +- .../README.md | 146 ++++++- .../StorageHostApp.xcodeproj/project.pbxproj | 157 +++++++- ...StoragePluginGen2IntegrationTests.xcscheme | 58 +++ .../StorageHostApp/copy_configuration.sh | 10 +- .../API/APICategoryConfigurationTests.swift | 32 +- .../AnalyticsCategoryConfigurationTests.swift | 31 +- .../Auth/AuthCategoryConfigurationTests.swift | 21 +- .../DataStoreCategoryConfigurationTests.swift | 20 +- .../Geo/GeoCategoryConfigurationTests.swift | 13 +- .../Hub/HubCategoryConfigurationTests.swift | 31 +- .../LoggingCategoryConfigurationTests.swift | 15 +- ...ificationsCategoryConfigurationTests.swift | 18 +- ...redictionsCategoryConfigurationTests.swift | 22 +- .../StorageCategoryConfigurationTests.swift | 15 +- .../AmplifyOutputsInitializationTests.swift | 143 +++++++ 128 files changed, 5943 insertions(+), 279 deletions(-) create mode 100644 Amplify/Core/Configuration/AmplifyOutputsData.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/AWSAPIPluginGen2FunctionalTests.xcscheme create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AWSAPIPluginGen2FunctionalTests.xctestplan create mode 100644 AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Options.swift create mode 100644 AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/Configuration/AWSPinpointAnalyticsPluginAmplifyOutputsConfigurationTests.swift create mode 100644 AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/AWSPinpointAnalyticsPluginGen2IntegrationTests.xctestplan create mode 100644 AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/xcshareddata/xcschemes/AWSPinpointAnalyticsPluginGen2IntegrationTests.xcscheme create mode 100644 AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginAmplifyOutputsConfigTests.swift create mode 100644 AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift create mode 100644 AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/xcshareddata/xcschemes/AuthGen2IntegrationTests.xcscheme create mode 100644 AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AuthGen2IntegrationTests.xctestplan create mode 100644 AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/AuthHostedUIGen2App.xctestplan create mode 100644 AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Configuration/AWSLocationGeoPluginAmplifyOutputsConfigurationTests.swift create mode 100644 AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/AWSLocationGeoPluginGen2IntegrationTests.xctestplan create mode 100644 AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/xcshareddata/xcschemes/AWSLocationGeoPluginGen2IntegrationTests.xcscheme create mode 100644 AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginGen2IntegrationTests.xctestplan create mode 100644 AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/CloudWatchLoggingHostApp.xcodeproj/xcshareddata/xcschemes/AWSCloudWatchLoggingPluginGen2IntegrationTests.xcscheme create mode 100644 AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationGen2HostApp.xctestplan create mode 100644 AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp copy-Info.plist create mode 100644 AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationGen2HostApp.xcscheme create mode 100644 AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationHostApp copy.xcscheme create mode 100644 AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationHostApp.xcscheme create mode 100644 AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/AWSS3StoragePluginAmplifyOutputsConfigurationTests.swift create mode 100644 AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGen2IntegrationTests.xctestplan create mode 100644 AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/xcshareddata/xcschemes/AWSS3StoragePluginGen2IntegrationTests.xcscheme create mode 100644 AmplifyTests/CoreTests/AmplifyOutputsInitializationTests.swift diff --git a/Amplify/Categories/API/Internal/APICategory+CategoryConfigurable.swift b/Amplify/Categories/API/Internal/APICategory+CategoryConfigurable.swift index 817b40d326..26e8f1a9e5 100644 --- a/Amplify/Categories/API/Internal/APICategory+CategoryConfigurable.swift +++ b/Amplify/Categories/API/Internal/APICategory+CategoryConfigurable.swift @@ -25,4 +25,10 @@ extension APICategory: CategoryConfigurable { try configure(using: categoryConfiguration(from: amplifyConfiguration)) } + func configure(using amplifyOutputs: AmplifyOutputsData) throws { + for plugin in Array(plugins.values) { + try plugin.configure(using: amplifyOutputs) + } + isConfigured = true + } } diff --git a/Amplify/Categories/Analytics/Internal/AnalyticsCategory+CategoryConfigurable.swift b/Amplify/Categories/Analytics/Internal/AnalyticsCategory+CategoryConfigurable.swift index e284dcb487..3978b10615 100644 --- a/Amplify/Categories/Analytics/Internal/AnalyticsCategory+CategoryConfigurable.swift +++ b/Amplify/Categories/Analytics/Internal/AnalyticsCategory+CategoryConfigurable.swift @@ -25,4 +25,10 @@ extension AnalyticsCategory: CategoryConfigurable { try configure(using: categoryConfiguration(from: amplifyConfiguration)) } + func configure(using amplifyOutputs: AmplifyOutputsData) throws { + for plugin in Array(plugins.values) { + try plugin.configure(using: amplifyOutputs) + } + isConfigured = true + } } diff --git a/Amplify/Categories/Auth/Internal/AuthCategory+CategoryConfigurable.swift b/Amplify/Categories/Auth/Internal/AuthCategory+CategoryConfigurable.swift index 44fade465c..aff88dc2b1 100644 --- a/Amplify/Categories/Auth/Internal/AuthCategory+CategoryConfigurable.swift +++ b/Amplify/Categories/Auth/Internal/AuthCategory+CategoryConfigurable.swift @@ -26,4 +26,11 @@ extension AuthCategory: CategoryConfigurable { func configure(using amplifyConfiguration: AmplifyConfiguration) throws { try configure(using: categoryConfiguration(from: amplifyConfiguration)) } + + func configure(using amplifyOutputs: AmplifyOutputsData) throws { + for plugin in Array(plugins.values) { + try plugin.configure(using: amplifyOutputs) + } + isConfigured = true + } } diff --git a/Amplify/Categories/DataStore/Internal/DataStoreCategory+Configurable.swift b/Amplify/Categories/DataStore/Internal/DataStoreCategory+Configurable.swift index dd3ac68569..a6241d4980 100644 --- a/Amplify/Categories/DataStore/Internal/DataStoreCategory+Configurable.swift +++ b/Amplify/Categories/DataStore/Internal/DataStoreCategory+Configurable.swift @@ -15,6 +15,10 @@ extension DataStoreCategory: CategoryConfigurable { } } + func configure(using amplifyConfiguration: AmplifyOutputsData) throws { + try configureFirstWithEmptyConfiguration() + } + func configure(using configuration: CategoryConfiguration?) throws { guard !isConfigured else { let error = ConfigurationError.amplifyAlreadyConfigured( diff --git a/Amplify/Categories/Geo/Internal/GeoCategory+CategoryConfigurable.swift b/Amplify/Categories/Geo/Internal/GeoCategory+CategoryConfigurable.swift index 06b15ae809..ce233726dd 100644 --- a/Amplify/Categories/Geo/Internal/GeoCategory+CategoryConfigurable.swift +++ b/Amplify/Categories/Geo/Internal/GeoCategory+CategoryConfigurable.swift @@ -25,4 +25,10 @@ extension GeoCategory: CategoryConfigurable { try configure(using: categoryConfiguration(from: amplifyConfiguration)) } + func configure(using amplifyOutputs: AmplifyOutputsData) throws { + for plugin in Array(plugins.values) { + try plugin.configure(using: amplifyOutputs) + } + isConfigured = true + } } diff --git a/Amplify/Categories/Hub/Internal/HubCategory+CategoryConfigurable.swift b/Amplify/Categories/Hub/Internal/HubCategory+CategoryConfigurable.swift index 7adfbcb77f..878abf30de 100644 --- a/Amplify/Categories/Hub/Internal/HubCategory+CategoryConfigurable.swift +++ b/Amplify/Categories/Hub/Internal/HubCategory+CategoryConfigurable.swift @@ -27,4 +27,19 @@ extension HubCategory: CategoryConfigurable { try configure(using: categoryConfiguration(from: amplifyConfiguration)) } + func configure(using amplifyOutputs: AmplifyOutputsData) throws { + guard configurationState.get() != .configured else { + let error = ConfigurationError.amplifyAlreadyConfigured( + "\(categoryType.displayName) has already been configured.", + "Remove the duplicate call to `Amplify.configure()`" + ) + throw error + } + + for plugin in Array(plugins.values) { + try plugin.configure(using: amplifyOutputs) + } + configurationState.set(.configured) + } + } diff --git a/Amplify/Categories/Logging/Internal/LoggingCategory+CategoryConfigurable.swift b/Amplify/Categories/Logging/Internal/LoggingCategory+CategoryConfigurable.swift index 4c691fd65c..67a9c69899 100644 --- a/Amplify/Categories/Logging/Internal/LoggingCategory+CategoryConfigurable.swift +++ b/Amplify/Categories/Logging/Internal/LoggingCategory+CategoryConfigurable.swift @@ -39,4 +39,30 @@ extension LoggingCategory: CategoryConfigurable { try configure(using: categoryConfiguration(from: amplifyConfiguration)) } + func configure(using amplifyOutputs: AmplifyOutputsData) throws { + let plugin: LoggingCategoryPlugin + switch configurationState { + case .default: + // Default plugin is already assigned, and no configuration is applicable, exit early + configurationState = .configured + return + case .pendingConfiguration(let pendingPlugin): + plugin = pendingPlugin + case .configured: + let error = ConfigurationError.amplifyAlreadyConfigured( + "\(categoryType.displayName) has already been configured.", + "Remove the duplicate call to `Amplify.configure()`" + ) + throw error + } + + try plugin.configure(using: amplifyOutputs) + self.plugins[plugin.key] = plugin + + if plugin.key != AWSUnifiedLoggingPlugin.key, let consolePlugin = try? self.getPlugin(for: AWSUnifiedLoggingPlugin.key) { + try consolePlugin.configure(using: amplifyOutputs) + } + + configurationState = .configured + } } diff --git a/Amplify/Categories/Notifications/PushNotifications/Internal/PushNotificationsCategory+CategoryConfigurable.swift b/Amplify/Categories/Notifications/PushNotifications/Internal/PushNotificationsCategory+CategoryConfigurable.swift index 6986d75558..59d80d72aa 100644 --- a/Amplify/Categories/Notifications/PushNotifications/Internal/PushNotificationsCategory+CategoryConfigurable.swift +++ b/Amplify/Categories/Notifications/PushNotifications/Internal/PushNotificationsCategory+CategoryConfigurable.swift @@ -23,4 +23,11 @@ extension PushNotificationsCategory: CategoryConfigurable { func configure(using amplifyConfiguration: AmplifyConfiguration) throws { try configure(using: categoryConfiguration(from: amplifyConfiguration)) } + + func configure(using amplifyOutputs: AmplifyOutputsData) throws { + for plugin in Array(plugins.values) { + try plugin.configure(using: amplifyOutputs) + } + isConfigured = true + } } diff --git a/Amplify/Categories/Predictions/Internal/PredictionsCategory+CategoryConfigurable.swift b/Amplify/Categories/Predictions/Internal/PredictionsCategory+CategoryConfigurable.swift index 2e29ec5146..1551e9aa79 100644 --- a/Amplify/Categories/Predictions/Internal/PredictionsCategory+CategoryConfigurable.swift +++ b/Amplify/Categories/Predictions/Internal/PredictionsCategory+CategoryConfigurable.swift @@ -25,4 +25,11 @@ extension PredictionsCategory: CategoryConfigurable { try configure(using: categoryConfiguration(from: amplifyConfiguration)) } + func configure(using amplifyOutputs: AmplifyOutputsData) throws { + for plugin in Array(plugins.values) { + try plugin.configure(using: amplifyOutputs) + } + isConfigured = true + } + } diff --git a/Amplify/Categories/Storage/Internal/StorageCategory+CategoryConfigurable.swift b/Amplify/Categories/Storage/Internal/StorageCategory+CategoryConfigurable.swift index 20eaafd4ad..caf4552793 100644 --- a/Amplify/Categories/Storage/Internal/StorageCategory+CategoryConfigurable.swift +++ b/Amplify/Categories/Storage/Internal/StorageCategory+CategoryConfigurable.swift @@ -25,4 +25,10 @@ extension StorageCategory: CategoryConfigurable { try configure(using: categoryConfiguration(from: amplifyConfiguration)) } + func configure(using amplifyOutputs: AmplifyOutputsData) throws { + for plugin in Array(plugins.values) { + try plugin.configure(using: amplifyOutputs) + } + isConfigured = true + } } diff --git a/Amplify/Core/Configuration/AmplifyConfiguration.swift b/Amplify/Core/Configuration/AmplifyConfiguration.swift index fb7f634348..2cb769f981 100644 --- a/Amplify/Core/Configuration/AmplifyConfiguration.swift +++ b/Amplify/Core/Configuration/AmplifyConfiguration.swift @@ -175,7 +175,7 @@ extension Amplify { /// Notifies all hub channels that Amplify is configured, in case any plugins need to be notified of the end of the /// configuration phase (e.g., to set up cross-channel dependencies) - private static func notifyAllHubChannels() { + static func notifyAllHubChannels() { let payload = HubPayload(eventName: HubPayload.EventName.Amplify.configured) for channel in HubChannel.amplifyChannels { Hub.plugins.values.forEach { $0.dispatch(to: channel, payload: payload) } @@ -210,7 +210,7 @@ extension Amplify { } //// Indicates is the runtime is for SwiftUI Previews - private static var isRunningForSwiftUIPreviews: Bool { + static var isRunningForSwiftUIPreviews: Bool { ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != nil } diff --git a/Amplify/Core/Configuration/AmplifyOutputsData.swift b/Amplify/Core/Configuration/AmplifyOutputsData.swift new file mode 100644 index 0000000000..5fb9435c2f --- /dev/null +++ b/Amplify/Core/Configuration/AmplifyOutputsData.swift @@ -0,0 +1,364 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +// swiftlint:disable nesting +// `nesting` is disabled to best represent `AmplifyOutputsData` as close as possible +// to the JSON schema which is derived from. The JSON schema is hosted at +// https://github.com/aws-amplify/amplify-backend/blob/main/packages/client-config/src/client-config-schema/schema_v1.json + +/// Represents Amplify's Gen2 configuration for all categories intended to be used in an application. +/// +/// See: [Amplify.configure](x-source-tag://Amplify.configure) +/// +/// - Tag: AmplifyOutputs +/// +@_spi(InternalAmplifyConfiguration) +public struct AmplifyOutputsData: Codable { + public let version: String + public let analytics: Analytics? + public let auth: Auth? + public let data: DataCategory? + public let geo: Geo? + public let notifications: Notifications? + public let storage: Storage? + public let custom: CustomOutput? + + @_spi(InternalAmplifyConfiguration) + public struct Analytics: Codable { + public let amazonPinpoint: AmazonPinpoint? + + public struct AmazonPinpoint: Codable { + public let awsRegion: AWSRegion + public let appId: String + } + } + + @_spi(InternalAmplifyConfiguration) + public struct Auth: Codable { + public let awsRegion: AWSRegion + public let userPoolId: String + public let userPoolClientId: String + public let identityPoolId: String? + public let passwordPolicy: PasswordPolicy? + public let oauth: OAuth? + public let standardRequiredAttributes: [AmazonCognitoStandardAttributes]? + public let usernameAttributes: [UsernameAttributes]? + public let userVerificationTypes: [UserVerificationType]? + public let unauthenticatedIdentitiesEnabled: Bool? + public let mfaConfiguration: String? + public let mfaMethods: [String]? + + @_spi(InternalAmplifyConfiguration) + public struct PasswordPolicy: Codable { + public let minLength: UInt + public let requireNumbers: Bool + public let requireLowercase: Bool + public let requireUppercase: Bool + public let requireSymbols: Bool + } + + @_spi(InternalAmplifyConfiguration) + public struct OAuth: Codable { + public let identityProviders: [String] + public let cognitoDomain: String + public let customDomain: String? + public let scopes: [String] + public let redirectSignInUri: [String] + public let redirectSignOutUri: [String] + public let responseType: String + } + + @_spi(InternalAmplifyConfiguration) + public enum UsernameAttributes: String, Codable { + case email = "email" + case phoneNumber = "phone_number" + } + + @_spi(InternalAmplifyConfiguration) + public enum UserVerificationType: String, Codable { + case email = "email" + case phoneNumber = "phone_number" + } + + init(awsRegion: AWSRegion, + userPoolId: String, + userPoolClientId: String, + identityPoolId: String? = nil, + passwordPolicy: PasswordPolicy? = nil, + oauth: OAuth? = nil, + standardRequiredAttributes: [AmazonCognitoStandardAttributes]? = nil, + usernameAttributes: [UsernameAttributes]? = nil, + userVerificationTypes: [UserVerificationType]? = nil, + unauthenticatedIdentitiesEnabled: Bool? = nil, + mfaConfiguration: String? = nil, + mfaMethods: [String]? = nil) { + self.awsRegion = awsRegion + self.userPoolId = userPoolId + self.userPoolClientId = userPoolClientId + self.identityPoolId = identityPoolId + self.passwordPolicy = passwordPolicy + self.oauth = oauth + self.standardRequiredAttributes = standardRequiredAttributes + self.usernameAttributes = usernameAttributes + self.userVerificationTypes = userVerificationTypes + self.unauthenticatedIdentitiesEnabled = unauthenticatedIdentitiesEnabled + self.mfaConfiguration = mfaConfiguration + self.mfaMethods = mfaMethods + } + + } + + @_spi(InternalAmplifyConfiguration) + public struct DataCategory: Codable { + public let awsRegion: AWSRegion + public let url: String + public let modelIntrospection: JSONValue? + public let apiKey: String? + public let defaultAuthorizationType: AWSAppSyncAuthorizationType + public let authorizationTypes: [AWSAppSyncAuthorizationType] + } + + @_spi(InternalAmplifyConfiguration) + public struct Geo: Codable { + public let awsRegion: AWSRegion + public let maps: Maps? + public let searchIndices: SearchIndices? + public let geofenceCollections: GeofenceCollections? + + @_spi(InternalAmplifyConfiguration) + public struct Maps: Codable { + public let items: [String: AmazonLocationServiceConfig] + public let `default`: String + + @_spi(InternalAmplifyConfiguration) + public struct AmazonLocationServiceConfig: Codable { + public let style: String + } + } + + @_spi(InternalAmplifyConfiguration) + public struct SearchIndices: Codable { + public let items: [String] + public let `default`: String + } + + @_spi(InternalAmplifyConfiguration) + public struct GeofenceCollections: Codable { + public let items: [String] + public let `default`: String + } + + // Internal init used for testing + init(awsRegion: AWSRegion, + maps: Maps? = nil, + searchIndices: SearchIndices? = nil, + geofenceCollections: GeofenceCollections? = nil) { + self.awsRegion = awsRegion + self.maps = maps + self.searchIndices = searchIndices + self.geofenceCollections = geofenceCollections + } + } + + @_spi(InternalAmplifyConfiguration) + public struct Notifications: Codable { + public let awsRegion: String + public let amazonPinpointAppId: String + public let channels: [AmazonPinpointChannelType] + } + + @_spi(InternalAmplifyConfiguration) + public struct Storage: Codable { + public let awsRegion: AWSRegion + public let bucketName: String + } + + @_spi(InternalAmplifyConfiguration) + public struct CustomOutput: Codable {} + + @_spi(InternalAmplifyConfiguration) + public typealias AWSRegion = String + + @_spi(InternalAmplifyConfiguration) + public enum AmazonCognitoStandardAttributes: String, Codable, CodingKeyRepresentable { + case address + case birthdate + case email + case familyName + case gender + case givenName + case locale + case middleName + case name + case nickname + case phoneNumber + case picture + case preferredUsername + case profile + case sub + case updatedAt + case website + case zoneinfo + } + + @_spi(InternalAmplifyConfiguration) + public enum AWSAppSyncAuthorizationType: String, Codable { + case amazonCognitoUserPools = "AMAZON_COGNITO_USER_POOLS" + case apiKey = "API_KEY" + case awsIAM = "AWS_IAM" + case awsLambda = "AWS_LAMBDA" + case openIDConnect = "OPENID_CONNECT" + } + + @_spi(InternalAmplifyConfiguration) + public enum AmazonPinpointChannelType: String, Codable { + case inAppMessaging = "IN_APP_MESSAGING" + case fcm = "FCM" + case apns = "APNS" + case email = "EMAIL" + case sms = "SMS" + } + + // Internal init used for testing + init(version: String = "", + analytics: Analytics? = nil, + auth: Auth? = nil, + data: DataCategory? = nil, + geo: Geo? = nil, + notifications: Notifications? = nil, + storage: Storage? = nil, + custom: CustomOutput? = nil) { + self.version = version + self.analytics = analytics + self.auth = auth + self.data = data + self.geo = geo + self.notifications = notifications + self.storage = storage + self.custom = custom + } +} +// swiftlint:enable nesting + +// MARK: - Configure + +/// Represents helper methods to configure with Amplify CLI Gen2 configuration. +public struct AmplifyOutputs { + + /// A closure that resolves the `AmplifyOutputsData` configuration + let resolveConfiguration: () throws -> AmplifyOutputsData + + /// Resolves configuration with `amplify_outputs.json` in the main bundle. + public static let amplifyOutputs: AmplifyOutputs = { + .init { + try AmplifyOutputsData(bundle: Bundle.main, resource: "amplify_outputs") + } + }() + + /// Resolves configuration with a data object, from the contents of an `amplify_outputs.json` file. + public static func data(_ data: Data) -> AmplifyOutputs { + .init { + try AmplifyOutputsData.decodeAmplifyOutputsData(from: data) + } + } + + /// Resolves configuration with the resource in the main bundle. + public static func resource(named resource: String) -> AmplifyOutputs { + .init { + try AmplifyOutputsData(bundle: Bundle.main, resource: resource) + } + } +} + +extension Amplify { + + /// API to configure with Amplify CLI Gen2's configuration. + /// + /// - Parameter with: `AmplifyOutputs` configuration resolver + public static func configure(with amplifyOutputs: AmplifyOutputs) throws { + do { + let resolvedConfiguration = try amplifyOutputs.resolveConfiguration() + try configure(resolvedConfiguration) + } catch { + log.info("Failed to find configuration.") + if isRunningForSwiftUIPreviews { + log.info("Running for SwiftUI previews with no configuration file present, skipping configuration.") + return + } else { + throw error + } + } + } + + /// Configures Amplify with the specified configuration. + /// + /// This method must be invoked after registering plugins, and before using any Amplify category. It must not be + /// invoked more than once. + /// + /// **Lifecycle** + /// + /// Internally, Amplify configures the Hub and Logging categories first, so they are available to plugins in the + /// remaining categories during the configuration phase. Plugins for the Hub and Logging categories must not + /// assume that any other categories are available. + /// + /// After Amplify has configured all of its categories, it will dispatch a `HubPayload.EventName.Amplify.configured` + /// event to each Amplify Hub channel. After this point, plugins may invoke calls on other Amplify categories. + /// + /// - Parameter configuration: The AmplifyOutputsData object + /// + /// - Tag: Amplify.configure + @_spi(InternalAmplifyConfiguration) + public static func configure(_ configuration: AmplifyOutputsData) throws { + // Always configure logging first since Auth dependings on logging + try configure(CategoryType.logging.category, using: configuration) + + // Always configure Hub and Auth next, so they are available to other categories. + // Auth is a special case for other plugins which depend on using Auth when being configured themselves. + let manuallyConfiguredCategories = [CategoryType.hub, .auth] + for categoryType in manuallyConfiguredCategories { + try configure(categoryType.category, using: configuration) + } + + // Looping through all categories to ensure we don't accidentally forget a category at some point in the future + let remainingCategories = CategoryType.allCases.filter { !manuallyConfiguredCategories.contains($0) } + for categoryType in remainingCategories { + switch categoryType { + case .analytics: + try configure(Analytics, using: configuration) + case .api: + try configure(API, using: configuration) + case .dataStore: + try configure(DataStore, using: configuration) + case .geo: + try configure(Geo, using: configuration) + case .predictions: + try configure(Predictions, using: configuration) + case .pushNotifications: + try configure(Notifications.Push, using: configuration) + case .storage: + try configure(Storage, using: configuration) + case .hub, .logging, .auth: + // Already configured + break + } + } + isConfigured = true + + notifyAllHubChannels() + } + + /// If `candidate` is `CategoryConfigurable`, then invokes `candidate.configure(using: configuration)`. + private static func configure(_ candidate: Category, using configuration: AmplifyOutputsData) throws { + guard let configurable = candidate as? CategoryConfigurable else { + return + } + + try configurable.configure(using: configuration) + } +} diff --git a/Amplify/Core/Configuration/ConfigurationError.swift b/Amplify/Core/Configuration/ConfigurationError.swift index 48c52986b8..36d7d7abab 100644 --- a/Amplify/Core/Configuration/ConfigurationError.swift +++ b/Amplify/Core/Configuration/ConfigurationError.swift @@ -21,6 +21,11 @@ public enum ConfigurationError { /// - Tag: ConfigurationError.invalidAmplifyConfigurationFile case invalidAmplifyConfigurationFile(ErrorDescription, RecoverySuggestion, Error? = nil) + /// The specified `amplify_outputs.json` file was not present or unreadable + /// + /// - Tag: ConfigurationError.invalidAmplifyOutputsFile + case invalidAmplifyOutputsFile(ErrorDescription, RecoverySuggestion, Error? = nil) + /// Unable to decode `amplifyconfiguration.json` into a valid AmplifyConfiguration object /// /// - Tag: ConfigurationError.unableToDecode @@ -38,6 +43,7 @@ extension ConfigurationError: AmplifyError { switch self { case .amplifyAlreadyConfigured(let description, _, _), .invalidAmplifyConfigurationFile(let description, _, _), + .invalidAmplifyOutputsFile(let description, _, _), .unableToDecode(let description, _, _), .unknown(let description, _, _): return description @@ -49,6 +55,7 @@ extension ConfigurationError: AmplifyError { switch self { case .amplifyAlreadyConfigured(_, let recoverySuggestion, _), .invalidAmplifyConfigurationFile(_, let recoverySuggestion, _), + .invalidAmplifyOutputsFile(_, let recoverySuggestion, _), .unableToDecode(_, let recoverySuggestion, _), .unknown(_, let recoverySuggestion, _): return recoverySuggestion @@ -60,6 +67,7 @@ extension ConfigurationError: AmplifyError { switch self { case .amplifyAlreadyConfigured(_, _, let underlyingError), .invalidAmplifyConfigurationFile(_, _, let underlyingError), + .invalidAmplifyOutputsFile(_, _, let underlyingError), .unableToDecode(_, _, let underlyingError), .unknown(_, _, let underlyingError): return underlyingError diff --git a/Amplify/Core/Configuration/Internal/Amplify+Resolve.swift b/Amplify/Core/Configuration/Internal/Amplify+Resolve.swift index 27e9e7d3c0..43e2f7cc4b 100644 --- a/Amplify/Core/Configuration/Internal/Amplify+Resolve.swift +++ b/Amplify/Core/Configuration/Internal/Amplify+Resolve.swift @@ -16,5 +16,4 @@ extension Amplify { return try AmplifyConfiguration(bundle: Bundle.main) } - } diff --git a/Amplify/Core/Configuration/Internal/AmplifyConfigurationInitialization.swift b/Amplify/Core/Configuration/Internal/AmplifyConfigurationInitialization.swift index 7b1df887db..4c2b0ebcda 100644 --- a/Amplify/Core/Configuration/Internal/AmplifyConfigurationInitialization.swift +++ b/Amplify/Core/Configuration/Internal/AmplifyConfigurationInitialization.swift @@ -73,3 +73,73 @@ extension AmplifyConfiguration { } } + +extension AmplifyOutputsData { + init(bundle: Bundle, resource: String) throws { + guard let path = bundle.path(forResource: resource, ofType: "json") else { + throw ConfigurationError.invalidAmplifyOutputsFile( + """ + Could not load default `\(resource).json` file + """, + + """ + Expected to find the file, `\(resource).json` in the app bundle at `\(bundle.bundlePath)`, but + it was not present. Add `\(resource).json` to your app's "Copy Bundle Resources" build + phase and invoke `Amplify.configure(with: resource(named: "\(resource)")` with a configuration + object that you load. If your resource file is the default `amplify_outputs.json`, you can + invoke `Amplify.configure(with: .amplifyOutputs)` instead. + """ + ) + } + + let url = URL(fileURLWithPath: path) + + self = try AmplifyOutputsData.loadAmplifyOutputsData(from: url) + } + + static func loadAmplifyOutputsData(from url: URL) throws -> AmplifyOutputsData { + let fileData: Data + do { + fileData = try Data(contentsOf: url) + } catch { + throw ConfigurationError.invalidAmplifyOutputsFile( + """ + Could not extract UTF-8 data from `\(url.path)` + """, + + """ + Could not load data from the file at `\(url.path)`. Inspect the file to ensure it is present. + The system reported the following error: + \(error.localizedDescription) + """, + error + ) + } + + return try decodeAmplifyOutputsData(from: fileData) + } + + static func decodeAmplifyOutputsData(from data: Data) throws -> AmplifyOutputsData { + let jsonDecoder = JSONDecoder() + + do { + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + let configuration = try jsonDecoder.decode(AmplifyOutputsData.self, from: data) + return configuration + } catch { + throw ConfigurationError.unableToDecode( + """ + Could not decode `amplify_outputs.json`. + """, + + """ + `amplify_outputs.json` was found, but could not be converted to an object + using JSONDecoder. The system reported the following error: + \(error.localizedDescription) + """, + error + ) + } + } + +} diff --git a/Amplify/Core/Configuration/Internal/Category+Configuration.swift b/Amplify/Core/Configuration/Internal/Category+Configuration.swift index f3ae70888f..ea8e81af0a 100644 --- a/Amplify/Core/Configuration/Internal/Category+Configuration.swift +++ b/Amplify/Core/Configuration/Internal/Category+Configuration.swift @@ -37,5 +37,4 @@ extension CategoryTypeable { return amplifyConfiguration.auth } } - } diff --git a/Amplify/Core/Configuration/Internal/CategoryConfigurable.swift b/Amplify/Core/Configuration/Internal/CategoryConfigurable.swift index 8873a30a77..d92c18d93d 100644 --- a/Amplify/Core/Configuration/Internal/CategoryConfigurable.swift +++ b/Amplify/Core/Configuration/Internal/CategoryConfigurable.swift @@ -20,7 +20,11 @@ protocol CategoryConfigurable: AnyObject, CategoryTypeable { /// - Parameter amplifyConfiguration: The AmplifyConfiguration func configure(using amplifyConfiguration: AmplifyConfiguration) throws + /// Convenience method for configuring the category using the top-level AmplifyOutputsData + /// + /// - Parameter amplifyOutputs: The AmplifyOutputsData configuration + func configure(using amplifyOutputs: AmplifyOutputsData) throws + /// Clears the category configurations, and invokes `reset` on each added plugin func reset() async - } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift index 8ea423d995..ec27a34b41 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import AWSPluginsCore import AwsCommonRuntimeKit @@ -19,8 +19,15 @@ public extension AWSAPIPlugin { /// - Throws: /// - PluginError.pluginConfigurationError: If one of the required configuration values is invalid or empty func configure(using configuration: Any?) throws { + let dependencies: ConfigurationDependencies + if let configuration = configuration as? AmplifyOutputsData { + dependencies = try ConfigurationDependencies(configuration: configuration, + apiAuthProviderFactory: authProviderFactory) + } else if let jsonValue = configuration as? JSONValue { + dependencies = try ConfigurationDependencies(configurationValues: jsonValue, + apiAuthProviderFactory: authProviderFactory) - guard let jsonValue = configuration as? JSONValue else { + } else { throw PluginError.pluginConfigurationError( "Could not cast incoming configuration to JSONValue", """ @@ -32,8 +39,6 @@ public extension AWSAPIPlugin { ) } - let dependencies = try ConfigurationDependencies(configurationValues: jsonValue, - apiAuthProviderFactory: authProviderFactory) configure(using: dependencies) // Initialize SwiftSDK's CRT dependency for SigV4 signing functionality @@ -63,7 +68,7 @@ extension AWSAPIPlugin { logLevel: Amplify.LogLevel? = nil ) throws { let authService = authService - ?? AWSAuthService() + ?? AWSAuthService() let pluginConfig = try AWSAPICategoryPluginConfiguration( jsonValue: configurationValues, @@ -82,6 +87,28 @@ extension AWSAPIPlugin { ) } + init( + configuration: AmplifyOutputsData, + apiAuthProviderFactory: APIAuthProviderFactory, + authService: AWSAuthServiceBehavior = AWSAuthService(), + appSyncRealTimeClientFactory: AppSyncRealTimeClientFactoryProtocol? = nil, + logLevel: Amplify.LogLevel = Amplify.Logging.logLevel + ) throws { + let pluginConfig = try AWSAPICategoryPluginConfiguration( + configuration: configuration, + apiAuthProviderFactory: apiAuthProviderFactory, + authService: authService + ) + + self.init( + pluginConfig: pluginConfig, + authService: authService, + appSyncRealTimeClientFactory: appSyncRealTimeClientFactory + ?? AppSyncRealTimeClientFactory(), + logLevel: logLevel + ) + } + init( pluginConfig: AWSAPICategoryPluginConfiguration, authService: AWSAuthServiceBehavior, diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift index ce124f1f54..e7ea03dc09 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift @@ -10,6 +10,10 @@ import AWSPluginsCore import Foundation final public class AWSAPIPlugin: NSObject, APICategoryPlugin, APICategoryGraphQLBehaviorExtended, AWSAPIAuthInformation { + /// Used for the default GraphQL API represented by the `data` category in `amplify_outputs.json` + /// This constant is not used for APIs present in `amplifyconfiguration.json` since they always have names. + public static let defaultGraphQLAPI = "defaultGraphQLAPI" + /// The unique key of the plugin within the API category. public var key: PluginKey { return "awsAPIPlugin" diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration+EndpointConfig.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration+EndpointConfig.swift index a5fda2fe29..8be955bdb2 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration+EndpointConfig.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration+EndpointConfig.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import Foundation import AWSPluginsCore @@ -59,6 +59,21 @@ public extension AWSAPICategoryPluginConfiguration { authService: authService) } + init(name: String, + config: AmplifyOutputsData.DataCategory, + apiAuthProviderFactory: APIAuthProviderFactory, + authService: AWSAuthServiceBehavior? = nil) throws { + + try self.init(name: name, + baseURL: try EndpointConfig.getBaseURL(from: config.url), + region: config.awsRegion, + authorizationType: try AWSAuthorizationType.from(authorizationTypeString: config.defaultAuthorizationType.rawValue), + endpointType: .graphQL, + apiKey: config.apiKey, + apiAuthProviderFactory: apiAuthProviderFactory, + authService: authService) + } + init(name: String, baseURL: URL, region: AWSRegionType?, @@ -98,13 +113,16 @@ public extension AWSAPICategoryPluginConfiguration { ) } + return try getBaseURL(from: baseURLString) + } + + private static func getBaseURL(from baseURLString: String) throws -> URL { guard let baseURL = URL(string: baseURLString) else { throw PluginError.pluginConfigurationError( "Could not convert `\(baseURLString)` to a URL", """ The "endpoint" value in the specified configuration cannot be converted to a URL. Review the \ - configuration and ensure it contains the expected values: - \(endpointJSON) + configuration and ensure it contains the expected values. """ ) } @@ -174,6 +192,11 @@ private extension AWSAuthorizationType { ) } + return try from(authorizationTypeString: authorizationTypeString) + + } + + static func from(authorizationTypeString: String) throws -> AWSAuthorizationType { guard let authorizationType = AWSAuthorizationType(rawValue: authorizationTypeString) else { let authTypes = AWSAuthorizationType.allCases.map { $0.rawValue }.joined(separator: ", ") throw PluginError.pluginConfigurationError( @@ -181,8 +204,7 @@ private extension AWSAuthorizationType { """ The "authorizationType" value in the specified configuration cannot be converted to an \ AWSAuthorizationType. Review the configuration and ensure it contains a valid value \ - (\(authTypes)): - \(endpointJSON) + (\(authTypes)) """ ) } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift index 200a813e96..1ea015dd7f 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import Foundation import AWSPluginsCore @@ -48,6 +48,36 @@ public struct AWSAPICategoryPluginConfiguration { } + init(configuration: AmplifyOutputsData, + apiAuthProviderFactory: APIAuthProviderFactory, + authService: AWSAuthServiceBehavior) throws { + + guard let data = configuration.data else { + throw PluginError.pluginConfigurationError( + "Missing `data` category in the configuration.", + """ + The specified configuration does not contain `data` category. Review the configuration and ensure it \ + contains the expected values. + """ + ) + } + + let endpoints = try AWSAPICategoryPluginConfiguration.endpointsFromConfig( + config: data, + apiAuthProviderFactory: apiAuthProviderFactory, + authService: authService) + let interceptors = try AWSAPICategoryPluginConfiguration.makeInterceptors( + forEndpoints: endpoints, + apiAuthProviderFactory: apiAuthProviderFactory, + authService: authService) + + self.init(endpoints: endpoints, + interceptors: interceptors, + apiAuthProviderFactory: apiAuthProviderFactory, + authService: authService) + + } + /// Used for testing /// - Parameters: /// - endpoints: dictionary of EndpointConfig whose keys are the API endpoint name @@ -160,6 +190,21 @@ public struct AWSAPICategoryPluginConfiguration { return endpoints } + private static func endpointsFromConfig( + config: AmplifyOutputsData.DataCategory, + apiAuthProviderFactory: APIAuthProviderFactory, + authService: AWSAuthServiceBehavior + ) throws -> [APIEndpointName: EndpointConfig] { + var endpoints = [APIEndpointName: EndpointConfig]() + let name = AWSAPIPlugin.defaultGraphQLAPI + let endpointConfig = try EndpointConfig(name: name, + config: config, + apiAuthProviderFactory: apiAuthProviderFactory, + authService: authService) + endpoints[name] = endpointConfig + return endpoints + } + /// Given a dictionary of EndpointConfig indexed by API endpoint name, /// builds a dictionary of AWSAPIEndpointInterceptors. /// - Parameters: diff --git a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj index 495154738f..15f0bc7a05 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj @@ -214,6 +214,76 @@ 21EA887F28F9BCC30000BA75 /* AsyncExpectation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFE7028E7451D0000C36A /* AsyncExpectation.swift */; }; 21EA888028F9BCC50000BA75 /* XCTestCase+AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFE7128E7451D0000C36A /* XCTestCase+AsyncTesting.swift */; }; 21EA888228F9BCD90000BA75 /* TestConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21EA888128F9BCD90000BA75 /* TestConfigHelper.swift */; }; + 21F762512BD6B0710048845A /* Team2+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126271F289ABFE9003788E3 /* Team2+Schema.swift */; }; + 21F762522BD6B0710048845A /* EnumTestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262707289ABFE6003788E3 /* EnumTestModel.swift */; }; + 21F762532BD6B0710048845A /* GraphQLScalarAPISwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21809B802A69D09B00F70E38 /* GraphQLScalarAPISwiftTests.swift */; }; + 21F762542BD6B0710048845A /* ScalarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262720289ABFE9003788E3 /* ScalarContainer.swift */; }; + 21F762552BD6B0710048845A /* Project2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262719289ABFE8003788E3 /* Project2.swift */; }; + 21F762562BD6B0710048845A /* EnumTestModel+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126271E289ABFE9003788E3 /* EnumTestModel+Schema.swift */; }; + 21F762572BD6B0710048845A /* ListStringContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262715289ABFE8003788E3 /* ListStringContainer.swift */; }; + 21F762582BD6B0710048845A /* GraphQLConnectionScenario3Tests+List.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AAD2889996A004BD994 /* GraphQLConnectionScenario3Tests+List.swift */; }; + 21F762592BD6B0710048845A /* GraphQLConnectionScenario3Tests+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AB02889996A004BD994 /* GraphQLConnectionScenario3Tests+Helpers.swift */; }; + 21F7625A2BD6B0710048845A /* AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFE6F28E7451D0000C36A /* AsyncTesting.swift */; }; + 21F7625B2BD6B0710048845A /* ListIntContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126270C289ABFE6003788E3 /* ListIntContainer.swift */; }; + 21F7625C2BD6B0710048845A /* Team2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262712289ABFE7003788E3 /* Team2.swift */; }; + 21F7625D2BD6B0710048845A /* GraphQLConnectionScenario3APISwiftTests+Subscribe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E581E52A698C4D0027D13A /* GraphQLConnectionScenario3APISwiftTests+Subscribe.swift */; }; + 21F7625E2BD6B0710048845A /* Todo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698A9F28899921004BD994 /* Todo.swift */; }; + 21F7625F2BD6B0710048845A /* ListStringContainer+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262724289ABFE9003788E3 /* ListStringContainer+Schema.swift */; }; + 21F762602BD6B0710048845A /* Project2+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262710289ABFE7003788E3 /* Project2+Schema.swift */; }; + 21F762612BD6B0710048845A /* NestedTypeTestModel+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126272F289ABFEB003788E3 /* NestedTypeTestModel+Schema.swift */; }; + 21F762622BD6B0710048845A /* NestedTypeTestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126272D289ABFEB003788E3 /* NestedTypeTestModel.swift */; }; + 21F762632BD6B0710048845A /* Post5+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262714289ABFE7003788E3 /* Post5+Schema.swift */; }; + 21F762642BD6B0710048845A /* TestEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126272C289ABFEB003788E3 /* TestEnum.swift */; }; + 21F762652BD6B0710048845A /* GraphQLConnectionScenario3Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AAA2889996A004BD994 /* GraphQLConnectionScenario3Tests.swift */; }; + 21F762662BD6B0710048845A /* XCTestCase+AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFE7128E7451D0000C36A /* XCTestCase+AsyncTesting.swift */; }; + 21F762672BD6B0710048845A /* GraphQLTestBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AAF2889996A004BD994 /* GraphQLTestBase.swift */; }; + 21F762682BD6B0710048845A /* GraphQLConnectionScenario4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AB32889996A004BD994 /* GraphQLConnectionScenario4Tests.swift */; }; + 21F762692BD6B0710048845A /* PostEditor5+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262721289ABFE9003788E3 /* PostEditor5+Schema.swift */; }; + 21F7626A2BD6B0710048845A /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E581E32A6835910027D13A /* API.swift */; }; + 21F7626B2BD6B0710048845A /* Nested.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262711289ABFE7003788E3 /* Nested.swift */; }; + 21F7626C2BD6B0710048845A /* Comment3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126272A289ABFEA003788E3 /* Comment3.swift */; }; + 21F7626D2BD6B0710048845A /* Comment6+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126271B289ABFE8003788E3 /* Comment6+Schema.swift */; }; + 21F7626E2BD6B0710048845A /* TestConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262523289ABB0C003788E3 /* TestConfigHelper.swift */; }; + 21F7626F2BD6B0710048845A /* Team1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126270D289ABFE6003788E3 /* Team1.swift */; }; + 21F762702BD6B0710048845A /* Comment+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126272B289ABFEA003788E3 /* Comment+Schema.swift */; }; + 21F762712BD6B0710048845A /* GraphQLConnectionScenario2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AB62889996A004BD994 /* GraphQLConnectionScenario2Tests.swift */; }; + 21F762722BD6B0710048845A /* Post5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262703289ABFE5003788E3 /* Post5.swift */; }; + 21F762732BD6B0710048845A /* Nested+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262718289ABFE8003788E3 /* Nested+Schema.swift */; }; + 21F762742BD6B0710048845A /* Post3+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126270A289ABFE6003788E3 /* Post3+Schema.swift */; }; + 21F762752BD6B0710048845A /* GraphQLConnectionScenario6Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AAE2889996A004BD994 /* GraphQLConnectionScenario6Tests.swift */; }; + 21F762762BD6B0710048845A /* GraphQLConnectionScenario1APISwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E581E12A6707900027D13A /* GraphQLConnectionScenario1APISwiftTests.swift */; }; + 21F762772BD6B0710048845A /* Comment4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262717289ABFE8003788E3 /* Comment4.swift */; }; + 21F762782BD6B0710048845A /* GraphQLModelBasedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AB42889996A004BD994 /* GraphQLModelBasedTests.swift */; }; + 21F762792BD6B0710048845A /* ListIntContainer+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262716289ABFE8003788E3 /* ListIntContainer+Schema.swift */; }; + 21F7627A2BD6B0710048845A /* AsyncExpectation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFE7028E7451D0000C36A /* AsyncExpectation.swift */; }; + 21F7627B2BD6B0710048845A /* Post4+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262727289ABFEA003788E3 /* Post4+Schema.swift */; }; + 21F7627C2BD6B0710048845A /* GraphQLConnectionScenario3Tests+Subscribe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AB22889996A004BD994 /* GraphQLConnectionScenario3Tests+Subscribe.swift */; }; + 21F7627D2BD6B0710048845A /* Post+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126272E289ABFEB003788E3 /* Post+Schema.swift */; }; + 21F7627E2BD6B0710048845A /* Blog6.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126270E289ABFE7003788E3 /* Blog6.swift */; }; + 21F7627F2BD6B0710048845A /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126271C289ABFE8003788E3 /* Comment.swift */; }; + 21F762802BD6B0710048845A /* Post6.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126270F289ABFE7003788E3 /* Post6.swift */; }; + 21F762812BD6B0710048845A /* AppSyncRealTimeClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 606C8B782B895E5A00716094 /* AppSyncRealTimeClientTests.swift */; }; + 21F762822BD6B0710048845A /* Post3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262730289ABFEB003788E3 /* Post3.swift */; }; + 21F762832BD6B0710048845A /* User5+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262728289ABFEA003788E3 /* User5+Schema.swift */; }; + 21F762842BD6B0710048845A /* Blog6+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262725289ABFEA003788E3 /* Blog6+Schema.swift */; }; + 21F762852BD6B0710048845A /* Comment6.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126271D289ABFE9003788E3 /* Comment6.swift */; }; + 21F762862BD6B0710048845A /* GraphQLScalarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AAB2889996A004BD994 /* GraphQLScalarTests.swift */; }; + 21F762872BD6B0710048845A /* Post4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262726289ABFEA003788E3 /* Post4.swift */; }; + 21F762882BD6B0710048845A /* ScalarContainer+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262702289ABFE4003788E3 /* ScalarContainer+Schema.swift */; }; + 21F762892BD6B0710048845A /* Project1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262723289ABFE9003788E3 /* Project1.swift */; }; + 21F7628A2BD6B0710048845A /* Team1+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126270B289ABFE6003788E3 /* Team1+Schema.swift */; }; + 21F7628B2BD6B0710048845A /* AmplifyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2126271A289ABFE8003788E3 /* AmplifyModels.swift */; }; + 21F7628C2BD6B0710048845A /* User5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262706289ABFE5003788E3 /* User5.swift */; }; + 21F7628D2BD6B0710048845A /* GraphQLConnectionScenario1Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AA82889996A004BD994 /* GraphQLConnectionScenario1Tests.swift */; }; + 21F7628E2BD6B0710048845A /* PostStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262722289ABFE9003788E3 /* PostStatus.swift */; }; + 21F7628F2BD6B0710048845A /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262704289ABFE5003788E3 /* Post.swift */; }; + 21F762902BD6B0710048845A /* Project1+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262713289ABFE7003788E3 /* Project1+Schema.swift */; }; + 21F762912BD6B0710048845A /* Comment3+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262729289ABFEA003788E3 /* Comment3+Schema.swift */; }; + 21F762922BD6B0710048845A /* GraphQLModelBasedTests+List.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AAC2889996A004BD994 /* GraphQLModelBasedTests+List.swift */; }; + 21F762932BD6B0710048845A /* Comment4+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262708289ABFE6003788E3 /* Comment4+Schema.swift */; }; + 21F762942BD6B0710048845A /* Post6+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262709289ABFE6003788E3 /* Post6+Schema.swift */; }; + 21F762952BD6B0710048845A /* PostEditor5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21262705289ABFE5003788E3 /* PostEditor5.swift */; }; + 21F762962BD6B0710048845A /* GraphQLConnectionScenario5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AB52889996A004BD994 /* GraphQLConnectionScenario5Tests.swift */; }; 21FA8EF7295C9609009F6A07 /* GraphQLLazyLoadHasOneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21FA8EF6295C9609009F6A07 /* GraphQLLazyLoadHasOneTests.swift */; }; 21FA8EF9295C962E009F6A07 /* GraphQLLazyLoadDefaultPKTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21FA8EF8295C962E009F6A07 /* GraphQLLazyLoadDefaultPKTests.swift */; }; 21FA8EFB295C9647009F6A07 /* GraphQLLazyLoadCompositePKTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21FA8EFA295C9647009F6A07 /* GraphQLLazyLoadCompositePKTests.swift */; }; @@ -385,6 +455,13 @@ remoteGlobalIDString = 21E73E6A28898D7800D7DB7E; remoteInfo = APIHostApp; }; + 21F7624F2BD6B0710048845A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 21E73E6328898D7800D7DB7E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 21E73E6A28898D7800D7DB7E; + remoteInfo = APIHostApp; + }; 395906B028AC4A16004B96B1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 21E73E6328898D7800D7DB7E /* Project object */; @@ -443,6 +520,19 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 212371362BBB0279003B1B44 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 21262523289ABB0C003788E3 /* TestConfigHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConfigHelper.swift; sourceTree = ""; }; 21262702289ABFE4003788E3 /* ScalarContainer+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ScalarContainer+Schema.swift"; sourceTree = ""; }; @@ -665,6 +755,8 @@ 21EA887D28F9BCBB0000BA75 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 21EA888128F9BCD90000BA75 /* TestConfigHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestConfigHelper.swift; sourceTree = ""; }; 21EA888328F9BD2D0000BA75 /* lazyload-schema.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = "lazyload-schema.graphql"; sourceTree = ""; }; + 21F7629D2BD6B0710048845A /* AWSAPIPluginGen2FunctionalTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSAPIPluginGen2FunctionalTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 21F7629E2BD6B0B40048845A /* AWSAPIPluginGen2FunctionalTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AWSAPIPluginGen2FunctionalTests.xctestplan; sourceTree = ""; }; 21FA8EF6295C9609009F6A07 /* GraphQLLazyLoadHasOneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLLazyLoadHasOneTests.swift; sourceTree = ""; }; 21FA8EF8295C962E009F6A07 /* GraphQLLazyLoadDefaultPKTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLLazyLoadDefaultPKTests.swift; sourceTree = ""; }; 21FA8EFA295C9647009F6A07 /* GraphQLLazyLoadCompositePKTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLLazyLoadCompositePKTests.swift; sourceTree = ""; }; @@ -737,6 +829,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 21F762972BD6B0710048845A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 395906A928AC4A16004B96B1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -899,6 +998,7 @@ 21698A7C28899805004BD994 /* AWSAPIPluginFunctionalTests */ = { isa = PBXGroup; children = ( + 21F7629E2BD6B0B40048845A /* AWSAPIPluginGen2FunctionalTests.xctestplan */, 21E581E32A6835910027D13A /* API.swift */, 212626CA289ABC79003788E3 /* Base */, 606C8B782B895E5A00716094 /* AppSyncRealTimeClientTests.swift */, @@ -1189,6 +1289,7 @@ 681B35892A43962D0074F369 /* AWSAPIPluginFunctionalTestsWatch.xctest */, 681B35A12A4396CF0074F369 /* AWSAPIPluginGraphQLLambdaAuthTestsWatch.xctest */, 681B35C52A43970A0074F369 /* AWSAPIPluginRESTIAMTestsWatch.xctest */, + 21F7629D2BD6B0710048845A /* AWSAPIPluginGen2FunctionalTests.xctest */, ); name = Products; sourceTree = ""; @@ -1475,6 +1576,7 @@ 21E73E6728898D7800D7DB7E /* Sources */, 21E73E6828898D7800D7DB7E /* Frameworks */, 21E73E6928898D7800D7DB7E /* Resources */, + 212371362BBB0279003B1B44 /* Embed Frameworks */, ); buildRules = ( ); @@ -1510,6 +1612,25 @@ productReference = 21EA887328F9BC600000BA75 /* AWSAPIPluginLazyLoadTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 21F7624D2BD6B0710048845A /* AWSAPIPluginGen2FunctionalTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 21F7629A2BD6B0710048845A /* Build configuration list for PBXNativeTarget "AWSAPIPluginGen2FunctionalTests" */; + buildPhases = ( + 21F762502BD6B0710048845A /* Sources */, + 21F762972BD6B0710048845A /* Frameworks */, + 21F762982BD6B0710048845A /* Resources */, + 21F762992BD6B0710048845A /* Copy Configuration folder */, + ); + buildRules = ( + ); + dependencies = ( + 21F7624E2BD6B0710048845A /* PBXTargetDependency */, + ); + name = AWSAPIPluginGen2FunctionalTests; + productName = AWSAPIPluginFunctionalTests; + productReference = 21F7629D2BD6B0710048845A /* AWSAPIPluginGen2FunctionalTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 395906AB28AC4A16004B96B1 /* AWSAPIPluginRESTIAMTests */ = { isa = PBXNativeTarget; buildConfigurationList = 395906B428AC4A16004B96B1 /* Build configuration list for PBXNativeTarget "AWSAPIPluginRESTIAMTests" */; @@ -1706,7 +1827,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1430; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1340; TargetAttributes = { 213DBC7428A6C47000B30280 = { @@ -1790,6 +1911,7 @@ 681B353E2A43962D0074F369 /* AWSAPIPluginFunctionalTestsWatch */, 681B35912A4396CF0074F369 /* AWSAPIPluginGraphQLLambdaAuthTestsWatch */, 681B35B62A43970A0074F369 /* AWSAPIPluginRESTIAMTestsWatch */, + 21F7624D2BD6B0710048845A /* AWSAPIPluginGen2FunctionalTests */, ); }; /* End PBXProject section */ @@ -1832,6 +1954,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 21F762982BD6B0710048845A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 395906AA28AC4A16004B96B1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1953,6 +2082,24 @@ shellPath = /bin/sh; shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nTEMP_FILE=$HOME/.aws-amplify/amplify-ios/testconfiguration/.\nDEST_PATH=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/testconfiguration/\"\n\nif [[ ! -d $TEMP_FILE ]] ; then\n echo \"${TEMP_FILE} does not exist. Using empty configuration.\"\n exit 0\nfi\n\nif [[ -f $DEST_PATH ]] ; then\n rm $DEST_PATH\nfi\n \ncp -r $TEMP_FILE $DEST_PATH\n"; }; + 21F762992BD6B0710048845A /* Copy Configuration folder */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy Configuration folder"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "TEMP_FILE=$HOME/.aws-amplify/amplify-ios/testconfiguration/.\nDEST_PATH=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/testconfiguration/\"\n\nif [[ ! -d $TEMP_FILE ]] ; then\n echo \"${TEMP_FILE} does not exist. Using empty configuration.\"\n exit 0\nfi\n\nif [[ -f $DEST_PATH ]] ; then\n rm $DEST_PATH\nfi\n \ncp -r $TEMP_FILE $DEST_PATH\n"; + }; 395906B528AC4A22004B96B1 /* Copy Configuration Files */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -2364,6 +2511,83 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 21F762502BD6B0710048845A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 21F762512BD6B0710048845A /* Team2+Schema.swift in Sources */, + 21F762522BD6B0710048845A /* EnumTestModel.swift in Sources */, + 21F762532BD6B0710048845A /* GraphQLScalarAPISwiftTests.swift in Sources */, + 21F762542BD6B0710048845A /* ScalarContainer.swift in Sources */, + 21F762552BD6B0710048845A /* Project2.swift in Sources */, + 21F762562BD6B0710048845A /* EnumTestModel+Schema.swift in Sources */, + 21F762572BD6B0710048845A /* ListStringContainer.swift in Sources */, + 21F762582BD6B0710048845A /* GraphQLConnectionScenario3Tests+List.swift in Sources */, + 21F762592BD6B0710048845A /* GraphQLConnectionScenario3Tests+Helpers.swift in Sources */, + 21F7625A2BD6B0710048845A /* AsyncTesting.swift in Sources */, + 21F7625B2BD6B0710048845A /* ListIntContainer.swift in Sources */, + 21F7625C2BD6B0710048845A /* Team2.swift in Sources */, + 21F7625D2BD6B0710048845A /* GraphQLConnectionScenario3APISwiftTests+Subscribe.swift in Sources */, + 21F7625E2BD6B0710048845A /* Todo.swift in Sources */, + 21F7625F2BD6B0710048845A /* ListStringContainer+Schema.swift in Sources */, + 21F762602BD6B0710048845A /* Project2+Schema.swift in Sources */, + 21F762612BD6B0710048845A /* NestedTypeTestModel+Schema.swift in Sources */, + 21F762622BD6B0710048845A /* NestedTypeTestModel.swift in Sources */, + 21F762632BD6B0710048845A /* Post5+Schema.swift in Sources */, + 21F762642BD6B0710048845A /* TestEnum.swift in Sources */, + 21F762652BD6B0710048845A /* GraphQLConnectionScenario3Tests.swift in Sources */, + 21F762662BD6B0710048845A /* XCTestCase+AsyncTesting.swift in Sources */, + 21F762672BD6B0710048845A /* GraphQLTestBase.swift in Sources */, + 21F762682BD6B0710048845A /* GraphQLConnectionScenario4Tests.swift in Sources */, + 21F762692BD6B0710048845A /* PostEditor5+Schema.swift in Sources */, + 21F7626A2BD6B0710048845A /* API.swift in Sources */, + 21F7626B2BD6B0710048845A /* Nested.swift in Sources */, + 21F7626C2BD6B0710048845A /* Comment3.swift in Sources */, + 21F7626D2BD6B0710048845A /* Comment6+Schema.swift in Sources */, + 21F7626E2BD6B0710048845A /* TestConfigHelper.swift in Sources */, + 21F7626F2BD6B0710048845A /* Team1.swift in Sources */, + 21F762702BD6B0710048845A /* Comment+Schema.swift in Sources */, + 21F762712BD6B0710048845A /* GraphQLConnectionScenario2Tests.swift in Sources */, + 21F762722BD6B0710048845A /* Post5.swift in Sources */, + 21F762732BD6B0710048845A /* Nested+Schema.swift in Sources */, + 21F762742BD6B0710048845A /* Post3+Schema.swift in Sources */, + 21F762752BD6B0710048845A /* GraphQLConnectionScenario6Tests.swift in Sources */, + 21F762762BD6B0710048845A /* GraphQLConnectionScenario1APISwiftTests.swift in Sources */, + 21F762772BD6B0710048845A /* Comment4.swift in Sources */, + 21F762782BD6B0710048845A /* GraphQLModelBasedTests.swift in Sources */, + 21F762792BD6B0710048845A /* ListIntContainer+Schema.swift in Sources */, + 21F7627A2BD6B0710048845A /* AsyncExpectation.swift in Sources */, + 21F7627B2BD6B0710048845A /* Post4+Schema.swift in Sources */, + 21F7627C2BD6B0710048845A /* GraphQLConnectionScenario3Tests+Subscribe.swift in Sources */, + 21F7627D2BD6B0710048845A /* Post+Schema.swift in Sources */, + 21F7627E2BD6B0710048845A /* Blog6.swift in Sources */, + 21F7627F2BD6B0710048845A /* Comment.swift in Sources */, + 21F762802BD6B0710048845A /* Post6.swift in Sources */, + 21F762812BD6B0710048845A /* AppSyncRealTimeClientTests.swift in Sources */, + 21F762822BD6B0710048845A /* Post3.swift in Sources */, + 21F762832BD6B0710048845A /* User5+Schema.swift in Sources */, + 21F762842BD6B0710048845A /* Blog6+Schema.swift in Sources */, + 21F762852BD6B0710048845A /* Comment6.swift in Sources */, + 21F762862BD6B0710048845A /* GraphQLScalarTests.swift in Sources */, + 21F762872BD6B0710048845A /* Post4.swift in Sources */, + 21F762882BD6B0710048845A /* ScalarContainer+Schema.swift in Sources */, + 21F762892BD6B0710048845A /* Project1.swift in Sources */, + 21F7628A2BD6B0710048845A /* Team1+Schema.swift in Sources */, + 21F7628B2BD6B0710048845A /* AmplifyModels.swift in Sources */, + 21F7628C2BD6B0710048845A /* User5.swift in Sources */, + 21F7628D2BD6B0710048845A /* GraphQLConnectionScenario1Tests.swift in Sources */, + 21F7628E2BD6B0710048845A /* PostStatus.swift in Sources */, + 21F7628F2BD6B0710048845A /* Post.swift in Sources */, + 21F762902BD6B0710048845A /* Project1+Schema.swift in Sources */, + 21F762912BD6B0710048845A /* Comment3+Schema.swift in Sources */, + 21F762922BD6B0710048845A /* GraphQLModelBasedTests+List.swift in Sources */, + 21F762932BD6B0710048845A /* Comment4+Schema.swift in Sources */, + 21F762942BD6B0710048845A /* Post6+Schema.swift in Sources */, + 21F762952BD6B0710048845A /* PostEditor5.swift in Sources */, + 21F762962BD6B0710048845A /* GraphQLConnectionScenario5Tests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 395906A828AC4A16004B96B1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2566,6 +2790,11 @@ target = 21E73E6A28898D7800D7DB7E /* APIHostApp */; targetProxy = 21EA887728F9BC610000BA75 /* PBXContainerItemProxy */; }; + 21F7624E2BD6B0710048845A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 21E73E6A28898D7800D7DB7E /* APIHostApp */; + targetProxy = 21F7624F2BD6B0710048845A /* PBXContainerItemProxy */; + }; 395906B128AC4A16004B96B1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 21E73E6A28898D7800D7DB7E /* APIHostApp */; @@ -2989,6 +3218,61 @@ }; name = Release; }; + 21F7629B2BD6B0710048845A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.api.AWSAPIPluginFunctionalTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/APIHostApp.app/APIHostApp"; + }; + name = Debug; + }; + 21F7629C2BD6B0710048845A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.api.AWSAPIPluginFunctionalTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/APIHostApp.app/APIHostApp"; + }; + name = Release; + }; 395906B228AC4A16004B96B1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3470,6 +3754,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 21F7629A2BD6B0710048845A /* Build configuration list for PBXNativeTarget "AWSAPIPluginGen2FunctionalTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 21F7629B2BD6B0710048845A /* Debug */, + 21F7629C2BD6B0710048845A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 395906B428AC4A16004B96B1 /* Build configuration list for PBXNativeTarget "AWSAPIPluginRESTIAMTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/APIHostApp.xcscheme b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/APIHostApp.xcscheme index 5e6d13047d..8d3f8617a9 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/APIHostApp.xcscheme +++ b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/APIHostApp.xcscheme @@ -49,6 +49,17 @@ ReferencedContainer = "container:APIHostApp.xcodeproj"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AWSAPIPluginGen2FunctionalTests.xctestplan b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AWSAPIPluginGen2FunctionalTests.xctestplan new file mode 100644 index 0000000000..1567400a72 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AWSAPIPluginGen2FunctionalTests.xctestplan @@ -0,0 +1,39 @@ +{ + "configurations" : [ + { + "id" : "59DC9034-3288-4494-BBD9-9F891FF0A7FA", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "commandLineArgumentEntries" : [ + { + "argument" : "GEN2" + } + ] + }, + "testTargets" : [ + { + "skippedTests" : [ + "AppSyncRealTimeClientTests", + "GraphQLConnectionScenario1Tests", + "GraphQLConnectionScenario2Tests", + "GraphQLConnectionScenario3Tests", + "GraphQLConnectionScenario4Tests", + "GraphQLConnectionScenario5Tests", + "GraphQLConnectionScenario6Tests", + "GraphQLScalarTests", + "GraphQLTestBase" + ], + "target" : { + "containerPath" : "container:APIHostApp.xcodeproj", + "identifier" : "21F7624D2BD6B0710048845A", + "name" : "AWSAPIPluginGen2FunctionalTests" + } + } + ], + "version" : 1 +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/Base/TestConfigHelper.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/Base/TestConfigHelper.swift index 44837045b1..1cec4e476b 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/Base/TestConfigHelper.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/Base/TestConfigHelper.swift @@ -6,16 +6,25 @@ // import Foundation -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify class TestConfigHelper { + static var useGen2Configuration: Bool { + ProcessInfo.processInfo.arguments.contains("GEN2") + } + static func retrieveAmplifyConfiguration(forResource: String) throws -> AmplifyConfiguration { let data = try retrieve(forResource: forResource) return try AmplifyConfiguration.decodeAmplifyConfiguration(from: data) } + static func retrieveAmplifyOutputsData(forResource: String) throws -> AmplifyOutputsData { + let data = try retrieve(forResource: forResource) + return try AmplifyOutputsData.decodeAmplifyOutputsData(from: data) + } + static func retrieveCredentials(forResource: String) throws -> [String: String] { let data = try retrieve(forResource: forResource) diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests.swift index c5c6b87cb4..1790c35b16 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests.swift @@ -7,7 +7,7 @@ import XCTest @testable import AWSAPIPlugin -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify #if os(watchOS) @testable import APIWatchApp #else @@ -18,7 +18,8 @@ import XCTest class GraphQLModelBasedTests: XCTestCase { static let amplifyConfiguration = "testconfiguration/GraphQLModelBasedTests-amplifyconfiguration" - + static let amplifyOutputs = "testconfiguration/GraphQLModelBasedTests-amplify_outputs" + final public class PostCommentModelRegistration: AmplifyModelRegistration { public func registerModels(registry: ModelRegistry.Type) { ModelRegistry.register(modelType: Post.self) @@ -37,10 +38,15 @@ class GraphQLModelBasedTests: XCTestCase { do { try Amplify.add(plugin: plugin) - let amplifyConfig = try TestConfigHelper.retrieveAmplifyConfiguration( - forResource: GraphQLModelBasedTests.amplifyConfiguration) - try Amplify.configure(amplifyConfig) - + if TestConfigHelper.useGen2Configuration { + let amplifyConfig = try TestConfigHelper.retrieveAmplifyOutputsData( + forResource: GraphQLModelBasedTests.amplifyOutputs) + try Amplify.configure(amplifyConfig) + } else { + let amplifyConfig = try TestConfigHelper.retrieveAmplifyConfiguration( + forResource: GraphQLModelBasedTests.amplifyConfiguration) + try Amplify.configure(amplifyConfig) + } ModelRegistry.register(modelType: Comment.self) ModelRegistry.register(modelType: Post.self) @@ -225,7 +231,7 @@ class GraphQLModelBasedTests: XCTestCase { post: post) let createdCommentResult = try await Amplify.API.mutate(request: .create(comment)) guard case .success(let resultComment) = createdCommentResult else { - XCTFail("Error creating a Comment") + XCTFail("Error creating a Comment \(createdCommentResult)") return } XCTAssertEqual(resultComment.content, "commentContent") diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/README.md b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/README.md index 52b40878d6..ece9d90323 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/README.md +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/README.md @@ -1,4 +1,4 @@ -## Model Based GraphQL +## Schema: AWSAPIPluginFunctionalTests The following steps demonstrate how to set up a GraphQL endpoint with AppSync. The auth configured will be API key. @@ -249,3 +249,113 @@ Keep in mind that the API.swift file in the tests has been manually modified to cp amplifyconfiguration.json ~/.aws-amplify/amplify-ios/testconfiguration/GraphQLModelBasedTests-amplifyconfiguration.json ``` You can now run the tests! + + +## Schema: AWSAPIPluginGen2FunctionalTests + +The following steps demonstrate how to set up an GraphQL endpoint with AppSync using Amplify CLI (Gen2). The auth configured will be API Key. + +### Set-up + +At the time this was written, it follows the steps from here https://docs.amplify.aws/gen2/deploy-and-host/fullstack-branching/mono-and-multi-repos/ + +1. From a new folder, run `npm create amplify@beta`. This uses the following versions of the Amplify CLI, see `package.json` file below. + +```json +{ + ... + "devDependencies": { + "@aws-amplify/backend": "^0.13.0-beta.14", + "@aws-amplify/backend-cli": "^0.12.0-beta.16", + "aws-cdk": "^2.134.0", + "aws-cdk-lib": "^2.134.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "tsx": "^4.7.1", + "typescript": "^5.4.3" + }, + "dependencies": { + "aws-amplify": "^6.0.25" + } +} + +``` +2. Update `amplify/data/resource.ts` to allow `public` access. This allows using API Key as the auth type to perform CRUD operations against the Comment and Post models. The resulting file should look like this + +```ts +const schema = a.schema({ + Post: a + .model({ + title: a.string().required(), + content: a.string().required(), + draft: a.boolean(), + rating: a.float(), + status: a.enum(["PRIVATE", "DRAFT", "PUBLISHED"]), + comments: a.hasMany('Comment') + }) + .authorization([a.allow.public()]), + Comment: a + .model({ + content: a.string().required(), + post: a.belongsTo('Post'), + }) + .authorization([a.allow.public()]), +}); +``` + +3. (Optional) Update the API Key expiry to the maximum. This should be done if this backend is used for CI testing. + +``` +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: 'apiKey', + // API Key is used for a.allow.public() rules + apiKeyAuthorizationMode: { + expiresInDays: 365, + }, + }, +}); +``` + +4. Deploy the backend with npx amplify sandbox + +For example, this deploys to a sandbox env and generates the amplify_outputs.json file. + +``` +npx amplify sandbox --config-out-dir ./config --config-version 1 --profile [PROFILE] +``` + +5. Copy the `amplify_outputs.json` file over to the test directory as `GraphQLModelBasedTests-amplify_outputs.json`. The tests will automatically pick this file up. Create the directories in this path first if it currently doesn't exist. + +``` +cp amplify_outputs.json ~/.aws-amplify/amplify-ios/testconfiguration/GraphQLModelBasedTests-amplify_outputs.json +``` + +6. (Optional) The code generated model files are already checked into the tests so you will only have to re-generate them if you are expecting modifications to them and replace the existing ones checked in. + +``` +npx amplify generate graphql-client-code --format=modelgen --model-target=swift --branch main --app-id [APP_ID] --profile [AWS_PROFILE] +``` + +### Deploying from a branch (Optional) + +If you want to be able utilize Git commits for deployments + +1. Commit and push the files to a git repository. + +2. Navigate to the AWS Amplify console (https://us-east-1.console.aws.amazon.com/amplify/home?region=us-east-1#/) + +3. Click on "Try Amplify Gen 2" button. + +4. Choose "Option 2: Start with an existing app", and choose Github, and press Next. + +5. Find the repository and branch, and click Next + +6. Click "Save and deploy" and wait for deployment to finish. + +7. Generate the `amplify_outputs.json` configuration file + +``` +npx amplify generate config --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 +``` diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ConfigureTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ConfigureTests.swift index 3609fe61f2..8d16c47b05 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ConfigureTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ConfigureTests.swift @@ -7,7 +7,7 @@ import XCTest -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AWSAPIPlugin class AWSAPICategoryPluginConfigureTests: AWSAPICategoryPluginTestBase { @@ -41,10 +41,46 @@ class AWSAPICategoryPluginConfigureTests: AWSAPICategoryPluginTestBase { func testConfigureFailureForNilConfiguration() throws { let plugin = AWSAPIPlugin() - do { - try plugin.configure(using: nil) - XCTFail("Api configuration should not succeed") - } catch { + XCTAssertThrowsError(try plugin.configure(using: nil)) { error in + guard let apiError = error as? PluginError, + case .pluginConfigurationError = apiError else { + XCTFail("Should throw invalidConfiguration exception. But received \(error) ") + return + } + } + } + + /// Configure with data category and assert expected endpoint configured. + func testConfigureAmplifyOutputs() throws { + let config = AmplifyOutputsData(data: .init( + awsRegion: "us-east-1", + url: "http://www.example.com", + modelIntrospection: nil, + apiKey: "apiKey123", + defaultAuthorizationType: .amazonCognitoUserPools, + authorizationTypes: [.apiKey, .awsIAM])) + + let plugin = AWSAPIPlugin() + try plugin.configure(using: config) + guard let endpoint = plugin.pluginConfig.endpoints.first else { + XCTFail("Missing endpoint configuration") + return + } + XCTAssertEqual(endpoint.key, AWSAPIPlugin.defaultGraphQLAPI) + XCTAssertEqual(endpoint.value.name, AWSAPIPlugin.defaultGraphQLAPI) + XCTAssertEqual(endpoint.value.endpointType, .graphQL) + XCTAssertEqual(endpoint.value.apiKey, "apiKey123") + XCTAssertEqual(endpoint.value.baseURL, URL(string: "http://www.example.com")) + XCTAssertEqual(endpoint.value.region, "us-east-1") + XCTAssertEqual(endpoint.value.authorizationType, .amazonCognitoUserPools) + } + + /// Configure with missing data category and throws plugin configuration error. + func testConfigureAmplifyOutputs_DataCategoryMissing() throws { + let config = AmplifyOutputsData(data: nil) + + let plugin = AWSAPIPlugin() + XCTAssertThrowsError(try plugin.configure(using: config)) { error in guard let apiError = error as? PluginError, case .pluginConfigurationError = apiError else { XCTFail("Should throw invalidConfiguration exception. But received \(error) ") @@ -53,4 +89,23 @@ class AWSAPICategoryPluginConfigureTests: AWSAPICategoryPluginTestBase { } } + /// Configuring `.apiKey` auth without the `apiKey` value will fail. + func testConfigureAmplifyOutputs_APIKeyMissing() throws { + let config = AmplifyOutputsData(data: .init( + awsRegion: "us-east-1", + url: "http://www.example.com", + modelIntrospection: nil, + apiKey: nil, + defaultAuthorizationType: .apiKey, + authorizationTypes: [])) + + let plugin = AWSAPIPlugin() + XCTAssertThrowsError(try plugin.configure(using: config)) { error in + guard let apiError = error as? PluginError, + case .pluginConfigurationError = apiError else { + XCTFail("Should throw invalidConfiguration exception. But received \(error) ") + return + } + } + } } diff --git a/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Configure.swift b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Configure.swift index 421c54b7b1..2acad0b80b 100644 --- a/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Configure.swift +++ b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Configure.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import AWSPluginsCore import Foundation @_spi(InternalAWSPinpoint) import InternalAWSPinpoint @@ -20,14 +20,27 @@ extension AWSPinpointAnalyticsPlugin { /// - Throws: /// - PluginError.pluginConfigurationError: If one of the configuration values is invalid or empty public func configure(using configuration: Any?) throws { - guard let config = configuration as? JSONValue else { + let pluginConfiguration: AWSPinpointAnalyticsPluginConfiguration + if let config = configuration as? AmplifyOutputsData { + print(config) + + if let configuredOptions = options { + pluginConfiguration = try AWSPinpointAnalyticsPluginConfiguration(config, options: configuredOptions) + } else { + let defaultOptions = AWSPinpointAnalyticsPlugin.Options.default + options = defaultOptions + pluginConfiguration = try AWSPinpointAnalyticsPluginConfiguration(config, options: defaultOptions) + } + } else if let config = configuration as? JSONValue { + pluginConfiguration = try AWSPinpointAnalyticsPluginConfiguration(config, options) + options = pluginConfiguration.options + } else { throw PluginError.pluginConfigurationError( AnalyticsPluginErrorConstant.decodeConfigurationError.errorDescription, AnalyticsPluginErrorConstant.decodeConfigurationError.recoverySuggestion ) } - let pluginConfiguration = try AWSPinpointAnalyticsPluginConfiguration(config) try configure(using: pluginConfiguration) } @@ -38,8 +51,7 @@ extension AWSPinpointAnalyticsPlugin { region: configuration.region ) - let interval = TimeInterval(configuration.autoFlushEventsInterval) - pinpoint.setAutomaticSubmitEventsInterval(interval) { result in + pinpoint.setAutomaticSubmitEventsInterval(configuration.options.autoFlushEventsInterval) { result in switch result { case .success(let events): Amplify.Hub.dispatchFlushEvents(events.asAnalyticsEventArray()) @@ -48,15 +60,8 @@ extension AWSPinpointAnalyticsPlugin { } } - if configuration.trackAppSessions { - let sessionBackgroundTimeout: TimeInterval - if configuration.autoSessionTrackingInterval == .max { - sessionBackgroundTimeout = .infinity - } else { - sessionBackgroundTimeout = TimeInterval(configuration.autoSessionTrackingInterval) - } - - pinpoint.startTrackingSessions(backgroundTimeout: sessionBackgroundTimeout) + if configuration.options.trackAppSessions { + pinpoint.startTrackingSessions(backgroundTimeout: configuration.autoSessionTrackingInterval) } let networkMonitor = NWPathMonitor() diff --git a/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Options.swift b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Options.swift new file mode 100644 index 0000000000..9849e6e9f5 --- /dev/null +++ b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Options.swift @@ -0,0 +1,36 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +extension AWSPinpointAnalyticsPlugin { + public struct Options { + static let defaultAutoFlushEventsInterval: TimeInterval = 60 + static let defaultTrackAppSession = true + + public let autoFlushEventsInterval: TimeInterval + public let trackAppSessions: Bool + + #if os(macOS) + public init(autoFlushEventsInterval: TimeInterval = 60, + trackAppSessions: Bool = true) { + self.autoFlushEventsInterval = autoFlushEventsInterval + self.trackAppSessions = trackAppSessions + } + #else + public init(autoFlushEventsInterval: TimeInterval = 60, + trackAppSessions: Bool = true) { + self.autoFlushEventsInterval = autoFlushEventsInterval + self.trackAppSessions = trackAppSessions + } + #endif + + public static var `default`: Options { + .init() + } + } +} diff --git a/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Reset.swift b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Reset.swift index 4d29463c05..c6e43f86da 100644 --- a/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Reset.swift +++ b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin+Reset.swift @@ -28,5 +28,9 @@ extension AWSPinpointAnalyticsPlugin { networkMonitor.stopMonitoring() networkMonitor = nil } + + if options != nil { + options = nil + } } } diff --git a/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin.swift b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin.swift index 0bea7a69cd..f99e9496d8 100644 --- a/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin.swift +++ b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/AWSPinpointAnalyticsPlugin.swift @@ -25,13 +25,19 @@ public final class AWSPinpointAnalyticsPlugin: AnalyticsCategoryPlugin { /// An observer to monitor connectivity changes var networkMonitor: NetworkMonitor! + /// Optional passed in `options`, overrides JSON configuration if exists. + var options: Options? + /// The unique key of the plugin within the analytics category public var key: PluginKey { "awsPinpointAnalyticsPlugin" } /// Instantiates an instance of the AWSPinpointAnalyticsPlugin - public init() {} + public init(options: Options? = nil) { + self.options = options + } } extension AWSPinpointAnalyticsPlugin: AmplifyVersionable { } + diff --git a/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/Configuration/AWSPinpointAnalyticsPluginConfiguration.swift b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/Configuration/AWSPinpointAnalyticsPluginConfiguration.swift index 06db245ea7..44456d7c60 100644 --- a/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/Configuration/AWSPinpointAnalyticsPluginConfiguration.swift +++ b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/Configuration/AWSPinpointAnalyticsPluginConfiguration.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import AWSPinpoint import AWSClientRuntime import Foundation @@ -20,11 +20,9 @@ public struct AWSPinpointAnalyticsPluginConfiguration { static let appIdConfigKey = "appId" static let regionConfigKey = "region" - static let defaultAutoFlushEventsInterval = 60 - static let defaultTrackAppSession = true - static let defaultAutoSessionTrackingInterval: Int = { + static let defaultAutoSessionTrackingInterval: TimeInterval = { #if os(macOS) - .max + .infinity #else 5 #endif @@ -32,13 +30,14 @@ public struct AWSPinpointAnalyticsPluginConfiguration { let appId: String let region: String - let autoFlushEventsInterval: Int - let trackAppSessions: Bool - let autoSessionTrackingInterval: Int + + let autoSessionTrackingInterval: TimeInterval + + let options: AWSPinpointAnalyticsPlugin.Options private static let logger = Amplify.Logging.logger(forCategory: CategoryType.analytics.displayName, forNamespace: String(describing: Self.self)) - init(_ configuration: JSONValue) throws { + init(_ configuration: JSONValue, _ options: AWSPinpointAnalyticsPlugin.Options? = nil) throws { guard case let .object(configObject) = configuration else { throw PluginError.pluginConfigurationError( AnalyticsPluginErrorConstant.configurationObjectExpected.errorDescription, @@ -55,8 +54,14 @@ public struct AWSPinpointAnalyticsPluginConfiguration { let pluginConfiguration = try AWSPinpointPluginConfiguration(pinpointAnalyticsConfig) - let autoFlushEventsInterval = try Self.getAutoFlushEventsInterval(configObject) - let trackAppSessions = try Self.getTrackAppSessions(configObject) + let configOptions: AWSPinpointAnalyticsPlugin.Options + if let options { + configOptions = options + } else { + configOptions = .init( + autoFlushEventsInterval: try Self.getAutoFlushEventsInterval(configObject), + trackAppSessions: try Self.getTrackAppSessions(configObject)) + } let autoSessionTrackingInterval = try Self.getAutoSessionTrackingInterval(configObject) // Warn users in case they set different regions between pinpointTargeting and pinpointAnalytics @@ -68,26 +73,45 @@ public struct AWSPinpointAnalyticsPluginConfiguration { self.init(appId: pluginConfiguration.appId, region: pluginConfiguration.region, - autoFlushEventsInterval: autoFlushEventsInterval, - trackAppSessions: trackAppSessions, - autoSessionTrackingInterval: autoSessionTrackingInterval) + autoSessionTrackingInterval: autoSessionTrackingInterval, + options: configOptions) + } + + init(_ configuration: AmplifyOutputsData, + options: AWSPinpointAnalyticsPlugin.Options) throws { + guard let analyticsConfig = configuration.analytics else { + throw PluginError.pluginConfigurationError( + AnalyticsPluginErrorConstant.missingAnalyticsCategoryConfiguration.errorDescription, + AnalyticsPluginErrorConstant.missingAnalyticsCategoryConfiguration.recoverySuggestion + ) + } + + guard let pinpointAnalyticsConfig = analyticsConfig.amazonPinpoint else { + throw PluginError.pluginConfigurationError( + AnalyticsPluginErrorConstant.missingAmazonPinpointConfiguration.errorDescription, + AnalyticsPluginErrorConstant.missingAmazonPinpointConfiguration.recoverySuggestion + ) + } + + self.init(appId: pinpointAnalyticsConfig.appId, + region: pinpointAnalyticsConfig.awsRegion, + autoSessionTrackingInterval: Self.defaultAutoSessionTrackingInterval, + options: options) } init(appId: String, region: String, - autoFlushEventsInterval: Int, - trackAppSessions: Bool, - autoSessionTrackingInterval: Int) { + autoSessionTrackingInterval: TimeInterval, + options: AWSPinpointAnalyticsPlugin.Options) { self.appId = appId self.region = region - self.autoFlushEventsInterval = autoFlushEventsInterval - self.trackAppSessions = trackAppSessions self.autoSessionTrackingInterval = autoSessionTrackingInterval + self.options = options } - private static func getAutoFlushEventsInterval(_ configuration: [String: JSONValue]) throws -> Int { + private static func getAutoFlushEventsInterval(_ configuration: [String: JSONValue]) throws -> TimeInterval { guard let autoFlushEventsInterval = configuration[autoFlushEventsIntervalKey] else { - return Self.defaultAutoFlushEventsInterval + return AWSPinpointAnalyticsPlugin.Options.defaultAutoFlushEventsInterval } guard case let .number(autoFlushEventsIntervalValue) = autoFlushEventsInterval else { @@ -104,12 +128,12 @@ public struct AWSPinpointAnalyticsPluginConfiguration { ) } - return Int(autoFlushEventsIntervalValue) + return TimeInterval(autoFlushEventsIntervalValue) } private static func getTrackAppSessions(_ configuration: [String: JSONValue]) throws -> Bool { guard let trackAppSessions = configuration[trackAppSessionsKey] else { - return Self.defaultTrackAppSession + return AWSPinpointAnalyticsPlugin.Options.defaultTrackAppSession } guard case let .boolean(trackAppSessionsValue) = trackAppSessions else { @@ -122,7 +146,7 @@ public struct AWSPinpointAnalyticsPluginConfiguration { return trackAppSessionsValue } - private static func getAutoSessionTrackingInterval(_ configuration: [String: JSONValue]) throws -> Int { + private static func getAutoSessionTrackingInterval(_ configuration: [String: JSONValue]) throws -> TimeInterval { guard let autoSessionTrackingInterval = configuration[autoSessionTrackingIntervalKey] else { return Self.defaultAutoSessionTrackingInterval } @@ -142,6 +166,6 @@ public struct AWSPinpointAnalyticsPluginConfiguration { ) } - return Int(autoSessionTrackingIntervalValue) + return autoSessionTrackingIntervalValue } } diff --git a/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/Support/Constants/AnalyticsErrorConstants.swift b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/Support/Constants/AnalyticsErrorConstants.swift index 77c06023dd..2ea40f5eb7 100644 --- a/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/Support/Constants/AnalyticsErrorConstants.swift +++ b/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/Support/Constants/AnalyticsErrorConstants.swift @@ -26,6 +26,16 @@ struct AnalyticsPluginErrorConstant { "Add the `PinpointAnalytics` section to the plugin." ) + static let missingAnalyticsCategoryConfiguration: AnalyticsPluginErrorString = ( + "Plugin is missing `Analytics` category in configuration.", + "Add the `Analytics` section to the plugin." + ) + + static let missingAmazonPinpointConfiguration: AnalyticsPluginErrorString = ( + "Plugin is missing `amazon_pinpoint` section under `Analytics` category configuration.", + "Add the `amazon_pinpoint` section to the plugin." + ) + static let invalidAutoFlushEventsInterval: AnalyticsPluginErrorString = ( "AutoFlushEventsInterval is not a number or is less than 0", "Ensure AutoFlushEventsInterval is zero or positive number" diff --git a/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/AWSPinpointAnalyticsPluginConfigureTests.swift b/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/AWSPinpointAnalyticsPluginConfigureTests.swift index 89a8365fa4..4c14742004 100644 --- a/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/AWSPinpointAnalyticsPluginConfigureTests.swift +++ b/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/AWSPinpointAnalyticsPluginConfigureTests.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@testable @_spi(InternalAmplifyConfiguration) import Amplify @testable import AmplifyTestCommon @_spi(InternalAWSPinpoint) @testable import InternalAWSPinpoint @testable import AWSPinpointAnalyticsPlugin @@ -30,9 +30,9 @@ class AWSPinpointAnalyticsPluginConfigureTests: AWSPinpointAnalyticsPluginTestBa func testConfigureSuccess() { let appId = JSONValue(stringLiteral: testAppId) let region = JSONValue(stringLiteral: testRegion) - let autoFlushInterval = JSONValue(integerLiteral: testAutoFlushInterval) + let autoFlushInterval = JSONValue(integerLiteral: Int(testAutoFlushInterval)) let trackAppSession = JSONValue(booleanLiteral: false) - let autoSessionTrackingInterval = JSONValue(integerLiteral: testAutoSessionTrackingInterval) + let autoSessionTrackingInterval = JSONValue(integerLiteral: Int(testAutoSessionTrackingInterval)) let pinpointAnalyticsPluginConfiguration = JSONValue( dictionaryLiteral: @@ -59,6 +59,50 @@ class AWSPinpointAnalyticsPluginConfigureTests: AWSPinpointAnalyticsPluginTestBa XCTAssertNotNil(analyticsPlugin.pinpoint) XCTAssertNotNil(analyticsPlugin.globalProperties) XCTAssertNotNil(analyticsPlugin.isEnabled) + XCTAssertEqual(analyticsPlugin.options?.autoFlushEventsInterval, testAutoFlushInterval) + XCTAssertEqual(analyticsPlugin.options?.trackAppSessions, false) + } catch { + XCTFail("Failed to configure analytics plugin") + } + } + + func testConfigure_OptionsOverride() { + let appId = JSONValue(stringLiteral: testAppId) + let region = JSONValue(stringLiteral: testRegion) + let autoFlushInterval = JSONValue(integerLiteral: 30) + let trackAppSession = JSONValue(booleanLiteral: false) + let autoSessionTrackingInterval = JSONValue(integerLiteral: 40) + + let pinpointAnalyticsPluginConfiguration = JSONValue( + dictionaryLiteral: + (AWSPinpointAnalyticsPluginConfiguration.appIdConfigKey, appId), + (AWSPinpointAnalyticsPluginConfiguration.regionConfigKey, region) + ) + + let regionConfiguration = JSONValue(dictionaryLiteral: + (AWSPinpointAnalyticsPluginConfiguration.regionConfigKey, region)) + + let analyticsPluginConfig = JSONValue( + dictionaryLiteral: + (AWSPinpointAnalyticsPluginConfiguration.pinpointAnalyticsConfigKey, pinpointAnalyticsPluginConfiguration), + (AWSPinpointAnalyticsPluginConfiguration.pinpointTargetingConfigKey, regionConfiguration), + (AWSPinpointAnalyticsPluginConfiguration.autoFlushEventsIntervalKey, autoFlushInterval), + (AWSPinpointAnalyticsPluginConfiguration.trackAppSessionsKey, trackAppSession), + (AWSPinpointAnalyticsPluginConfiguration.autoSessionTrackingIntervalKey, autoSessionTrackingInterval) + ) + + do { + let analyticsPlugin = AWSPinpointAnalyticsPlugin( + options: .init( + autoFlushEventsInterval: 50, + trackAppSessions: true)) + try analyticsPlugin.configure(using: analyticsPluginConfig) + + XCTAssertNotNil(analyticsPlugin.pinpoint) + XCTAssertNotNil(analyticsPlugin.globalProperties) + XCTAssertNotNil(analyticsPlugin.isEnabled) + XCTAssertEqual(analyticsPlugin.options?.autoFlushEventsInterval, 50) + XCTAssertEqual(analyticsPlugin.options?.trackAppSessions, true) } catch { XCTFail("Failed to configure analytics plugin") } @@ -77,4 +121,53 @@ class AWSPinpointAnalyticsPluginConfigureTests: AWSPinpointAnalyticsPluginTestBa } } } + + // MARK: - AmplifyOutputsData Configuration tests + + func testConfigure_WithAmplifyOutputs() { + let config = AmplifyOutputsData.init(analytics: .init( + amazonPinpoint: .init(awsRegion: testRegion, + appId: testAppId))) + + do { + let analyticsPlugin = AWSPinpointAnalyticsPlugin() + try analyticsPlugin.configure(using: config) + + XCTAssertNotNil(analyticsPlugin.pinpoint) + XCTAssertNotNil(analyticsPlugin.globalProperties) + XCTAssertNotNil(analyticsPlugin.isEnabled) + + // Verify default options when none are passed in with the plugin's instantiation + XCTAssertEqual(analyticsPlugin.options?.autoFlushEventsInterval, AWSPinpointAnalyticsPlugin.Options.defaultAutoFlushEventsInterval) + XCTAssertEqual(analyticsPlugin.options?.trackAppSessions, AWSPinpointAnalyticsPlugin.Options.defaultTrackAppSession) + + } catch { + XCTFail("Failed to configure analytics plugin") + } + } + + func testConfigure_WithAmplifyOutputsAndOptions() { + let config = AmplifyOutputsData.init(analytics: .init( + amazonPinpoint: .init(awsRegion: testRegion, + appId: testAppId))) + + do { + let analyticsPlugin = AWSPinpointAnalyticsPlugin(options: .init( + autoFlushEventsInterval: 100, + trackAppSessions: false)) + try analyticsPlugin.configure(using: config) + + XCTAssertNotNil(analyticsPlugin.pinpoint) + XCTAssertNotNil(analyticsPlugin.globalProperties) + XCTAssertNotNil(analyticsPlugin.isEnabled) + + // Verify options override when passed in with the plugin's instantiation + XCTAssertEqual(analyticsPlugin.options?.autoFlushEventsInterval, 100) + XCTAssertEqual(analyticsPlugin.options?.trackAppSessions, false) + + } catch { + XCTFail("Failed to configure analytics plugin") + } + } + } diff --git a/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/AWSPinpointAnalyticsPluginTestBase.swift b/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/AWSPinpointAnalyticsPluginTestBase.swift index cfcc0047d6..6e637db34a 100644 --- a/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/AWSPinpointAnalyticsPluginTestBase.swift +++ b/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/AWSPinpointAnalyticsPluginTestBase.swift @@ -18,9 +18,9 @@ class AWSPinpointAnalyticsPluginTestBase: XCTestCase { let testAppId = "56e6f06fd4f244c6b202bc1234567890" let testRegion = "us-east-1" - let testAutoFlushInterval = 30 + let testAutoFlushInterval: TimeInterval = 30 let testTrackAppSession = true - let testAutoSessionTrackingInterval = 10 + let testAutoSessionTrackingInterval: TimeInterval = 10 var plugin: HubCategoryPlugin { guard let plugin = try? Amplify.Hub.getPlugin(for: "awsHubPlugin"), diff --git a/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/Configuration/AWSPinpointAnalyticsPluginAmplifyOutputsConfigurationTests.swift b/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/Configuration/AWSPinpointAnalyticsPluginAmplifyOutputsConfigurationTests.swift new file mode 100644 index 0000000000..e4aeffa943 --- /dev/null +++ b/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/Configuration/AWSPinpointAnalyticsPluginAmplifyOutputsConfigurationTests.swift @@ -0,0 +1,87 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable @_spi(InternalAmplifyConfiguration) import Amplify +import XCTest +@_spi(InternalAWSPinpoint) @testable import InternalAWSPinpoint +@testable import AWSPinpointAnalyticsPlugin + +// swiftlint:disable:next type_name +class AWSPinpointAnalyticsPluginAmplifyOutputsConfigurationTests: XCTestCase { + let testAppId = "testAppId" + let appId = "testAppId" + let testRegion = "us-east-1" + let region: JSONValue = "us-east-1" + let testAutoFlushInterval: UInt = 300 + let autoFlushInterval: JSONValue = 300 + let testTrackAppSession = false + let trackAppSession: JSONValue = false + let testAutoSessionTrackingInterval: UInt = 100 + let autoSessionTrackingInterval: JSONValue = 100 + let pinpointAnalyticsPluginConfiguration = JSONValue( + dictionaryLiteral: + (AWSPinpointAnalyticsPluginConfiguration.appIdConfigKey, "testAppId"), + (AWSPinpointAnalyticsPluginConfiguration.regionConfigKey, "us-east-1") + ) + + func testConfiguration_Success() throws { + let config = AmplifyOutputsData(analytics: .init(amazonPinpoint: .init(awsRegion: testRegion, appId: appId))) + let result = try AWSPinpointAnalyticsPluginConfiguration(config, options: .init()) + XCTAssertNotNil(result) + XCTAssertEqual(result.appId, testAppId) + XCTAssertEqual(result.region, testRegion) + XCTAssertEqual(result.options.autoFlushEventsInterval, + AWSPinpointAnalyticsPlugin.Options.defaultAutoFlushEventsInterval) + XCTAssertEqual(result.options.trackAppSessions, + AWSPinpointAnalyticsPlugin.Options.defaultTrackAppSession) + XCTAssertEqual(result.autoSessionTrackingInterval, + AWSPinpointAnalyticsPluginConfiguration.defaultAutoSessionTrackingInterval) + } + + func testConfiguration_OptionsOverride() throws { + let config = AmplifyOutputsData(analytics: .init(amazonPinpoint: .init(awsRegion: testRegion, appId: appId))) + let result = try AWSPinpointAnalyticsPluginConfiguration( + config, + options: .init(autoFlushEventsInterval: 100, + trackAppSessions: false)) + XCTAssertNotNil(result) + XCTAssertEqual(result.appId, testAppId) + XCTAssertEqual(result.region, testRegion) + XCTAssertEqual(result.options.autoFlushEventsInterval, 100) + XCTAssertFalse(result.options.trackAppSessions) + XCTAssertEqual(result.autoSessionTrackingInterval, AWSPinpointAnalyticsPluginConfiguration.defaultAutoSessionTrackingInterval) + } + + func testConfiguration_throwsMissingAnalytics() { + let config = AmplifyOutputsData(analytics: nil) + XCTAssertThrowsError( + try AWSPinpointAnalyticsPluginConfiguration(config, options: .init()) + ) { error in + guard case let PluginError.pluginConfigurationError(errorDescription, _, _) = error else { + XCTFail("Expected to catch PluginError.pluginConfigurationError.") + return + } + XCTAssertEqual(errorDescription, + AnalyticsPluginErrorConstant.missingAnalyticsCategoryConfiguration.errorDescription) + } + } + + func testConfiguration_throwAmazonPinpoint() { + let config = AmplifyOutputsData(analytics: .init(amazonPinpoint: nil)) + XCTAssertThrowsError( + try AWSPinpointAnalyticsPluginConfiguration(config, options: .init()) + ) { error in + guard case let PluginError.pluginConfigurationError(errorDescription, _, _) = error else { + XCTFail("Expected to catch PluginError.pluginConfigurationError.") + return + } + XCTAssertEqual(errorDescription, + AnalyticsPluginErrorConstant.missingAmazonPinpointConfiguration.errorDescription) + } + } +} + diff --git a/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/Configuration/AWSPinpointAnalyticsPluginConfigurationTests.swift b/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/Configuration/AWSPinpointAnalyticsPluginConfigurationTests.swift index 5c10cb82d3..f7b8f7216f 100644 --- a/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/Configuration/AWSPinpointAnalyticsPluginConfigurationTests.swift +++ b/AmplifyPlugins/Analytics/Tests/AWSPinpointAnalyticsPluginUnitTests/Configuration/AWSPinpointAnalyticsPluginConfigurationTests.swift @@ -16,11 +16,11 @@ class AWSPinpointAnalyticsPluginConfigurationTests: XCTestCase { let appId: JSONValue = "testAppId" let testRegion = "us-east-1" let region: JSONValue = "us-east-1" - let testAutoFlushInterval = 300 + let testAutoFlushInterval: TimeInterval = 300 let autoFlushInterval: JSONValue = 300 let testTrackAppSession = false let trackAppSession: JSONValue = false - let testAutoSessionTrackingInterval = 100 + let testAutoSessionTrackingInterval: TimeInterval = 100 let autoSessionTrackingInterval: JSONValue = 100 let pinpointAnalyticsPluginConfiguration = JSONValue( dictionaryLiteral: @@ -42,10 +42,10 @@ class AWSPinpointAnalyticsPluginConfigurationTests: XCTestCase { XCTAssertNotNil(config) XCTAssertEqual(config.appId, testAppId) XCTAssertEqual(config.region, testRegion) - XCTAssertEqual(config.autoFlushEventsInterval, - AWSPinpointAnalyticsPluginConfiguration.defaultAutoFlushEventsInterval) - XCTAssertEqual(config.trackAppSessions, - AWSPinpointAnalyticsPluginConfiguration.defaultTrackAppSession) + XCTAssertEqual(config.options.autoFlushEventsInterval, + AWSPinpointAnalyticsPlugin.Options.defaultAutoFlushEventsInterval) + XCTAssertEqual(config.options.trackAppSessions, + AWSPinpointAnalyticsPlugin.Options.defaultTrackAppSession) XCTAssertEqual(config.autoSessionTrackingInterval, AWSPinpointAnalyticsPluginConfiguration.defaultAutoSessionTrackingInterval) } catch { @@ -64,10 +64,10 @@ class AWSPinpointAnalyticsPluginConfigurationTests: XCTestCase { XCTAssertNotNil(config) XCTAssertEqual(config.appId, testAppId) XCTAssertEqual(config.region, testRegion) - XCTAssertEqual(config.autoFlushEventsInterval, - AWSPinpointAnalyticsPluginConfiguration.defaultAutoFlushEventsInterval) - XCTAssertEqual(config.trackAppSessions, - AWSPinpointAnalyticsPluginConfiguration.defaultTrackAppSession) + XCTAssertEqual(config.options.autoFlushEventsInterval, + AWSPinpointAnalyticsPlugin.Options.defaultAutoFlushEventsInterval) + XCTAssertEqual(config.options.trackAppSessions, + AWSPinpointAnalyticsPlugin.Options.defaultTrackAppSession) XCTAssertEqual(config.autoSessionTrackingInterval, AWSPinpointAnalyticsPluginConfiguration.defaultAutoSessionTrackingInterval) } catch { @@ -87,9 +87,9 @@ class AWSPinpointAnalyticsPluginConfigurationTests: XCTestCase { XCTAssertNotNil(config) XCTAssertEqual(config.appId, testAppId) XCTAssertEqual(config.region, testRegion) - XCTAssertEqual(config.autoFlushEventsInterval, testAutoFlushInterval) - XCTAssertEqual(config.trackAppSessions, - AWSPinpointAnalyticsPluginConfiguration.defaultTrackAppSession) + XCTAssertEqual(config.options.autoFlushEventsInterval, testAutoFlushInterval) + XCTAssertEqual(config.options.trackAppSessions, + AWSPinpointAnalyticsPlugin.Options.defaultTrackAppSession) XCTAssertEqual(config.autoSessionTrackingInterval, AWSPinpointAnalyticsPluginConfiguration.defaultAutoSessionTrackingInterval) } catch { @@ -127,9 +127,9 @@ class AWSPinpointAnalyticsPluginConfigurationTests: XCTestCase { XCTAssertNotNil(config) XCTAssertEqual(config.appId, testAppId) XCTAssertEqual(config.region, testRegion) - XCTAssertEqual(config.autoFlushEventsInterval, - AWSPinpointAnalyticsPluginConfiguration.defaultAutoFlushEventsInterval) - XCTAssertEqual(config.trackAppSessions, testTrackAppSession) + XCTAssertEqual(config.options.autoFlushEventsInterval, + AWSPinpointAnalyticsPlugin.Options.defaultAutoFlushEventsInterval) + XCTAssertEqual(config.options.trackAppSessions, testTrackAppSession) XCTAssertEqual(config.autoSessionTrackingInterval, AWSPinpointAnalyticsPluginConfiguration.defaultAutoSessionTrackingInterval) } catch { @@ -149,9 +149,9 @@ class AWSPinpointAnalyticsPluginConfigurationTests: XCTestCase { XCTAssertNotNil(config) XCTAssertEqual(config.appId, testAppId) XCTAssertEqual(config.region, testRegion) - XCTAssertEqual(config.autoFlushEventsInterval, - AWSPinpointAnalyticsPluginConfiguration.defaultAutoFlushEventsInterval) - XCTAssertEqual(config.trackAppSessions, AWSPinpointAnalyticsPluginConfiguration.defaultTrackAppSession) + XCTAssertEqual(config.options.autoFlushEventsInterval, + AWSPinpointAnalyticsPlugin.Options.defaultAutoFlushEventsInterval) + XCTAssertEqual(config.options.trackAppSessions, AWSPinpointAnalyticsPlugin.Options.defaultTrackAppSession) XCTAssertEqual(config.autoSessionTrackingInterval, testAutoSessionTrackingInterval) } catch { XCTFail("Failed to instantiate analytics plugin configuration") diff --git a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/AWSPinpointAnalyticsPluginGen2IntegrationTests.xctestplan b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/AWSPinpointAnalyticsPluginGen2IntegrationTests.xctestplan new file mode 100644 index 0000000000..b263081400 --- /dev/null +++ b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/AWSPinpointAnalyticsPluginGen2IntegrationTests.xctestplan @@ -0,0 +1,28 @@ +{ + "configurations" : [ + { + "id" : "78DC5EA3-B302-4726-8FCA-A9EC59103B63", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "commandLineArgumentEntries" : [ + { + "argument" : "GEN2" + } + ] + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:AnalyticsHostApp.xcodeproj", + "identifier" : "211035142BD6AB30006AC186", + "name" : "AWSPinpointAnalyticsPluginGen2IntegrationTests" + } + } + ], + "version" : 1 +} diff --git a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/AWSPinpointAnalyticsPluginIntegrationTests.swift b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/AWSPinpointAnalyticsPluginIntegrationTests.swift index acf152fdbc..3716620822 100644 --- a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/AWSPinpointAnalyticsPluginIntegrationTests.swift +++ b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/AWSPinpointAnalyticsPluginIntegrationTests.swift @@ -18,14 +18,25 @@ import Network class AWSPinpointAnalyticsPluginIntergrationTests: XCTestCase { static let amplifyConfiguration = "testconfiguration/AWSPinpointAnalyticsPluginIntegrationTests-amplifyconfiguration" + static let amplifyOutputs = "testconfiguration/AWSPinpointAnalyticsPluginIntegrationTests-amplify_outputs" static let analyticsPluginKey = "awsPinpointAnalyticsPlugin" + var useGen2Configuration: Bool { + ProcessInfo.processInfo.arguments.contains("GEN2") + } + override func setUp() { do { - let config = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: Self.amplifyConfiguration) try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: AWSPinpointAnalyticsPlugin()) - try Amplify.configure(config) + + if useGen2Configuration { + let data = try TestConfigHelper.retrieve(forResource: Self.amplifyOutputs) + try Amplify.configure(with: .data(data)) + } else { + let config = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: Self.amplifyConfiguration) + try Amplify.configure(config) + } } catch { XCTFail("Failed to initialize and configure Amplify \(error)") } diff --git a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/README.md b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/README.md index c1f230a9f8..eb10747a96 100644 --- a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/README.md +++ b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/README.md @@ -1,5 +1,7 @@ -## Analytics Integration Tests +# Analytics Integration Tests +## Schema: AWSPinpointAnalyticsPluginIntegrationTests + The following steps demonstrate how to set up Analytics. Auth category is also required for signing with AWS Pinpoint service and requesting with IAM credentials to allow unauthenticated and authenticated access. ### Set-up @@ -25,3 +27,159 @@ cp amplifyconfiguration.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSPin 5. You can now run all of the integration tests. 6. You can run `amplify console analytics` to check what happens at the backend. + +## Schema: AWSPinpointAnalyticsPluginGen2IntegrationTests + +The following steps demonstrate how to set up Pinpoint and Auth using Amplify CLI Gen2. + +### Set-up + +At the time this was written, it follows the steps from here https://docs.amplify.aws/gen2/deploy-and-host/fullstack-branching/mono-and-multi-repos/ + +1. From a new folder, run `npm create amplify@beta`. This uses the following versions of the Amplify CLI, see `package.json` file below. + +```json +{ + ... + "devDependencies": { + "@aws-amplify/backend": "^0.13.0-beta.14", + "@aws-amplify/backend-cli": "^0.12.0-beta.16", + "aws-cdk": "^2.134.0", + "aws-cdk-lib": "^2.134.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "tsx": "^4.7.1", + "typescript": "^5.4.3" + }, + "dependencies": { + "aws-amplify": "^6.0.25" + } +} + +``` +2. Update `amplify/auth/resource.ts`. The resulting file should look like this + +```ts +import { defineAuth, defineFunction } from '@aws-amplify/backend'; + +/** + * Define and configure your auth resource + * @see https://docs.amplify.aws/gen2/build-a-backend/auth + */ +export const auth = defineAuth({ + loginWith: { + email: true + }, + triggers: { + // configure a trigger to point to a function definition + preSignUp: defineFunction({ + entry: './pre-sign-up-handler.ts' + }) + } +}); + +``` + +```ts +import type { PreSignUpTriggerHandler } from 'aws-lambda'; + +export const handler: PreSignUpTriggerHandler = async (event) => { + // your code here + event.response.autoConfirmUser = true + return event; +}; +``` + +3. Update `amplify/backend.ts` to create the analytics stack (https://docs.amplify.aws/gen2/build-a-backend/add-aws-services/analytics/) + +Add the following imports + +```ts +import { Policy, PolicyStatement } from "aws-cdk-lib/aws-iam"; +import { CfnApp } from "aws-cdk-lib/aws-pinpoint"; +import { Stack } from 'aws-cdk-lib'; +``` + +Create `backend` const + +```ts +const backend = defineBackend({ + auth, + // data, + // storage + // additional resource +}); +``` + +Add the remaining code + +```ts +const analyticsStack = backend.createStack("analytics-stack"); + +// create a Pinpoint app +const pinpoint = new CfnApp(analyticsStack, "Pinpoint", { + name: "myPinpointApp", +}); + +// create an IAM policy to allow interacting with Pinpoint +const pinpointPolicy = new Policy(analyticsStack, "PinpointPolicy", { + policyName: "PinpointPolicy", + statements: [ + new PolicyStatement({ + actions: ["mobiletargeting:UpdateEndpoint", "mobiletargeting:PutEvents"], + resources: [pinpoint.attrArn + "/*"], + }), + ], +}); + +// apply the policy to the authenticated and unauthenticated roles +backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(pinpointPolicy); +backend.auth.resources.unauthenticatedUserIamRole.attachInlinePolicy(pinpointPolicy); + +// patch the custom Pinpoint resource to the expected output configuration +backend.addOutput({ + analytics: { + amazon_pinpoint: { + app_id: pinpoint.ref, + aws_region: Stack.of(pinpoint).region, + }, + }, +}); +``` + +4. Deploy the backend with npx amplify sandbox + +For example, this deploys to a sandbox env and generates the amplify_outputs.json file. + +``` +npx amplify sandbox --config-out-dir ./config --config-version 1 --profile [PROFILE] +``` + +5. Copy the `amplify_outputs.json` file over to the test directory as `AWSPinpointAnalyticsPluginIntegrationTests-amplify_outputs.json`. The tests will automatically pick this file up. Create the directories in this path first if it currently doesn't exist. + +``` +cp amplify_outputs.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSPinpointAnalyticsPluginIntegrationTests-amplify_outputs.json +``` + +### Deploying from a branch (Optional) + +If you want to be able utilize Git commits for deployments + +1. Commit and push the files to a git repository. + +2. Navigate to the AWS Amplify console (https://us-east-1.console.aws.amazon.com/amplify/home?region=us-east-1#/) + +3. Click on "Try Amplify Gen 2" button. + +4. Choose "Option 2: Start with an existing app", and choose Github, and press Next. + +5. Find the repository and branch, and click Next + +6. Click "Save and deploy" and wait for deployment to finish. + +7. Generate the `amplify_outputs.json` configuration file + +``` +npx amplify generate config --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 +``` + diff --git a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/project.pbxproj index 28338f40ec..ea8650abb3 100644 --- a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/project.pbxproj @@ -7,6 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 211035182BD6AB30006AC186 /* AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9737697029519DEC0074B63A /* AsyncTesting.swift */; }; + 211035192BD6AB30006AC186 /* AsyncExpectation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9737697129519DEC0074B63A /* AsyncExpectation.swift */; }; + 2110351A2BD6AB30006AC186 /* XCTestCase+AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9737697229519DEC0074B63A /* XCTestCase+AsyncTesting.swift */; }; + 2110351B2BD6AB30006AC186 /* AWSPinpointAnalyticsPluginIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6857647828AE95ED000CE2E9 /* AWSPinpointAnalyticsPluginIntegrationTests.swift */; }; + 2110351C2BD6AB30006AC186 /* TestConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6857648828AE9951000CE2E9 /* TestConfigHelper.swift */; }; 5C2E096829551E3100673FF9 /* Amplify in Frameworks */ = {isa = PBXBuildFile; productRef = 5C2E096729551E3100673FF9 /* Amplify */; }; 5C2E096A29551E3F00673FF9 /* AWSPinpointAnalyticsPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 5C2E096929551E3F00673FF9 /* AWSPinpointAnalyticsPlugin */; }; 5C2E096C2955210C00673FF9 /* AWSPluginsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 5C2E096B2955210C00673FF9 /* AWSPluginsCore */; }; @@ -39,6 +44,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 211035162BD6AB30006AC186 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6857645428AE94D9000CE2E9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6857645B28AE94D9000CE2E9; + remoteInfo = AnalyticsHostApp; + }; 6857647A28AE95ED000CE2E9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6857645428AE94D9000CE2E9 /* Project object */; @@ -63,6 +75,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 211035232BD6AB30006AC186 /* AWSPinpointAnalyticsPluginGen2IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSPinpointAnalyticsPluginGen2IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 211035242BD6AB98006AC186 /* AWSPinpointAnalyticsPluginGen2IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AWSPinpointAnalyticsPluginGen2IntegrationTests.xctestplan; sourceTree = ""; }; + 212371552BBC5414003B1B44 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 5C2E096629551CDD00673FF9 /* amplify-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "amplify-swift"; path = ../../../..; sourceTree = ""; }; 6857645C28AE94D9000CE2E9 /* AnalyticsHostApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AnalyticsHostApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6857645F28AE94D9000CE2E9 /* AnalyticsHostAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsHostAppApp.swift; sourceTree = ""; }; @@ -81,6 +96,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 2110351D2BD6AB30006AC186 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6857645928AE94D9000CE2E9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -148,6 +170,7 @@ 97914D2F29564227002000EA /* AnalyticsStressTests.xctest */, 68DBE9362A3B69CE002B73E3 /* AnalyticsWatchApp.app */, 68DBE9622A3B6EAE002B73E3 /* AWSPinpointAnalyticsPluginIntegrationTestsWatch.xctest */, + 211035232BD6AB30006AC186 /* AWSPinpointAnalyticsPluginGen2IntegrationTests.xctest */, ); name = Products; sourceTree = ""; @@ -165,6 +188,8 @@ 6857647728AE95ED000CE2E9 /* AWSPinpointAnalyticsPluginIntegrationTests */ = { isa = PBXGroup; children = ( + 211035242BD6AB98006AC186 /* AWSPinpointAnalyticsPluginGen2IntegrationTests.xctestplan */, + 212371552BBC5414003B1B44 /* README.md */, 6857647828AE95ED000CE2E9 /* AWSPinpointAnalyticsPluginIntegrationTests.swift */, 6857648828AE9951000CE2E9 /* TestConfigHelper.swift */, ); @@ -209,6 +234,25 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 211035142BD6AB30006AC186 /* AWSPinpointAnalyticsPluginGen2IntegrationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 211035202BD6AB30006AC186 /* Build configuration list for PBXNativeTarget "AWSPinpointAnalyticsPluginGen2IntegrationTests" */; + buildPhases = ( + 211035172BD6AB30006AC186 /* Sources */, + 2110351D2BD6AB30006AC186 /* Frameworks */, + 2110351E2BD6AB30006AC186 /* Resources */, + 2110351F2BD6AB30006AC186 /* Copy Configuration folder */, + ); + buildRules = ( + ); + dependencies = ( + 211035152BD6AB30006AC186 /* PBXTargetDependency */, + ); + name = AWSPinpointAnalyticsPluginGen2IntegrationTests; + productName = AWSPinpointAnalyticsPluginIntegrationTests; + productReference = 211035232BD6AB30006AC186 /* AWSPinpointAnalyticsPluginGen2IntegrationTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 6857645B28AE94D9000CE2E9 /* AnalyticsHostApp */ = { isa = PBXNativeTarget; buildConfigurationList = 6857646A28AE94DA000CE2E9 /* Build configuration list for PBXNativeTarget "AnalyticsHostApp" */; @@ -358,11 +402,19 @@ 97914D2029564227002000EA /* AnalyticsStressTests */, 68DBE9352A3B69CE002B73E3 /* AnalyticsWatchApp */, 68DBE9532A3B6EAE002B73E3 /* AWSPinpointAnalyticsPluginIntegrationTestsWatch */, + 211035142BD6AB30006AC186 /* AWSPinpointAnalyticsPluginGen2IntegrationTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 2110351E2BD6AB30006AC186 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6857645A28AE94D9000CE2E9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -402,6 +454,24 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2110351F2BD6AB30006AC186 /* Copy Configuration folder */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy Configuration folder"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "TEMP_FILE=$HOME/.aws-amplify/amplify-ios/testconfiguration/.\nDEST_PATH=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/testconfiguration/\"\n\nif [[ ! -d $TEMP_FILE ]] ; then\n echo \"${TEMP_FILE} does not exist. Using empty configuration.\"\n exit 0\nfi\n \nif [[ -f $DEST_PATH ]] ; then\n rm $DEST_PATH\nfi\n \ncp -r $TEMP_FILE $DEST_PATH\n"; + }; 6857647F28AE9615000CE2E9 /* Copy Configuration folder */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -459,6 +529,18 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 211035172BD6AB30006AC186 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 211035182BD6AB30006AC186 /* AsyncTesting.swift in Sources */, + 211035192BD6AB30006AC186 /* AsyncExpectation.swift in Sources */, + 2110351A2BD6AB30006AC186 /* XCTestCase+AsyncTesting.swift in Sources */, + 2110351B2BD6AB30006AC186 /* AWSPinpointAnalyticsPluginIntegrationTests.swift in Sources */, + 2110351C2BD6AB30006AC186 /* TestConfigHelper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6857645828AE94D9000CE2E9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -516,6 +598,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 211035152BD6AB30006AC186 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6857645B28AE94D9000CE2E9 /* AnalyticsHostApp */; + targetProxy = 211035162BD6AB30006AC186 /* PBXContainerItemProxy */; + }; 6857647B28AE95ED000CE2E9 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6857645B28AE94D9000CE2E9 /* AnalyticsHostApp */; @@ -534,6 +621,48 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 211035212BD6AB30006AC186 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W3DRXD72QU; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.analytics.ASPinpointAnalyticsPluginIntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AnalyticsHostApp.app/AnalyticsHostApp"; + }; + name = Debug; + }; + 211035222BD6AB30006AC186 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W3DRXD72QU; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.analytics.ASPinpointAnalyticsPluginIntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AnalyticsHostApp.app/AnalyticsHostApp"; + }; + name = Release; + }; 6857646828AE94DA000CE2E9 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -899,6 +1028,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 211035202BD6AB30006AC186 /* Build configuration list for PBXNativeTarget "AWSPinpointAnalyticsPluginGen2IntegrationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 211035212BD6AB30006AC186 /* Debug */, + 211035222BD6AB30006AC186 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 6857645728AE94D9000CE2E9 /* Build configuration list for PBXProject "AnalyticsHostApp" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/xcshareddata/xcschemes/AWSPinpointAnalyticsPluginGen2IntegrationTests.xcscheme b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/xcshareddata/xcschemes/AWSPinpointAnalyticsPluginGen2IntegrationTests.xcscheme new file mode 100644 index 0000000000..a967b16a4f --- /dev/null +++ b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/xcshareddata/xcschemes/AWSPinpointAnalyticsPluginGen2IntegrationTests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/xcshareddata/xcschemes/AnalyticsHostApp.xcscheme b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/xcshareddata/xcschemes/AnalyticsHostApp.xcscheme index 131a29bb5f..96467b3383 100644 --- a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/xcshareddata/xcschemes/AnalyticsHostApp.xcscheme +++ b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AnalyticsHostApp.xcodeproj/xcshareddata/xcschemes/AnalyticsHostApp.xcscheme @@ -43,7 +43,7 @@ parallelizable = "YES"> diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift index 12818addea..4581f1d799 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift @@ -6,7 +6,7 @@ // import Foundation -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import AWSCognitoIdentity import AWSCognitoIdentityProvider @@ -24,17 +24,19 @@ extension AWSCognitoAuthPlugin { /// - Throws: /// - PluginError.pluginConfigurationError: If one of the configuration values is invalid or empty public func configure(using configuration: Any?) throws { - - guard let jsonValueConfiguration = configuration as? JSONValue else { + let authConfiguration: AuthConfiguration + if let configuration = configuration as? AmplifyOutputsData { + authConfiguration = try ConfigurationHelper.authConfiguration(configuration) + jsonConfiguration = ConfigurationHelper.createUserPoolJsonConfiguration(authConfiguration) + } else if let jsonValueConfiguration = configuration as? JSONValue { + jsonConfiguration = jsonValueConfiguration + authConfiguration = try ConfigurationHelper.authConfiguration(jsonValueConfiguration) + } else { throw PluginError.pluginConfigurationError( AuthPluginErrorConstants.decodeConfigurationError.errorDescription, AuthPluginErrorConstants.decodeConfigurationError.recoverySuggestion) } - jsonConfiguration = jsonValueConfiguration - - let authConfiguration = try ConfigurationHelper.authConfiguration(jsonValueConfiguration) - let credentialStoreResolver = CredentialStoreState.Resolver().eraseToAnyResolver() let credentialEnvironment = credentialStoreEnvironment(authConfiguration: authConfiguration) let credentialStoreMachine = StateMachine(resolver: credentialStoreResolver, diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/UserPoolConfigurationData.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/UserPoolConfigurationData.swift index 829edc362d..ef95993e96 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/UserPoolConfigurationData.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/UserPoolConfigurationData.swift @@ -6,6 +6,7 @@ // import ClientRuntime +@_spi(InternalAmplifyConfiguration) import Amplify struct UserPoolConfigurationData: Equatable { @@ -17,6 +18,10 @@ struct UserPoolConfigurationData: Equatable { let pinpointAppId: String? let hostedUIConfig: HostedUIConfigurationData? let authFlowType: AuthFlowType + let passwordProtectionSettings: PasswordProtectionSettings? + let usernameAttributes: [UsernameAttribute] + let signUpAttributes: [SignUpAttributeType] + let verificationMechanisms: [VerificationMechanism] init( poolId: String, @@ -26,7 +31,11 @@ struct UserPoolConfigurationData: Equatable { clientSecret: String? = nil, pinpointAppId: String? = nil, authFlowType: AuthFlowType = .userSRP, - hostedUIConfig: HostedUIConfigurationData? = nil + hostedUIConfig: HostedUIConfigurationData? = nil, + passwordProtectionSettings: PasswordProtectionSettings? = nil, + usernameAttributes: [UsernameAttribute] = [], + signUpAttributes: [SignUpAttributeType] = [], + verificationMechanisms: [VerificationMechanism] = [] ) { self.poolId = poolId self.clientId = clientId @@ -36,6 +45,10 @@ struct UserPoolConfigurationData: Equatable { self.pinpointAppId = pinpointAppId self.hostedUIConfig = hostedUIConfig self.authFlowType = authFlowType + self.passwordProtectionSettings = passwordProtectionSettings + self.usernameAttributes = usernameAttributes + self.signUpAttributes = signUpAttributes + self.verificationMechanisms = verificationMechanisms } /// Amazon Cognito user pool: cognito-idp..amazonaws.com/, @@ -56,7 +69,11 @@ extension UserPoolConfigurationData: CustomDebugDictionaryConvertible { "endpoint": endpoint ?? "N/A", "clientSecret": clientSecret.masked(interiorCount: 4), "pinpointAppId": pinpointAppId.masked(interiorCount: 4, retainingCount: 4), - "hostedUI": hostedUIConfig?.debugDescription ?? "N/A" + "hostedUI": hostedUIConfig?.debugDescription ?? "N/A", + "passwordProtectionSettings": passwordProtectionSettings.debugDescription, + "usernameAttributes": usernameAttributes.debugDescription, + "signUpAttributes": signUpAttributes.debugDescription, + "verificationMechanisms": verificationMechanisms.debugDescription ] } } @@ -83,3 +100,136 @@ extension UserPoolConfigurationData.CustomEndpoint { validatedHost = endpoint.host } } + +extension UserPoolConfigurationData { + + /// settings used in the Authenticator + struct PasswordProtectionSettings: Equatable, Codable { + let minLength: UInt + let characterPolicy: [PasswordCharacterPolicy] + + init(from passwordPolicy: AmplifyOutputsData.Auth.PasswordPolicy) { + var characterPolicy = [UserPoolConfigurationData.PasswordCharacterPolicy]() + if passwordPolicy.requireLowercase { + characterPolicy.append(.lowercase) + } + if passwordPolicy.requireUppercase { + characterPolicy.append(.uppercase) + } + if passwordPolicy.requireNumbers { + characterPolicy.append(.numbers) + } + if passwordPolicy.requireSymbols { + characterPolicy.append(.symbols) + } + + self.minLength = passwordPolicy.minLength + self.characterPolicy = characterPolicy + } + } + + enum PasswordCharacterPolicy: String, Codable { + case lowercase = "REQUIRES_LOWERCASE" + case uppercase = "REQUIRES_UPPERCASE" + case numbers = "REQUIRES_NUMBERS" + case symbols = "REQUIRES_SYMBOLS" + } +} + +extension UserPoolConfigurationData { + + /// Supported username attributes used in the Authenticator. + enum UsernameAttribute: String, Codable { + case username = "USERNAME" + case email = "EMAIL" + case phoneNumber = "PHONE_NUMBER" + + init(from attribute: AmplifyOutputsData.Auth.UsernameAttributes) { + switch attribute { + case .email: + self = .email + case .phoneNumber: + self = .phoneNumber + } + } + } +} + +extension UserPoolConfigurationData { + + /// Supported sign up attributes used in the Authenticator. + enum SignUpAttributeType: String, Codable { + case address = "ADDRESS" + case birthDate = "BIRTHDATE" + case email = "EMAIL" + case familyName = "FAMILY_NAME" + case gender = "GENDER" + case givenName = "GIVEN_NAME" + case middleName = "MIDDLE_NAME" + case name = "NAME" + case nickname = "NICKNAME" + case phoneNumber = "PHONE_NUMBER" + case preferredUsername = "PREFERRED_USERNAME" + case profile = "PROFILE" + case website = "WEBSITE" + + init?(from attribute: AmplifyOutputsData.AmazonCognitoStandardAttributes) { + switch attribute { + case .address: + self = .address + case .birthdate: + self = .birthDate + case .email: + self = .email + case .familyName: + self = .familyName + case .gender: + self = .gender + case .givenName: + self = .givenName + case .locale: + return nil + case .middleName: + self = .middleName + case .name: + self = .name + case .nickname: + self = .nickname + case .phoneNumber: + self = .phoneNumber + case .picture: + return nil + case .preferredUsername: + self = .preferredUsername + case .profile: + self = .profile + case .sub: + return nil + case .updatedAt: + return nil + case .website: + self = .website + case .zoneinfo: + return nil + } + } + } +} + +extension UserPoolConfigurationData { + + /// Supported verification mechanisms used in the Authenticator. + enum VerificationMechanism: String, Codable { + case email = "EMAIL" + case phoneNumber = "PHONE_NUMBER" + + init(from attribute: AmplifyOutputsData.Auth.UserVerificationType) { + switch attribute { + case .email: + self = .email + case .phoneNumber: + self = .phoneNumber + } + } + } +} diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift index 60deb125ab..42f647f95e 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift @@ -6,7 +6,7 @@ // import Foundation -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify struct ConfigurationHelper { @@ -23,6 +23,7 @@ struct ConfigurationHelper { return nil } + // parse `pinpointId` var pinpointId: String? if case .string(let pinpointIdFromConfig) = cognitoUserPoolJSON.value(at: "PinpointAppId") { pinpointId = pinpointIdFromConfig @@ -44,6 +45,7 @@ struct ConfigurationHelper { return nil }() + // parse `authFlowType` var authFlowType: AuthFlowType if case .boolean(let isMigrationEnabled) = cognitoUserPoolJSON.value(at: "MigrationEnabled"), isMigrationEnabled == true { @@ -56,11 +58,13 @@ struct ConfigurationHelper { authFlowType = .userSRP } + // parse `clientSecret` var clientSecret: String? if case .string(let clientSecretFromConfig) = cognitoUserPoolJSON.value(at: "AppClientSecret") { clientSecret = clientSecretFromConfig } + // parse `hostedUIConfig` let hostedUIConfig = parseHostedConfiguration( configuration: config.value(at: "Auth.Default.OAuth")) @@ -71,7 +75,49 @@ struct ConfigurationHelper { clientSecret: clientSecret, pinpointAppId: pinpointId, authFlowType: authFlowType, - hostedUIConfig: hostedUIConfig) + hostedUIConfig: hostedUIConfig, + passwordProtectionSettings: nil, + usernameAttributes: [], + signUpAttributes: [], + verificationMechanisms: []) + } + + static func parseUserPoolData(_ config: AmplifyOutputsData.Auth) -> UserPoolConfigurationData? { + let hostedUIConfig = parseHostedConfiguration(configuration: config) + + // parse `passwordProtectionSettings` + var passwordProtectionSettings: UserPoolConfigurationData.PasswordProtectionSettings? = nil + if let passwordPolicy = config.passwordPolicy { + passwordProtectionSettings = .init(from: passwordPolicy) + } + + // parse `usernameAttributes` + let usernameAttributes: [UserPoolConfigurationData.UsernameAttribute] = config + .usernameAttributes? + .compactMap { .init(from: $0) } ?? [] + + // parse `signUpAttributes` + let signUpAttributes: [UserPoolConfigurationData.SignUpAttributeType] = config + .standardRequiredAttributes? + .compactMap { .init(from: $0) } ?? [] + + // parse `verificationMechanisms` + let verificationMechanisms: [UserPoolConfigurationData.VerificationMechanism] = config + .userVerificationTypes? + .compactMap { .init(from: $0) } ?? [] + + return UserPoolConfigurationData(poolId: config.userPoolId, + clientId: config.userPoolClientId, + region: config.awsRegion, + endpoint: nil, // Gen2 does not support this field + clientSecret: nil, // Gen2 does not support this field + pinpointAppId: nil, // Gen2 does not support this field + authFlowType: .userSRP, + hostedUIConfig: hostedUIConfig, + passwordProtectionSettings: passwordProtectionSettings, + usernameAttributes: usernameAttributes, + signUpAttributes: signUpAttributes, + verificationMechanisms: verificationMechanisms) } static func parseHostedConfiguration(configuration: JSONValue?) -> HostedUIConfigurationData? { @@ -90,15 +136,50 @@ struct ConfigurationHelper { } return "" } - let oauth = OAuthConfigurationData(domain: domain, - scopes: scopesArray, - signInRedirectURI: signInRedirectURI, - signOutRedirectURI: signOutRedirectURI) + var clientSecret: String? if case .string(let appClientSecret) = configuration?.value(at: "AppClientSecret") { clientSecret = appClientSecret } - return HostedUIConfigurationData(clientId: appClientId, oauth: oauth, clientSecret: clientSecret) + + return createHostedConfiguration(appClientId: appClientId, + clientSecret: clientSecret, + domain: domain, + scopes: scopesArray, + signInRedirectURI: signInRedirectURI, + signOutRedirectURI: signOutRedirectURI) + } + + static func parseHostedConfiguration(configuration: AmplifyOutputsData.Auth) -> HostedUIConfigurationData? { + guard let oauth = configuration.oauth, + let signInRedirectURI = oauth.redirectSignInUri.first, + let signOutRedirectURI = oauth.redirectSignOutUri.first else { + return nil + } + + return createHostedConfiguration(appClientId: configuration.userPoolClientId, + clientSecret: nil, + domain: oauth.customDomain ?? oauth.cognitoDomain, + scopes: oauth.scopes, + signInRedirectURI: signInRedirectURI, + signOutRedirectURI: signOutRedirectURI) + + } + static func createHostedConfiguration(appClientId: String, + clientSecret: String?, + domain: String, + scopes: [String], + signInRedirectURI: String, + signOutRedirectURI: String) -> HostedUIConfigurationData { + + let oauth = OAuthConfigurationData(domain: domain, + scopes: scopes, + signInRedirectURI: signInRedirectURI, + signOutRedirectURI: signOutRedirectURI) + + return HostedUIConfigurationData(clientId: appClientId, + oauth: oauth, + clientSecret: clientSecret) } static func parseIdentityPoolData(_ config: JSONValue) -> IdentityPoolConfigurationData? { @@ -115,10 +196,40 @@ struct ConfigurationHelper { return IdentityPoolConfigurationData(poolId: poolId, region: region) } + static func parseIdentityPoolData(_ config: AmplifyOutputsData.Auth) -> IdentityPoolConfigurationData? { + if let identityPoolId = config.identityPoolId { + return IdentityPoolConfigurationData(poolId: identityPoolId, + region: config.awsRegion) + } else { + return nil + } + } + static func authConfiguration(_ config: JSONValue) throws -> AuthConfiguration { let userPoolConfig = try parseUserPoolData(config) let identityPoolConfig = parseIdentityPoolData(config) + return try createAuthConfiguration(userPoolConfig: userPoolConfig, + identityPoolConfig: identityPoolConfig) + } + + static func authConfiguration(_ config: AmplifyOutputsData) throws -> AuthConfiguration { + guard let config = config.auth else { + throw AuthError.configuration( + "Error configuring \(String(describing: self))", + AuthPluginErrorConstants.configurationMissingError + ) + } + let userPoolConfig = try parseUserPoolData(config) + let identityPoolConfig = parseIdentityPoolData(config) + + return try createAuthConfiguration(userPoolConfig: userPoolConfig, + identityPoolConfig: identityPoolConfig) + + } + + static func createAuthConfiguration(userPoolConfig: UserPoolConfigurationData?, + identityPoolConfig: IdentityPoolConfigurationData?) throws -> AuthConfiguration { if let userPoolConfigNonNil = userPoolConfig, let identityPoolConfigNonNil = identityPoolConfig { return .userPoolsAndIdentityPools(userPoolConfigNonNil, identityPoolConfigNonNil) } @@ -135,4 +246,46 @@ struct ConfigurationHelper { AuthPluginErrorConstants.configurationMissingError ) } + + static func createUserPoolJsonConfiguration(_ authConfig: AuthConfiguration) -> JSONValue { + let config: UserPoolConfigurationData + switch authConfig { + case .userPools(let userPoolConfig): + config = userPoolConfig + case .userPoolsAndIdentityPools(let userPoolConfig, _): + config = userPoolConfig + case .identityPools: + return JSONValue.null + } + + let usernameAttributes: [JSONValue] = config.usernameAttributes.map { .string($0.rawValue) } + let signUpAttributes: [JSONValue] = config.signUpAttributes.map { .string($0.rawValue) } + let verificationMechanisms: [JSONValue] = config.verificationMechanisms.map { .string($0.rawValue) } + + let authConfigObject: JSONValue + if let passwordProtectionSettings = config.passwordProtectionSettings { + let minLength = Double(passwordProtectionSettings.minLength) + let characterPolicy: [JSONValue] = passwordProtectionSettings.characterPolicy.map { .string($0.rawValue) } + + authConfigObject = .object( + ["usernameAttributes": .array(usernameAttributes), + "signupAttributes": .array(signUpAttributes), + "verificationMechanism": .array(verificationMechanisms), + "passwordProtectionSettings": .object( + ["passwordPolicyMinLength": .number(Double(minLength)), + "passwordPolicyCharacters": .array(characterPolicy)])]) + } else { + authConfigObject = .object( + ["usernameAttributes": .array(usernameAttributes), + "signupAttributes": .array(signUpAttributes), + "verificationMechanism": .array(verificationMechanisms)]) + } + + return JSONValue.object( + ["auth": .object( + ["plugins": .object( + ["awsCognitoAuthPlugin": .object( + ["Auth": .object( + ["Default": authConfigObject])])])])]) + } } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginAmplifyOutputsConfigTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginAmplifyOutputsConfigTests.swift new file mode 100644 index 0000000000..85eba053d7 --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/AWSCognitoAuthPluginAmplifyOutputsConfigTests.swift @@ -0,0 +1,126 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +@_spi(InternalAmplifyConfiguration) @testable import Amplify +@testable import AWSCognitoAuthPlugin + +class AWSCognitoAuthPluginAmplifyOutputsConfigTests: XCTestCase { + + override func tearDown() async throws { + await Amplify.reset() + } + + /// Test Auth configuration with invalid config for auth + /// + /// - Given: Given an invalid auth config + /// - When: + /// - I configure auth with the invalid configuration + /// - Then: + /// - I should get an exception. + /// + func testThrowsOnMissingConfig() throws { + let plugin = AWSCognitoAuthPlugin() + try Amplify.add(plugin: plugin) + + let amplifyConfig = AmplifyOutputsData() + + do { + try Amplify.configure(amplifyConfig) + } catch { + guard case AuthError.configuration = error else { + XCTFail("Should have thrown an AuthError.configuration if not supplied with auth config.") + return + } + } + } + + /// Test Auth configuration with valid config for user pool and identity pool + /// + /// - Given: Given valid config for user pool and identity pool + /// - When: + /// - I configure auth with the given configuration + /// - Then: + /// - I should not get any error while configuring auth + /// + func testConfigWithUserPoolAndIdentityPool() throws { + let plugin = AWSCognitoAuthPlugin() + try Amplify.add(plugin: plugin) + + let amplifyConfig = AmplifyOutputsData(auth: .init( + awsRegion: "us-east-1", + userPoolId: "xx", + userPoolClientId: "xx", + identityPoolId: "xx")) + do { + try Amplify.configure(amplifyConfig) + } catch { + XCTFail("Should not throw error. \(error)") + } + } + + /// Test Auth configuration with valid config for only user pool + /// + /// - Given: Given valid config for only user pool + /// - When: + /// - I configure auth with the given configuration + /// - Then: + /// - I should not get any error while configuring auth + /// + func testConfigWithOnlyUserPool() throws { + let plugin = AWSCognitoAuthPlugin() + try Amplify.add(plugin: plugin) + + let amplifyConfig = AmplifyOutputsData(auth: .init( + awsRegion: "us-east-1", + userPoolId: "xx", + userPoolClientId: "xx")) + do { + try Amplify.configure(amplifyConfig) + } catch { + XCTFail("Should not throw error. \(error)") + } + } + + /// Test Auth configuration with valid config for user pool and identity pool, with network preferences + /// + /// - Given: Given valid config for user pool and identity pool, and network preferences + /// - When: + /// - I configure auth with the given configuration and network preferences + /// - Then: + /// - I should not get any error while configuring auth + /// + func testConfigWithUserPoolAndIdentityPoolWithNetworkPreferences() throws { + let plugin = AWSCognitoAuthPlugin( + networkPreferences: .init( + maxRetryCount: 2, + timeoutIntervalForRequest: 60, + timeoutIntervalForResource: 60)) + try Amplify.add(plugin: plugin) + + let amplifyConfig = AmplifyOutputsData(auth: .init( + awsRegion: "us-east-1", + userPoolId: "xx", + userPoolClientId: "xx", + identityPoolId: "xx")) + + do { + try Amplify.configure(amplifyConfig) + + let escapeHatch = plugin.getEscapeHatch() + guard case .userPoolAndIdentityPool(let userPoolClient, let identityPoolClient) = escapeHatch else { + XCTFail("Expected .userPool, got \(escapeHatch)") + return + } + XCTAssertNotNil(userPoolClient) + XCTAssertNotNil(identityPoolClient) + + } catch { + XCTFail("Should not throw error. \(error)") + } + } +} diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift new file mode 100644 index 0000000000..812cb05b7c --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift @@ -0,0 +1,292 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +@_spi(InternalAmplifyConfiguration) @testable import Amplify +@testable import AWSCognitoAuthPlugin + +final class ConfigurationHelperTests: XCTestCase { + + /// Test parsing the config and verifying the defaults that are set. + func testParseUserPoolData_Defaults() throws { + let config = AmplifyOutputsData.Auth( + awsRegion: "us-east-1", + userPoolId: "poolId", + userPoolClientId: "clientId", + identityPoolId: "identityPoolId", + standardRequiredAttributes: [.email], + usernameAttributes: [.email], + userVerificationTypes: [.email], + unauthenticatedIdentitiesEnabled: true, + mfaConfiguration: nil, + mfaMethods: nil) + + guard let result = ConfigurationHelper.parseUserPoolData(config) else { + XCTFail("Expected to parse UserPoolData into object") + return + } + + XCTAssertEqual(result.poolId, "poolId") + XCTAssertEqual(result.clientId, "clientId") + XCTAssertEqual(result.region, "us-east-1") + XCTAssertEqual(result.authFlowType, .userSRP) + XCTAssertNil(result.endpoint, "Gen2 currently does not support custom endpoints") + XCTAssertNil(result.clientSecret, "Gen2 currently does not support using client secret") + XCTAssertNil(result.pinpointAppId, "Gen2 currently does not support automatically sending auth events through Pinpoint.") + } + + /// Testing the OAuth mapping logic, such as taking the first redirect URI in the array. + func testParseUserPoolData_WithOAuth() throws { + let config = AmplifyOutputsData.Auth( + awsRegion: "us-east-1", + userPoolId: "poolId", + userPoolClientId: "clientId", + oauth: AmplifyOutputsData.Auth.OAuth(identityProviders: ["provider1", "provider2"], + cognitoDomain: "cognitoDomain", + customDomain: nil, + scopes: ["scope1", "scope2"], + redirectSignInUri: ["redirect1", "redirect2"], + redirectSignOutUri: ["signOut1", "signOut2"], + responseType: "responseType")) + + guard let config = ConfigurationHelper.parseUserPoolData(config), + let hostedUIConfig = config.hostedUIConfig else { + XCTFail("Expected to parse UserPoolData into object") + return + } + + XCTAssertEqual(hostedUIConfig.clientId, "clientId") + XCTAssertNil(hostedUIConfig.clientSecret, "Client secret should be nil as its not supported in Gen2") + XCTAssertEqual(hostedUIConfig.oauth.scopes, ["scope1", "scope2"]) + XCTAssertEqual(hostedUIConfig.oauth.domain, "cognitoDomain") + XCTAssertEqual(hostedUIConfig.oauth.signInRedirectURI, "redirect1") + XCTAssertEqual(hostedUIConfig.oauth.signOutRedirectURI, "signOut1") + } + + /// Test Oauth section's `customDomain` overwrites `cognitoDomain` + func testParseUserPoolData_WithOAuth_CustomDomain() throws { + let config = AmplifyOutputsData.Auth( + awsRegion: "us-east-1", + userPoolId: "poolId", + userPoolClientId: "clientId", + oauth: AmplifyOutputsData.Auth.OAuth(identityProviders: ["provider1", "provider2"], + cognitoDomain: "cognitoDomain", + customDomain: "customDomain", + scopes: ["scope1", "scope2"], + redirectSignInUri: ["redirect1", "redirect2"], + redirectSignOutUri: ["signOut1", "signOut2"], + responseType: "responseType")) + + guard let config = ConfigurationHelper.parseUserPoolData(config), + let hostedUIConfig = config.hostedUIConfig else { + XCTFail("Expected to parse UserPoolData into object") + return + } + + XCTAssertEqual(hostedUIConfig.clientId, "clientId") + XCTAssertNil(hostedUIConfig.clientSecret) + XCTAssertEqual(hostedUIConfig.oauth.scopes, ["scope1", "scope2"]) + XCTAssertEqual(hostedUIConfig.oauth.domain, "customDomain") + XCTAssertEqual(hostedUIConfig.oauth.signInRedirectURI, "redirect1") + XCTAssertEqual(hostedUIConfig.oauth.signOutRedirectURI, "signOut1") + } + + /// Test that password policy is parsed correctly + func testParseUserPoolData_WithPasswordPolicy() throws { + let config = AmplifyOutputsData.Auth( + awsRegion: "us-east-1", + userPoolId: "poolId", + userPoolClientId: "clientId", + passwordPolicy: .init(minLength: 5, + requireNumbers: true, + requireLowercase: true, + requireUppercase: true, + requireSymbols: true)) + + guard let config = ConfigurationHelper.parseUserPoolData(config), + let result = config.passwordProtectionSettings else { + XCTFail("Expected to parse UserPoolData into object") + return + } + + XCTAssertEqual(result.minLength, 5) + XCTAssertTrue(result.characterPolicy.contains(.numbers)) + XCTAssertTrue(result.characterPolicy.contains(.lowercase)) + XCTAssertTrue(result.characterPolicy.contains(.uppercase)) + XCTAssertTrue(result.characterPolicy.contains(.symbols)) + } + + /// Test that the username attribute is parsed corrctly + func testParseUserPoolData_WithUsernameAttributes() throws { + let config = AmplifyOutputsData.Auth( + awsRegion: "us-east-1", + userPoolId: "poolId", + userPoolClientId: "clientId", + usernameAttributes: [.email, .phoneNumber]) + + guard let result = ConfigurationHelper.parseUserPoolData(config) else { + XCTFail("Expected to parse UserPoolData into object") + return + } + + XCTAssertEqual(result.usernameAttributes, [.email, .phoneNumber]) + } + + func testParseUserPoolData_WithStandardAttributes() throws { + let config = AmplifyOutputsData.Auth( + awsRegion: "us-east-1", + userPoolId: "poolId", + userPoolClientId: "clientId", + standardRequiredAttributes: [ + .address, + .birthdate, + .email, + .familyName, + .gender, + .givenName, + .middleName, + .name, + .nickname, + .phoneNumber, + .preferredUsername, + .profile, + .website + ]) + + guard let result = ConfigurationHelper.parseUserPoolData(config) else { + XCTFail("Expected to parse UserPoolData into object") + return + } + + XCTAssertEqual(result.signUpAttributes.count, config.standardRequiredAttributes?.count) + XCTAssertTrue(result.signUpAttributes.contains(.address)) + XCTAssertTrue(result.signUpAttributes.contains(.birthDate)) + XCTAssertTrue(result.signUpAttributes.contains(.email)) + XCTAssertTrue(result.signUpAttributes.contains(.familyName)) + XCTAssertTrue(result.signUpAttributes.contains(.gender)) + XCTAssertTrue(result.signUpAttributes.contains(.givenName)) + XCTAssertTrue(result.signUpAttributes.contains(.middleName)) + XCTAssertTrue(result.signUpAttributes.contains(.name)) + XCTAssertTrue(result.signUpAttributes.contains(.nickname)) + XCTAssertTrue(result.signUpAttributes.contains(.phoneNumber)) + XCTAssertTrue(result.signUpAttributes.contains(.preferredUsername)) + XCTAssertTrue(result.signUpAttributes.contains(.profile)) + XCTAssertTrue(result.signUpAttributes.contains(.website)) + } + + /// Test that some sign up attributes do not correspond to any standard attribute. + func testParseUserPoolData_WithMissingStandardToSignUpAttributeMapping() throws { + let config = AmplifyOutputsData.Auth( + awsRegion: "us-east-1", + userPoolId: "poolId", + userPoolClientId: "clientId", + standardRequiredAttributes: [ + .locale, + .picture, + .sub, + .updatedAt, + .zoneinfo + ]) + + guard let result = ConfigurationHelper.parseUserPoolData(config) else { + XCTFail("Expected to parse UserPoolData into object") + return + } + + XCTAssertEqual(result.signUpAttributes.count, 0) + } + + /// Test that the verification mechanisms are parsed correctly. + func testParseUserPoolData_WithVerificationMechanisms() throws { + let config = AmplifyOutputsData.Auth( + awsRegion: "us-east-1", + userPoolId: "poolId", + userPoolClientId: "clientId", + userVerificationTypes: [.phoneNumber, .email]) + + guard let result = ConfigurationHelper.parseUserPoolData(config) else { + XCTFail("Expected to parse UserPoolData into object") + return + } + + XCTAssertEqual(result.verificationMechanisms, [.phoneNumber, .email]) + } + + // MARK: - `createUserPoolJsonConfiguration` tests + + /// Test that the AuthConfiguration can be translated back to the expected JSON + /// for the authenticator to parse. + func testCreateUserPoolJsonConfiguration() throws { + let config = AuthConfiguration + .userPools(.init( + poolId: "", + clientId: "", + region: "", + passwordProtectionSettings: .init(from: .init( + minLength: 8, + requireNumbers: true, + requireLowercase: true, + requireUppercase: true, + requireSymbols: true)), + usernameAttributes: [ + .init(from: .email), + .init(from: .phoneNumber) + ], + signUpAttributes: [ + .init(from: .email)!, + .init(from: .address)!, + ], + verificationMechanisms: [ + .init(from: .email), + .init(from: .phoneNumber) + ])) + let json = ConfigurationHelper.createUserPoolJsonConfiguration(config) + + guard let authConfig = json.auth?.plugins?.awsCognitoAuthPlugin?.Auth?.Default else { + XCTFail("Could not retrieve auth configuration from json") + return + } + + XCTAssertEqual(authConfig.passwordProtectionSettings?.passwordPolicyMinLength, 8) + guard let passwordPolicyCharacters = authConfig.passwordProtectionSettings?.passwordPolicyCharacters?.asArray else { + XCTFail("Could not retrieve passwordPolicyCharacters from json") + return + } + XCTAssertTrue(passwordPolicyCharacters.contains("REQUIRES_LOWERCASE")) + XCTAssertTrue(passwordPolicyCharacters.contains("REQUIRES_UPPERCASE")) + XCTAssertTrue(passwordPolicyCharacters.contains("REQUIRES_NUMBERS")) + XCTAssertTrue(passwordPolicyCharacters.contains("REQUIRES_SYMBOLS")) + + guard let usernameAttributes = authConfig.usernameAttributes?.asArray else { + XCTFail("Could not retrieve usernameAttributes from json") + return + } + + XCTAssertEqual(usernameAttributes.count, 2) + XCTAssertTrue(usernameAttributes.contains("EMAIL")) + XCTAssertTrue(usernameAttributes.contains("PHONE_NUMBER")) + + guard let signupAttributes = authConfig.signupAttributes?.asArray else { + XCTFail("Could not retrieve signupAttributes from json") + return + } + + XCTAssertEqual(signupAttributes.count, 2) + XCTAssertTrue(signupAttributes.contains("EMAIL")) + XCTAssertTrue(signupAttributes.contains("ADDRESS")) + + guard let verificationMechanism = authConfig.verificationMechanism?.asArray else { + XCTFail("Could not retrieve verificationMechanism from json") + return + } + + XCTAssertEqual(verificationMechanism.count, 2) + XCTAssertTrue(verificationMechanism.contains("EMAIL")) + XCTAssertTrue(verificationMechanism.contains("PHONE_NUMBER")) + } +} + diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj index 6b7a9aa264..5d10a33c39 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj @@ -7,6 +7,38 @@ objects = { /* Begin PBXBuildFile section */ + 21F762A52BD6B1AA0048845A /* AuthSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5B727B61F0F006CCEC7 /* AuthSessionHelper.swift */; }; + 21F762A62BD6B1AA0048845A /* AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFEA828E747B80000C36A /* AsyncTesting.swift */; }; + 21F762A72BD6B1AA0048845A /* AuthSRPSignInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5BE27B61F1D006CCEC7 /* AuthSRPSignInTests.swift */; }; + 21F762A82BD6B1AA0048845A /* AuthForgetDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9737C74F2880BFD600DA0D2B /* AuthForgetDeviceTests.swift */; }; + 21F762A92BD6B1AA0048845A /* AuthConfirmSignUpTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43C26C827BC9D54003F3BF7 /* AuthConfirmSignUpTests.swift */; }; + 21F762AA2BD6B1AA0048845A /* MFAPreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48916F3B2A42333E00E3E1B1 /* MFAPreferenceTests.swift */; }; + 21F762AB2BD6B1AA0048845A /* AuthSignOutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5BD27B61F1D006CCEC7 /* AuthSignOutTests.swift */; }; + 21F762AC2BD6B1AA0048845A /* AuthFetchDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B370C42878DA5A00F1C088 /* AuthFetchDeviceTests.swift */; }; + 21F762AD2BD6B1AA0048845A /* TOTPSetupWhenUnauthenticatedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 483B0D332A42BB1400A1196B /* TOTPSetupWhenUnauthenticatedTests.swift */; }; + 21F762AE2BD6B1AA0048845A /* AsyncExpectation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFEA928E747B80000C36A /* AsyncExpectation.swift */; }; + 21F762AF2BD6B1AA0048845A /* GetCurrentUserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48E3AB3028E52590004EE395 /* GetCurrentUserTests.swift */; }; + 21F762B02BD6B1AA0048845A /* TOTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48916F392A412CEE00E3E1B1 /* TOTPHelper.swift */; }; + 21F762B12BD6B1AA0048845A /* AWSAuthBaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5AF27B61EAA006CCEC7 /* AWSAuthBaseTest.swift */; }; + 21F762B22BD6B1AA0048845A /* SignedOutAuthSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5BC27B61F1D006CCEC7 /* SignedOutAuthSessionTests.swift */; }; + 21F762B32BD6B1AA0048845A /* AuthSignInHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5B827B61F0F006CCEC7 /* AuthSignInHelper.swift */; }; + 21F762B42BD6B1AA0048845A /* FederatedSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4834D7C028B0770800DD564B /* FederatedSessionTests.swift */; }; + 21F762B52BD6B1AA0048845A /* AuthCustomSignInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4821B2F328737130000EC1D7 /* AuthCustomSignInTests.swift */; }; + 21F762B62BD6B1AA0048845A /* AuthEventIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 484EDEB127F4FFBE000284B4 /* AuthEventIntegrationTests.swift */; }; + 21F762B72BD6B1AA0048845A /* AuthEnvironmentHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 484834BD27B6FD9B00649D11 /* AuthEnvironmentHelper.swift */; }; + 21F762B82BD6B1AA0048845A /* TOTPSetupWhenAuthenticatedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48916F372A412B2800E3E1B1 /* TOTPSetupWhenAuthenticatedTests.swift */; }; + 21F762B92BD6B1AA0048845A /* CredentialStoreConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 484834BB27B6ED8700649D11 /* CredentialStoreConfigurationTests.swift */; }; + 21F762BA2BD6B1AA0048845A /* AuthRememberDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9737C74D287E208400DA0D2B /* AuthRememberDeviceTests.swift */; }; + 21F762BB2BD6B1AA0048845A /* XCTestCase+AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFEAA28E747B80000C36A /* XCTestCase+AsyncTesting.swift */; }; + 21F762BC2BD6B1AA0048845A /* AuthResendSignUpCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43C26C927BC9D54003F3BF7 /* AuthResendSignUpCodeTests.swift */; }; + 21F762BD2BD6B1AA0048845A /* AuthResetPasswordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97829200286B802E000DE190 /* AuthResetPasswordTests.swift */; }; + 21F762BE2BD6B1AA0048845A /* AuthUserAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485105A92840513C002D6FC8 /* AuthUserAttributesTests.swift */; }; + 21F762BF2BD6B1AA0048845A /* MFASignInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48599D492A429893009DE21C /* MFASignInTests.swift */; }; + 21F762C02BD6B1AA0048845A /* SignedInAuthSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5BB27B61F1D006CCEC7 /* SignedInAuthSessionTests.swift */; }; + 21F762C12BD6B1AA0048845A /* AuthSignUpTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43C26C727BC9D54003F3BF7 /* AuthSignUpTests.swift */; }; + 21F762C22BD6B1AA0048845A /* AuthConfirmResetPasswordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97829202286E41FA000DE190 /* AuthConfirmResetPasswordTests.swift */; }; + 21F762C32BD6B1AA0048845A /* AuthDeleteUserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4821B2F1286B5F74000EC1D7 /* AuthDeleteUserTests.swift */; }; + 21F762C62BD6B1AA0048845A /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 485CB5AE27B61EAA006CCEC7 /* README.md */; }; 4821B2F2286B5F74000EC1D7 /* AuthDeleteUserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4821B2F1286B5F74000EC1D7 /* AuthDeleteUserTests.swift */; }; 4821B2F428737130000EC1D7 /* AuthCustomSignInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4821B2F328737130000EC1D7 /* AuthCustomSignInTests.swift */; }; 4834D7C128B0770800DD564B /* FederatedSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4834D7C028B0770800DD564B /* FederatedSessionTests.swift */; }; @@ -94,6 +126,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 21F762A12BD6B1AA0048845A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 485CB53227B614CE006CCEC7 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 681B767F2A3CB86B004B59D9; + remoteInfo = AuthWatchApp; + }; + 21F762A32BD6B1AA0048845A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 485CB53227B614CE006CCEC7 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 485CB53927B614CE006CCEC7; + remoteInfo = AuthHostApp; + }; 485CB5A327B61E04006CCEC7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 485CB53227B614CE006CCEC7 /* Project object */; @@ -125,6 +171,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 21F762CB2BD6B1AA0048845A /* AuthGen2IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthGen2IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 21F762CC2BD6B1CD0048845A /* AuthGen2IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AuthGen2IntegrationTests.xctestplan; sourceTree = ""; }; 4821B2F1286B5F74000EC1D7 /* AuthDeleteUserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthDeleteUserTests.swift; sourceTree = ""; }; 4821B2F328737130000EC1D7 /* AuthCustomSignInTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCustomSignInTests.swift; sourceTree = ""; }; 4834D7C028B0770800DD564B /* FederatedSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederatedSessionTests.swift; sourceTree = ""; }; @@ -173,6 +221,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 21F762C42BD6B1AA0048845A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 485CB53727B614CE006CCEC7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -269,6 +324,7 @@ 97914B4B29550988002000EA /* AuthStressTests.xctest */, 681B76802A3CB86B004B59D9 /* AuthWatchApp.app */, 681B76C42A3CBBAE004B59D9 /* AuthIntegrationTestsWatch.xctest */, + 21F762CB2BD6B1AA0048845A /* AuthGen2IntegrationTests.xctest */, ); name = Products; sourceTree = ""; @@ -303,6 +359,7 @@ 485CB5A027B61E04006CCEC7 /* AuthIntegrationTests */ = { isa = PBXGroup; children = ( + 21F762CC2BD6B1CD0048845A /* AuthGen2IntegrationTests.xctestplan */, 48916F362A412AF800E3E1B1 /* MFATests */, 97B370C32878DA3500F1C088 /* DeviceTests */, 4821B2F0286B5F74000EC1D7 /* AuthDeleteUserTests */, @@ -432,6 +489,28 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 21F7629F2BD6B1AA0048845A /* AuthGen2IntegrationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 21F762C82BD6B1AA0048845A /* Build configuration list for PBXNativeTarget "AuthGen2IntegrationTests" */; + buildPhases = ( + 21F762A42BD6B1AA0048845A /* Sources */, + 21F762C42BD6B1AA0048845A /* Frameworks */, + 21F762C52BD6B1AA0048845A /* Resources */, + 21F762C72BD6B1AA0048845A /* Copy Configuration folder */, + ); + buildRules = ( + ); + dependencies = ( + 21F762A02BD6B1AA0048845A /* PBXTargetDependency */, + 21F762A22BD6B1AA0048845A /* PBXTargetDependency */, + ); + name = AuthGen2IntegrationTests; + packageProductDependencies = ( + ); + productName = AuthIntegrationTests; + productReference = 21F762CB2BD6B1AA0048845A /* AuthGen2IntegrationTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 485CB53927B614CE006CCEC7 /* AuthHostApp */ = { isa = PBXNativeTarget; buildConfigurationList = 485CB55E27B614CF006CCEC7 /* Build configuration list for PBXNativeTarget "AuthHostApp" */; @@ -583,11 +662,20 @@ 97914B2629550988002000EA /* AuthStressTests */, 681B767F2A3CB86B004B59D9 /* AuthWatchApp */, 681B769D2A3CBBAE004B59D9 /* AuthIntegrationTestsWatch */, + 21F7629F2BD6B1AA0048845A /* AuthGen2IntegrationTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 21F762C52BD6B1AA0048845A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 21F762C62BD6B1AA0048845A /* README.md in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 485CB53827B614CE006CCEC7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -631,6 +719,24 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 21F762C72BD6B1AA0048845A /* Copy Configuration folder */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy Configuration folder"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "TEMP_FILE=$HOME/.aws-amplify/amplify-ios/testconfiguration/.\nDEST_PATH=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/testconfiguration/\"\n\nif [[ ! -d $TEMP_FILE ]] ; then\n echo \"${TEMP_FILE} does not exist. Using empty configuration.\"\n exit 0\nfi\n \nif [[ -f $DEST_PATH ]] ; then\n rm $DEST_PATH\nfi\n \ncp -r $TEMP_FILE $DEST_PATH\n"; + }; 681B76C02A3CBBAE004B59D9 /* Copy Configuration folder */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -688,6 +794,44 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 21F762A42BD6B1AA0048845A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 21F762A52BD6B1AA0048845A /* AuthSessionHelper.swift in Sources */, + 21F762A62BD6B1AA0048845A /* AsyncTesting.swift in Sources */, + 21F762A72BD6B1AA0048845A /* AuthSRPSignInTests.swift in Sources */, + 21F762A82BD6B1AA0048845A /* AuthForgetDeviceTests.swift in Sources */, + 21F762A92BD6B1AA0048845A /* AuthConfirmSignUpTests.swift in Sources */, + 21F762AA2BD6B1AA0048845A /* MFAPreferenceTests.swift in Sources */, + 21F762AB2BD6B1AA0048845A /* AuthSignOutTests.swift in Sources */, + 21F762AC2BD6B1AA0048845A /* AuthFetchDeviceTests.swift in Sources */, + 21F762AD2BD6B1AA0048845A /* TOTPSetupWhenUnauthenticatedTests.swift in Sources */, + 21F762AE2BD6B1AA0048845A /* AsyncExpectation.swift in Sources */, + 21F762AF2BD6B1AA0048845A /* GetCurrentUserTests.swift in Sources */, + 21F762B02BD6B1AA0048845A /* TOTPHelper.swift in Sources */, + 21F762B12BD6B1AA0048845A /* AWSAuthBaseTest.swift in Sources */, + 21F762B22BD6B1AA0048845A /* SignedOutAuthSessionTests.swift in Sources */, + 21F762B32BD6B1AA0048845A /* AuthSignInHelper.swift in Sources */, + 21F762B42BD6B1AA0048845A /* FederatedSessionTests.swift in Sources */, + 21F762B52BD6B1AA0048845A /* AuthCustomSignInTests.swift in Sources */, + 21F762B62BD6B1AA0048845A /* AuthEventIntegrationTests.swift in Sources */, + 21F762B72BD6B1AA0048845A /* AuthEnvironmentHelper.swift in Sources */, + 21F762B82BD6B1AA0048845A /* TOTPSetupWhenAuthenticatedTests.swift in Sources */, + 21F762B92BD6B1AA0048845A /* CredentialStoreConfigurationTests.swift in Sources */, + 21F762BA2BD6B1AA0048845A /* AuthRememberDeviceTests.swift in Sources */, + 21F762BB2BD6B1AA0048845A /* XCTestCase+AsyncTesting.swift in Sources */, + 21F762BC2BD6B1AA0048845A /* AuthResendSignUpCodeTests.swift in Sources */, + 21F762BD2BD6B1AA0048845A /* AuthResetPasswordTests.swift in Sources */, + 21F762BE2BD6B1AA0048845A /* AuthUserAttributesTests.swift in Sources */, + 21F762BF2BD6B1AA0048845A /* MFASignInTests.swift in Sources */, + 21F762C02BD6B1AA0048845A /* SignedInAuthSessionTests.swift in Sources */, + 21F762C12BD6B1AA0048845A /* AuthSignUpTests.swift in Sources */, + 21F762C22BD6B1AA0048845A /* AuthConfirmResetPasswordTests.swift in Sources */, + 21F762C32BD6B1AA0048845A /* AuthDeleteUserTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 485CB53627B614CE006CCEC7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -799,6 +943,16 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 21F762A02BD6B1AA0048845A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 681B767F2A3CB86B004B59D9 /* AuthWatchApp */; + targetProxy = 21F762A12BD6B1AA0048845A /* PBXContainerItemProxy */; + }; + 21F762A22BD6B1AA0048845A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 485CB53927B614CE006CCEC7 /* AuthHostApp */; + targetProxy = 21F762A32BD6B1AA0048845A /* PBXContainerItemProxy */; + }; 485CB5A427B61E04006CCEC7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 485CB53927B614CE006CCEC7 /* AuthHostApp */; @@ -822,6 +976,50 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 21F762C92BD6B1AA0048845A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 94KV3E626L; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.auth.AuthIntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator watchos watchsimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AuthHostApp.app/AuthHostApp"; + "TEST_HOST[sdk=watchsimulator*]" = "$(BUILT_PRODUCTS_DIR)/AuthWatchApp.app/AuthWatchApp"; + }; + name = Debug; + }; + 21F762CA2BD6B1AA0048845A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 94KV3E626L; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.auth.AuthIntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator watchos watchsimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AuthHostApp.app/AuthHostApp"; + "TEST_HOST[sdk=watchsimulator*]" = "$(BUILT_PRODUCTS_DIR)/AuthWatchApp.app/AuthWatchApp"; + }; + name = Release; + }; 485CB55C27B614CF006CCEC7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1209,6 +1407,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 21F762C82BD6B1AA0048845A /* Build configuration list for PBXNativeTarget "AuthGen2IntegrationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 21F762C92BD6B1AA0048845A /* Debug */, + 21F762CA2BD6B1AA0048845A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 485CB53527B614CE006CCEC7 /* Build configuration list for PBXProject "AuthHostApp" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/xcshareddata/xcschemes/AuthGen2IntegrationTests.xcscheme b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/xcshareddata/xcschemes/AuthGen2IntegrationTests.xcscheme new file mode 100644 index 0000000000..407d632b29 --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/xcshareddata/xcschemes/AuthGen2IntegrationTests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift index 9e4b36b931..ee974fa662 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift @@ -6,7 +6,7 @@ // import XCTest -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify import AWSCognitoAuthPlugin class AWSAuthBaseTest: XCTestCase { @@ -27,10 +27,17 @@ class AWSAuthBaseTest: XCTestCase { } var amplifyConfigurationFile = "testconfiguration/AWSCognitoAuthPluginIntegrationTests-amplifyconfiguration" + let amplifyOutputsFile = + "testconfiguration/AWSCognitoAuthPluginIntegrationTests-amplify_outputs" let credentialsFile = "testconfiguration/AWSCognitoAuthPluginIntegrationTests-credentials" var amplifyConfiguration: AmplifyConfiguration! - + var amplifyOutputs: AmplifyOutputsData! + + var useGen2Configuration: Bool { + ProcessInfo.processInfo.arguments.contains("GEN2") + } + override func setUp() async throws { try await super.setUp() initializeAmplify() @@ -44,16 +51,21 @@ class AWSAuthBaseTest: XCTestCase { func initializeAmplify() { do { - let configuration = try TestConfigHelper.retrieveAmplifyConfiguration( - forResource: amplifyConfigurationFile) - amplifyConfiguration = configuration - let credentialsConfiguration = (try? TestConfigHelper.retrieveCredentials(forResource: credentialsFile)) ?? [:] defaultTestEmail = credentialsConfiguration["test_email_1"] ?? defaultTestEmail defaultTestPassword = credentialsConfiguration["password"] ?? defaultTestPassword let authPlugin = AWSCognitoAuthPlugin() try Amplify.add(plugin: authPlugin) - try Amplify.configure(configuration) + + if useGen2Configuration { + let data = try TestConfigHelper.retrieve(forResource: amplifyOutputsFile) + try Amplify.configure(with: .data(data)) + } else { + let configuration = try TestConfigHelper.retrieveAmplifyConfiguration( + forResource: amplifyConfigurationFile) + amplifyConfiguration = configuration + try Amplify.configure(amplifyConfiguration) + } Amplify.Logging.logLevel = .verbose print("Amplify configured with auth plugin") } catch { @@ -106,7 +118,6 @@ class AWSAuthBaseTest: XCTestCase { class TestConfigHelper { static func retrieveAmplifyConfiguration(forResource: String) throws -> AmplifyConfiguration { - let data = try retrieve(forResource: forResource) return try AmplifyConfiguration.decodeAmplifyConfiguration(from: data) } @@ -122,7 +133,7 @@ class TestConfigHelper { return json } - private static func retrieve(forResource: String) throws -> Data { + static func retrieve(forResource: String) throws -> Data { guard let path = Bundle(for: self).path(forResource: forResource, ofType: "json") else { throw TestConfigError.bundlePathError("Could not retrieve configuration file: \(forResource)") } diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AuthGen2IntegrationTests.xctestplan b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AuthGen2IntegrationTests.xctestplan new file mode 100644 index 0000000000..fe23bfbd1a --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AuthGen2IntegrationTests.xctestplan @@ -0,0 +1,28 @@ +{ + "configurations" : [ + { + "id" : "450D74A7-6EBF-40F5-87CE-BB73E08C2008", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "commandLineArgumentEntries" : [ + { + "argument" : "GEN2" + } + ] + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:AuthHostApp.xcodeproj", + "identifier" : "21F7629F2BD6B1AA0048845A", + "name" : "AuthGen2IntegrationTests" + } + } + ], + "version" : 1 +} diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthSignInHelper.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthSignInHelper.swift index d27dd5fbdb..1dc43decc6 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthSignInHelper.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/Helpers/AuthSignInHelper.swift @@ -26,6 +26,7 @@ enum AuthSignInHelper { var userAttributes = [ AuthUserAttribute(.email, value: email) ] + if let phoneNumber = phoneNumber { userAttributes.append(AuthUserAttribute(.phoneNumber, value: phoneNumber)) } diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/README.md b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/README.md index e3bec4bd04..271a8d22d9 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/README.md +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/README.md @@ -1,8 +1,8 @@ -# AWSCognitoAuthPlugin Integration tests +# Schema: AuthIntegrationTests - AWSCognitoAuthPlugin Integration tests The following steps demonstrate how to setup the integration tests for auth plugin. -## CLI setup +## (Gen1) CLI setup The integration test require auth configured with AWS Cognito User Pool and AWS Cognito Identity Pool. @@ -86,3 +86,125 @@ This will create a amplifyconfiguration.json file in your local, copy that file For Auth Device tests: Follow steps here (https://docs.amplify.aws/lib/auth/device_features/q/platform/ios/#configure-auth-category)[https://docs.amplify.aws/lib/auth/device_features/q/platform/ios/#configure-auth-category] and select "Always" for "Do you want to remember your user's devices?" + + +# Schema: AuthGen2IntegrationTests + +## Schema: AuthGen2IntegrationTests + +The following steps demonstrate how to setup the integration tests for auth plugin using Amplify CLI (Gen2). + +### Set-up + +At the time this was written, it follows the steps from here https://docs.amplify.aws/gen2/deploy-and-host/fullstack-branching/mono-and-multi-repos/ + +1. From a new folder, run `npm create amplify@beta`. This uses the following versions of the Amplify CLI, see `package.json` file below. + +```json +{ + ... + "devDependencies": { + "@aws-amplify/backend": "^0.13.0-beta.14", + "@aws-amplify/backend-cli": "^0.12.0-beta.16", + "aws-cdk": "^2.134.0", + "aws-cdk-lib": "^2.134.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "tsx": "^4.7.1", + "typescript": "^5.4.3" + }, + "dependencies": { + "aws-amplify": "^6.0.25" + } +} + +``` +2. Update `amplify/auth/resource.ts`. The resulting file should look like this + +```ts +import { defineAuth, defineFunction } from '@aws-amplify/backend'; + +/** + * Define and configure your auth resource + * @see https://docs.amplify.aws/gen2/build-a-backend/auth + */ +export const auth = defineAuth({ + loginWith: { + email: true + }, + triggers: { + // configure a trigger to point to a function definition + preSignUp: defineFunction({ + entry: './pre-sign-up-handler.ts' + }) + } +}); + +``` + +```ts +import type { PreSignUpTriggerHandler } from 'aws-lambda'; + +export const handler: PreSignUpTriggerHandler = async (event) => { + // your code here + event.response.autoConfirmUser = true + return event; +}; +``` + +Update `backend.ts` + +```ts +const { cfnUserPool } = backend.auth.resources.cfnResources +cfnUserPool.usernameAttributes = [] + +cfnUserPool.addPropertyOverride( + "Policies", + { + PasswordPolicy: { + MinimumLength: 10, + RequireLowercase: false, + RequireNumbers: true, + RequireSymbols: true, + RequireUppercase: true, + TemporaryPasswordValidityDays: 20, + }, + } +); +``` + +4. Deploy the backend with npx amplify sandbox + +For example, this deploys to a sandbox env and generates the amplify_outputs.json file. + +``` +npx amplify sandbox --config-out-dir ./config --config-version 1 --profile [PROFILE] +``` + +5. Copy the `amplify_outputs.json` file over to the test directory as `AWSCognitoAuthPluginIntegrationTests-amplify_outputs.json`. The tests will automatically pick this file up. Create the directories in this path first if it currently doesn't exist. + +``` +cp amplify_outputs.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSCognitoAuthPluginIntegrationTests-amplify_outputs.json +``` + +### Deploying from a branch (Optional) + +If you want to be able utilize Git commits for deployments + +1. Commit and push the files to a git repository. + +2. Navigate to the AWS Amplify console (https://us-east-1.console.aws.amazon.com/amplify/home?region=us-east-1#/) + +3. Click on "Try Amplify Gen 2" button. + +4. Choose "Option 2: Start with an existing app", and choose Github, and press Next. + +5. Find the repository and branch, and click Next + +6. Click "Save and deploy" and wait for deployment to finish. + +7. Generate the `amplify_outputs.json` configuration file + +``` +npx amplify generate config --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 +``` diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignInTests/AuthSRPSignInTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignInTests/AuthSRPSignInTests.swift index 217e0ed8e3..9f65e88532 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignInTests/AuthSRPSignInTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignInTests/AuthSRPSignInTests.swift @@ -31,13 +31,11 @@ class AuthSRPSignInTests: AWSAuthBaseTest { /// - I should get a completed signIn flow. /// func testSuccessfulSignIn() async throws { - let username = "integTest\(UUID().uuidString)" let password = "P123@\(UUID().uuidString)" - let didSucceed = try await AuthSignInHelper.signUpUser(username: username, - password: password, - email: defaultTestEmail) + password: password, + email: defaultTestEmail) XCTAssertTrue(didSucceed, "Signup operation failed") do { let signInResult = try await Amplify.Auth.signIn(username: username, password: password) @@ -56,10 +54,8 @@ class AuthSRPSignInTests: AWSAuthBaseTest { /// - I should get a completed signIn flow. /// func testSignInWithWrongPassword() async throws { - let username = "integTest\(UUID().uuidString)" let password = "P123@\(UUID().uuidString)" - let didSucceed = try await AuthSignInHelper.signUpUser(username: username, password: password, email: defaultTestEmail) @@ -159,6 +155,8 @@ class AuthSRPSignInTests: AWSAuthBaseTest { do { _ = try await Amplify.Auth.signIn(username: "username-doesnot-exist", password: "password") XCTFail("SignIn with unknown user should not succeed") + } catch AuthError.notAuthorized { + // App clients with "Prevent user existence errors" enabled will return this. } catch let error as AuthError { let underlyingError = error.underlyingError as? AWSCognitoAuthError switch underlyingError { @@ -187,7 +185,6 @@ class AuthSRPSignInTests: AWSAuthBaseTest { func testSignInWhenAlreadySignedIn() async throws { let username = "integTest\(UUID().uuidString)" let password = "P123@\(UUID().uuidString)" - let didSucceed = try await AuthSignInHelper.registerAndSignInUser(username: username, password: password, email: defaultTestEmail) XCTAssertTrue(didSucceed, "SignIn operation failed") @@ -310,7 +307,6 @@ class AuthSRPSignInTests: AWSAuthBaseTest { Task { let username = "integTest\(UUID().uuidString)" let password = "P123@\(UUID().uuidString)" - let didSucceed = try await AuthSignInHelper.signUpUser( username: username, password: password, diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthSignUpTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthSignUpTests.swift index c84a653041..73ce715670 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthSignUpTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthSignUpTests.swift @@ -22,6 +22,7 @@ class AuthSignUpTests: AWSAuthBaseTest { func testSuccessfulRegisterUser() async throws { let username = "integTest\(UUID().uuidString)" let password = "P123@\(UUID().uuidString)" + let options = AuthSignUpRequest.Options(userAttributes: [ AuthUserAttribute(.email, value: defaultTestEmail)]) let signUpResult = try await Amplify.Auth.signUp(username: username, diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/project.pbxproj index f99b0aa800..baf78f85ac 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 21D41D1A2BC728190019D811 /* AuthHostedUIGen2App.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AuthHostedUIGen2App.xctestplan; sourceTree = ""; }; B41080DE291ACF7E00297354 /* AuthHostedUIAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthHostedUIAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; B41080EA291ACFDB00297354 /* amplify-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "amplify-swift"; path = ../../../..; sourceTree = ""; }; B41080F4291AD10700297354 /* ConfigurationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationHelper.swift; sourceTree = ""; }; @@ -161,6 +162,7 @@ B4EB96AA291ACF4400B73755 /* AuthHostedUIApp */ = { isa = PBXGroup; children = ( + 21D41D1A2BC728190019D811 /* AuthHostedUIGen2App.xctestplan */, B4B978C5291C9A3B005B465D /* Views */, B4B978C2291C8F76005B465D /* Info.plist */, B41080F3291AD0F100297354 /* Utils */, diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/AuthHostedUIAppApp.swift b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/AuthHostedUIAppApp.swift index b12e2b2ec3..463e0fe906 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/AuthHostedUIAppApp.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/AuthHostedUIAppApp.swift @@ -13,8 +13,13 @@ import AWSCognitoAuthPlugin struct AuthHostedUIAppApp: App { let amplifyConfigurationFile = "testconfiguration/AWSCognitoAuthPluginHostedUIIntegrationTests-amplifyconfiguration" + let amplifyOutputsFile = "testconfiguration/AWSCognitoAuthPluginHostedUIIntegrationTests-amplify_outputs" var amplifyConfiguration: AmplifyConfiguration! + var useGen2Configuration: Bool { + ProcessInfo.processInfo.arguments.contains("GEN2") + } + var body: some Scene { WindowGroup { ContentView() @@ -23,16 +28,21 @@ struct AuthHostedUIAppApp: App { init() { do { - let configuration = retreiveConfiguration() try Amplify.add(plugin: AWSCognitoAuthPlugin()) - try Amplify.configure(configuration) + if useGen2Configuration { + let data = try ConfigurationHelper.retrieve(forResource: amplifyOutputsFile) + try Amplify.configure(with: .data(data)) + } else { + let configuration = retreiveConfiguration() + try Amplify.configure(configuration) + } + print("Amplify configured with auth plugin") } catch { print("Failed to initialize Amplify with \(error)") } } - - + func retreiveConfiguration() -> AmplifyConfiguration { do { return try ConfigurationHelper.retrieveAmplifyConfiguration( diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/AuthHostedUIGen2App.xctestplan b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/AuthHostedUIGen2App.xctestplan new file mode 100644 index 0000000000..35b9995c15 --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/AuthHostedUIGen2App.xctestplan @@ -0,0 +1,28 @@ +{ + "configurations" : [ + { + "id" : "30D3C91B-B890-4F80-A290-A937B0782750", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "commandLineArgumentEntries" : [ + { + "argument" : "GEN2" + } + ] + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:AuthHostedUIApp.xcodeproj", + "identifier" : "B41080DD291ACF7E00297354", + "name" : "AuthHostedUIAppUITests" + } + } + ], + "version" : 1 +} diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/ContentView.swift b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/ContentView.swift index 30ead7bc94..715142da7d 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/ContentView.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/ContentView.swift @@ -42,7 +42,6 @@ struct ContentView: View { } self.loading = false } - } struct ContentView_Previews: PreviewProvider { diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/Utils/ConfigurationHelper.swift b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/Utils/ConfigurationHelper.swift index 87613b4bdb..7881ee5d31 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/Utils/ConfigurationHelper.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/Utils/ConfigurationHelper.swift @@ -47,7 +47,7 @@ class ConfigurationHelper { return AmplifyConfiguration(auth: authConfiguration) } - private static func retrieve(forResource: String) throws -> Data { + static func retrieve(forResource: String) throws -> Data { guard let path = Bundle(for: self).path(forResource: forResource, ofType: "json") else { throw ConfigurationError.bundlePathError( "Could not retrieve configuration file: \(forResource)") diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/UITestCase.swift b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/UITestCase.swift index b280805b31..86e4450d4a 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/UITestCase.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/UITestCase.swift @@ -16,6 +16,9 @@ class UITestCase: XCTestCase { override func setUp() { continueAfterFailure = false app = XCUIApplication() + if ProcessInfo.processInfo.arguments.contains("GEN2") { + app.launchArguments.append("GEN2") + } app.launch() AuthenticatedScreen.signOutIfAuthenticated(app: app) diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/AWSDataStorePluginTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/AWSDataStorePluginTests.swift index 5b8e0271c4..de910896fe 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/AWSDataStorePluginTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/AWSDataStorePluginTests.swift @@ -9,11 +9,29 @@ import XCTest import AmplifyTestCommon @_implementationOnly import AmplifyAsyncTesting -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AWSDataStorePlugin // swiftlint:disable type_body_length class AWSDataStorePluginTests: XCTestCase { + + /// Ensure that DataStore configures successfully, regardless of what configuration is passed to `configure(using:)` + func testConfigureWithAmplifyOutputs() throws { + let storageEngineBehaviorFactory: StorageEngineBehaviorFactory = {_, _, _, _, _, _ throws in + return MockStorageEngineBehavior() + } + let plugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(), + storageEngineBehaviorFactory: storageEngineBehaviorFactory, + dataStorePublisher: DataStorePublisher(), + validAPIPluginKey: "MockAPICategoryPlugin", + validAuthPluginKey: "MockAuthCategoryPlugin") + + let config = AmplifyOutputsData() + do { + try plugin.configure(using: config) + } + } + func testStorageEngineDoesNotStartsOnConfigure() throws { let startExpectation = expectation(description: "Start Sync should not be called") startExpectation.isInverted = true diff --git a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift index 5bf5d34e23..7a59c3c809 100644 --- a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift +++ b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift @@ -6,7 +6,7 @@ // import Foundation -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import AWSPluginsCore @_spi(PluginHTTPClientEngine) import AWSPluginsCore import AWSLocation @@ -21,7 +21,15 @@ extension AWSLocationGeoPlugin { /// - Throws: /// - PluginError.pluginConfigurationError: If one of the configuration values is invalid or empty. public func configure(using configuration: Any?) throws { - let pluginConfiguration = try AWSLocationGeoPluginConfiguration(config: configuration) + let pluginConfiguration: AWSLocationGeoPluginConfiguration + if let configuration = configuration as? AmplifyOutputsData { + pluginConfiguration = try AWSLocationGeoPluginConfiguration(config: configuration) + } else if let configJSON = configuration as? JSONValue { + pluginConfiguration = try AWSLocationGeoPluginConfiguration(config: configJSON) + } else { + throw GeoPluginConfigError.configurationInvalid(section: .plugin) + } + try configure(using: pluginConfiguration) } diff --git a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/Configuration/AWSLocationGeoPluginConfiguration.swift b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/Configuration/AWSLocationGeoPluginConfiguration.swift index 9be12695fc..4d25b090a5 100644 --- a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/Configuration/AWSLocationGeoPluginConfiguration.swift +++ b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/Configuration/AWSLocationGeoPluginConfiguration.swift @@ -5,11 +5,15 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import Foundation import AWSLocation public struct AWSLocationGeoPluginConfiguration { + private static func urlString(regionName: String, mapName: String) -> String { + "https://maps.geo.\(regionName).amazonaws.com/maps/v0/maps/\(mapName)/style-descriptor" + } + let defaultMap: String? let maps: [String: Geo.MapStyle] let defaultSearchIndex: String? @@ -17,13 +21,9 @@ public struct AWSLocationGeoPluginConfiguration { public let regionName: String - init(config: Any?) throws { - guard let configJSON = config as? JSONValue else { - throw GeoPluginConfigError.configurationInvalid(section: .plugin) - } - + init(config: JSONValue) throws { let configObject = try AWSLocationGeoPluginConfiguration.getConfigObject(section: .plugin, - configJSON: configJSON) + configJSON: config) let regionName = try AWSLocationGeoPluginConfiguration.getRegion(configObject) var maps = [String: Geo.MapStyle]() @@ -60,6 +60,43 @@ public struct AWSLocationGeoPluginConfiguration { defaultSearchIndex: defaultSearchIndex, searchIndices: searchIndices) } + + init(config: AmplifyOutputsData) throws { + guard let geo = config.geo else { + throw GeoPluginConfigError.configurationInvalid(section: .plugin) + } + + var maps = [String: Geo.MapStyle]() + var defaultMap: String? + if let geoMaps = geo.maps { + maps = try AWSLocationGeoPluginConfiguration.getMaps( + mapConfig: geoMaps, + regionName: geo.awsRegion) + defaultMap = geoMaps.default + + // Validate that the default map exists in `maps` + guard let map = defaultMap, maps[map] != nil else { + throw GeoPluginConfigError.mapDefaultNotFound(mapName: defaultMap) + } + } + + var searchIndices = [String]() + var defaultSearchIndex: String? + // Validate that the default search index exists in `searchIndices` + if let geoSearchIndices = geo.searchIndices { + searchIndices = geoSearchIndices.items + defaultSearchIndex = geoSearchIndices.default + guard searchIndices.contains(geoSearchIndices.default) else { + throw GeoPluginConfigError.searchDefaultNotFound(indexName: geoSearchIndices.default) + } + } + + self.init(regionName: geo.awsRegion, + defaultMap: defaultMap, + maps: maps, + defaultSearchIndex: defaultSearchIndex, + searchIndices: searchIndices) + } init(regionName: String, defaultMap: String?, @@ -163,8 +200,8 @@ public struct AWSLocationGeoPluginConfiguration { throw GeoPluginConfigError.mapStyleIsNotString(mapName: mapName) } - let urlString = "https://maps.geo.\(regionName).amazonaws.com/maps/v0/maps/\(mapName)/style-descriptor" - let url = URL(string: urlString) + let url = URL(string: AWSLocationGeoPluginConfiguration.urlString(regionName: regionName, + mapName: mapName)) guard let styleURL = url else { throw GeoPluginConfigError.mapStyleURLInvalid(mapName: mapName) } @@ -177,4 +214,19 @@ public struct AWSLocationGeoPluginConfiguration { return mapStyles } + + private static func getMaps(mapConfig: AmplifyOutputsData.Geo.Maps, + regionName: String) throws -> [String: Geo.MapStyle] { + let mapTuples: [(String, Geo.MapStyle)] = try mapConfig.items.map { map in + let url = URL(string: AWSLocationGeoPluginConfiguration.urlString(regionName: regionName, + mapName: map.key)) + guard let styleURL = url else { + throw GeoPluginConfigError.mapStyleURLInvalid(mapName: map.key) + } + let mapStyle = Geo.MapStyle.init(mapName: map.key, style: map.value.style, styleURL: styleURL) + return (map.key, mapStyle) + } + + return Dictionary(uniqueKeysWithValues: mapTuples) + } } diff --git a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/Configuration/GeoPluginConfigError.swift b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/Configuration/GeoPluginConfigError.swift index ea23fb628c..749533765c 100644 --- a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/Configuration/GeoPluginConfigError.swift +++ b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/Configuration/GeoPluginConfigError.swift @@ -14,7 +14,7 @@ struct GeoPluginConfigError { static func configurationInvalid(section: AWSLocationGeoPluginConfiguration.Section) -> PluginError { PluginError.pluginConfigurationError( "Unable to decode \(section.key) configuration.", - "Make sure the \(section.key) configuration is a JSONValue." + "Make sure the \(section.key) configuration is valid JSON." ) } diff --git a/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/AWSLocationGeoPluginConfigureTests.swift b/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/AWSLocationGeoPluginConfigureTests.swift index 4629499672..f534374b07 100644 --- a/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/AWSLocationGeoPluginConfigureTests.swift +++ b/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/AWSLocationGeoPluginConfigureTests.swift @@ -34,6 +34,21 @@ class AWSLocationGeoPluginConfigureTests: AWSLocationGeoPluginTestBase { } } + func testConfigureAmplifyOutputsSuccess() async { + let resettable = geoPlugin as Resettable + await resettable.reset() + + do { + try geoPlugin.configure(using: GeoPluginTestConfig.geoPluginConfigAmplifyOutputs) + + XCTAssertNotNil(geoPlugin.locationService) + XCTAssertNotNil(geoPlugin.authService) + XCTAssertNotNil(geoPlugin.pluginConfig) + } catch { + XCTFail("Failed to configure geo plugin with error: \(error)") + } + } + func testConfigureFailureForNilConfiguration() throws { let plugin = AWSLocationGeoPlugin() do { diff --git a/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Configuration/AWSLocationGeoPluginAmplifyOutputsConfigurationTests.swift b/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Configuration/AWSLocationGeoPluginAmplifyOutputsConfigurationTests.swift new file mode 100644 index 0000000000..70e30ab8b4 --- /dev/null +++ b/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Configuration/AWSLocationGeoPluginAmplifyOutputsConfigurationTests.swift @@ -0,0 +1,143 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable @_spi(InternalAmplifyConfiguration) import Amplify +import XCTest + +@testable import AWSLocationGeoPlugin + +class AWSLocationGeoPluginAmplifyOutputsConfigurationTests: XCTestCase { + + func testConfigureSuccessAll() throws { + do { + let config = try AWSLocationGeoPluginConfiguration( + config: GeoPluginTestConfig.geoPluginConfigAmplifyOutputs) + XCTAssertNotNil(config) + XCTAssertEqual(config.regionName, GeoPluginTestConfig.regionName) + XCTAssertEqual(config.maps, GeoPluginTestConfig.maps) + XCTAssertEqual(config.defaultMap, GeoPluginTestConfig.map) + XCTAssertEqual(config.searchIndices, GeoPluginTestConfig.searchIndices) + XCTAssertEqual(config.defaultSearchIndex, GeoPluginTestConfig.searchIndex) + } catch { + XCTFail("Failed to instantiate geo plugin configuration") + } + } + + func testConfigureSuccessEmpty() throws { + let config = AmplifyOutputsData( + geo: .init(awsRegion: GeoPluginTestConfig.regionName)) + do { + let config = try AWSLocationGeoPluginConfiguration(config: config) + XCTAssertNotNil(config) + XCTAssertEqual(config.regionName, GeoPluginTestConfig.regionName) + XCTAssertTrue(config.maps.isEmpty) + XCTAssertNil(config.defaultMap) + XCTAssertTrue(config.searchIndices.isEmpty) + XCTAssertNil(config.defaultSearchIndex) + } catch { + XCTFail("Failed to instantiate geo plugin configuration") + } + } + + func testConfigureSuccessOnlyMaps() throws { + let config = AmplifyOutputsData( + geo: .init( + awsRegion: GeoPluginTestConfig.regionName, + maps: .init( + items: [GeoPluginTestConfig.map: .init(style: GeoPluginTestConfig.style)], + default: GeoPluginTestConfig.map))) + do { + let config = try AWSLocationGeoPluginConfiguration(config: config) + XCTAssertNotNil(config) + XCTAssertEqual(config.regionName, GeoPluginTestConfig.regionName) + XCTAssertEqual(config.maps, GeoPluginTestConfig.maps) + XCTAssertEqual(config.defaultMap, GeoPluginTestConfig.map) + XCTAssertTrue(config.searchIndices.isEmpty) + XCTAssertNil(config.defaultSearchIndex) + } catch { + XCTFail("Failed to instantiate geo plugin configuration") + } + } + + func testConfigureSuccessOnlySearch() throws { + let config = AmplifyOutputsData( + geo: .init( + awsRegion: GeoPluginTestConfig.regionName, + searchIndices: .init( + items: [GeoPluginTestConfig.searchIndex], + default: GeoPluginTestConfig.searchIndex))) + + do { + let config = try AWSLocationGeoPluginConfiguration(config: config) + XCTAssertNotNil(config) + XCTAssertEqual(config.regionName, GeoPluginTestConfig.regionName) + XCTAssertTrue(config.maps.isEmpty) + XCTAssertNil(config.defaultMap) + XCTAssertEqual(config.searchIndices, GeoPluginTestConfig.searchIndices) + XCTAssertEqual(config.defaultSearchIndex, GeoPluginTestConfig.searchIndex) + } catch { + XCTFail("Failed to instantiate geo plugin configuration") + } + } + + func testConfigureThrowsErrorForMissingGeoCategory() { + let config = AmplifyOutputsData(geo: nil) + + XCTAssertThrowsError(try AWSLocationGeoPluginConfiguration(config: config)) { error in + guard case let PluginError.pluginConfigurationError(errorDescription, _, _) = error else { + XCTFail("Expected PluginError pluginConfigurationError, got: \(error)") + return + } + XCTAssertEqual(errorDescription, + GeoPluginConfigError.configurationInvalid(section: .plugin).errorDescription) + } + } + + /// - Given: geo plugin configuration + /// - When: the object initializes missing default map + /// - Then: the configuration fails to initialize with mapDefaultNotFound error + func testConfigureThrowsErrorForDefaultMapNotFound() { + let map = "missingMapName" + + let config = AmplifyOutputsData( + geo: .init( + awsRegion: GeoPluginTestConfig.regionName, + maps: .init( + items: [GeoPluginTestConfig.map: .init(style: GeoPluginTestConfig.style)], + default: map))) + + XCTAssertThrowsError(try AWSLocationGeoPluginConfiguration(config: config)) { error in + guard case let PluginError.pluginConfigurationError(errorDescription, _, _) = error else { + XCTFail("Expected PluginError pluginConfigurationError, got: \(error)") + return + } + XCTAssertEqual(errorDescription, + GeoPluginConfigError.mapDefaultNotFound(mapName: map).errorDescription) + } + } + + /// - Given: geo plugin configuration + /// - When: the object initializes missing default search + /// - Then: the configuration fails to initialize with searchDefaultNotFound error + func testConfigureThrowsErrorForDefaultSearchIndexNotFound() { + let searchIndex = "missingSearchIndex" + let config = AmplifyOutputsData( + geo: .init( + awsRegion: GeoPluginTestConfig.regionName, + maps: nil, + searchIndices: .init(items: [GeoPluginTestConfig.searchIndex], default: searchIndex))) + + XCTAssertThrowsError(try AWSLocationGeoPluginConfiguration(config: config)) { error in + guard case let PluginError.pluginConfigurationError(errorDescription, _, _) = error else { + XCTFail("Expected PluginError pluginConfigurationError, got: \(error)") + return + } + XCTAssertEqual(errorDescription, + GeoPluginConfigError.searchDefaultNotFound(indexName: searchIndex).errorDescription) + } + } +} diff --git a/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Configuration/AWSLocationGeoPluginConfigurationTests.swift b/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Configuration/AWSLocationGeoPluginConfigurationTests.swift index 0d69798866..8118fc2fa0 100644 --- a/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Configuration/AWSLocationGeoPluginConfigurationTests.swift +++ b/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Configuration/AWSLocationGeoPluginConfigurationTests.swift @@ -79,19 +79,6 @@ class AWSLocationGeoPluginConfigurationTests: XCTestCase { } } - func testConfigureThrowsErrorForMissingConfigurationObject() { - let geoPluginConfig: Any? = nil - - XCTAssertThrowsError(try AWSLocationGeoPluginConfiguration(config: geoPluginConfig)) { error in - guard case let PluginError.pluginConfigurationError(errorDescription, _, _) = error else { - XCTFail("Expected PluginError pluginConfigurationError, got: \(error)") - return - } - XCTAssertEqual(errorDescription, - GeoPluginConfigError.configurationInvalid(section: .plugin).errorDescription) - } - } - func testConfigureThrowsErrorForInvalidConfigurationObject() { let geoPluginConfig = JSONValue(stringLiteral: "notADictionaryLiteral") diff --git a/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Support/Constants/GeoPluginTestConfig.swift b/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Support/Constants/GeoPluginTestConfig.swift index ad5a067e85..cf11c975e3 100644 --- a/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Support/Constants/GeoPluginTestConfig.swift +++ b/AmplifyPlugins/Geo/Tests/AWSLocationGeoPluginTests/Support/Constants/GeoPluginTestConfig.swift @@ -6,7 +6,7 @@ // import Foundation -import Amplify +@testable @_spi(InternalAmplifyConfiguration) import Amplify @testable import AWSLocationGeoPlugin struct GeoPluginTestConfig { @@ -55,4 +55,11 @@ struct GeoPluginTestConfig { (AWSLocationGeoPluginConfiguration.Node.region.key, regionJSON), (AWSLocationGeoPluginConfiguration.Section.maps.key, mapsConfigJSON), (AWSLocationGeoPluginConfiguration.Section.searchIndices.key, searchConfigJSON)) + + static let geoPluginConfigAmplifyOutputs = AmplifyOutputsData( + geo: .init( + awsRegion: regionName, + maps: .init(items: [map: .init(style: style)], default: map), + searchIndices: .init(items: [searchIndex], default: searchIndex), + geofenceCollections: nil)) } diff --git a/AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/AWSLocationGeoPluginGen2IntegrationTests.xctestplan b/AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/AWSLocationGeoPluginGen2IntegrationTests.xctestplan new file mode 100644 index 0000000000..0943053315 --- /dev/null +++ b/AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/AWSLocationGeoPluginGen2IntegrationTests.xctestplan @@ -0,0 +1,28 @@ +{ + "configurations" : [ + { + "id" : "49A89DBA-18FF-47BC-BF6B-90B7DBA9D44B", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "commandLineArgumentEntries" : [ + { + "argument" : "GEN2" + } + ] + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:GeoHostApp.xcodeproj", + "identifier" : "21F762CD2BD6B3A10048845A", + "name" : "AWSLocationGeoPluginGen2IntegrationTests" + } + } + ], + "version" : 1 +} diff --git a/AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/AWSLocationGeoPluginIntegrationTests.swift b/AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/AWSLocationGeoPluginIntegrationTests.swift index 2c4a2508ed..682ef7f4be 100644 --- a/AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/AWSLocationGeoPluginIntegrationTests.swift +++ b/AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/AWSLocationGeoPluginIntegrationTests.swift @@ -17,14 +17,25 @@ class AWSLocationGeoPluginIntergrationTests: XCTestCase { let searchText = "coffee shop" let coordinates = Geo.Coordinates(latitude: 39.7392, longitude: -104.9903) let amplifyConfigurationFile = "testconfiguration/AWSLocationGeoPluginIntegrationTests-amplifyconfiguration" + let amplifyOutputsFile = "testconfiguration/AWSLocationGeoPluginIntegrationTests-amplify_outputs" + + var useGen2Configuration: Bool { + ProcessInfo.processInfo.arguments.contains("GEN2") + } override func setUp() { continueAfterFailure = false do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: AWSLocationGeoPlugin()) - let configuration = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: amplifyConfigurationFile) - try Amplify.configure(configuration) + + if useGen2Configuration { + let data = try TestConfigHelper.retrieve(forResource: amplifyOutputsFile) + try Amplify.configure(with: .data(data)) + } else { + let configuration = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: amplifyConfigurationFile) + try Amplify.configure(configuration) + } } catch { XCTFail("Failed to initialize and configure Amplify: \(error)") } diff --git a/AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/README.md b/AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/README.md index ef8f754f3e..3b094fcdcb 100644 --- a/AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/README.md +++ b/AmplifyPlugins/Geo/Tests/GeoHostApp/AWSLocationGeoPluginIntegrationTests/README.md @@ -1,4 +1,6 @@ -## Geo Integration Tests +# Geo Integration Tests + +## Schema: AWSLocationGeoPluginIntegrationTests The following steps demonstrate how to set up Geo. Auth category is also required to allow unauthenticated and authenticated access. @@ -35,3 +37,180 @@ The following steps demonstrate how to set up Geo. Auth category is also require 5. Copy `amplifyconfiguration.json` to a new file named `AWSLocationGeoPluginIntegrationTests-amplifyconfiguration.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/`. 6. You can now run all of the integration tests. + +## Schema: AWSLocationGeoPluginGen2IntegrationTests + +The following steps demonstrate how to set up Geo and Auth using Amplify CLI Gen2. + +### Set-up + +At the time this was written, it follows the steps from here https://docs.amplify.aws/gen2/deploy-and-host/fullstack-branching/mono-and-multi-repos/ + +1. From a new folder, run `npm create amplify@beta`. This uses the following versions of the Amplify CLI, see `package.json` file below. + +```json +{ + ... + "devDependencies": { + "@aws-amplify/backend": "^0.13.0-beta.14", + "@aws-amplify/backend-cli": "^0.12.0-beta.16", + "aws-cdk": "^2.134.0", + "aws-cdk-lib": "^2.134.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "tsx": "^4.7.1", + "typescript": "^5.4.3" + }, + "dependencies": { + "aws-amplify": "^6.0.25" + } +} + +``` +2. Update `amplify/auth/resource.ts`. The resulting file should look like this + +```ts +import { defineAuth, defineFunction } from '@aws-amplify/backend'; + +/** + * Define and configure your auth resource + * @see https://docs.amplify.aws/gen2/build-a-backend/auth + */ +export const auth = defineAuth({ + loginWith: { + email: true + }, + triggers: { + // configure a trigger to point to a function definition + preSignUp: defineFunction({ + entry: './pre-sign-up-handler.ts' + }) + } +}); + +``` + +```ts +import type { PreSignUpTriggerHandler } from 'aws-lambda'; + +export const handler: PreSignUpTriggerHandler = async (event) => { + // your code here + event.response.autoConfirmUser = true + return event; +}; +``` + +3. Update `amplify/backend.ts` to create the analytics stack (https://docs.amplify.aws/gen2/build-a-backend/add-aws-services/geo/) + +Add the following imports + +```ts +import { CfnMap } from "aws-cdk-lib/aws-location"; +``` + +Create `backend` const + +```ts +const backend = defineBackend({ + auth, + // data, + // storage + // additional resource +}); +``` + + +Add the remaining code + +```ts + +const geoStack = backend.createStack("geo-stack"); + +// create a location services map +const map = new CfnMap(geoStack, "Map", { + mapName: "myMap", + description: "Map", + configuration: { + style: "VectorEsriNavigation", + }, + pricingPlan: "RequestBasedUsage", + tags: [ + { + key: "name", + value: "myMap", + }, + ], +}); + +// create an IAM policy to allow interacting with geo resource +const myGeoPolicy = new Policy(geoStack, "AuthenticatedUserIamRolePolicy", { + policyName: "GeoPolicy", + statements: [ + new PolicyStatement({ + actions: [ + "geo:GetMapTile", + "geo:GetMapSprites", + "geo:GetMapGlyphs", + "geo:GetMapStyleDescriptor", + ], + resources: [map.attrArn], + }), + ], +}); + +// apply the policy to the authenticated and unauthenticated roles +backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(myGeoPolicy); +backend.auth.resources.unauthenticatedUserIamRole.attachInlinePolicy(myGeoPolicy); + +// patch the custom map resource to the expected output configuration +backend.addOutput({ + geo: { + aws_region: Stack.of(geoStack).region, + maps: { + items: { + [map.mapName]: { + style: "VectorEsriNavigation", + }, + }, + default: map.mapName, + } + }, +}); +``` + +4. Deploy the backend with npx amplify sandbox + +For example, this deploys to a sandbox env and generates the amplify_outputs.json file. + +``` +npx amplify sandbox --config-out-dir ./config --config-version 1 --profile [PROFILE] +``` + +5. Copy the `amplify_outputs.json` file over to the test directory as `AWSLocationGeoPluginIntegrationTests-amplify_outputs.json`. The tests will automatically pick this file up. Create the directories in this path first if it currently doesn't exist. + +``` +cp amplify_outputs.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSLocationGeoPluginIntegrationTests-amplify_outputs.json +``` + +### Deploying from a branch (Optional) + +If you want to be able utilize Git commits for deployments + +1. Commit and push the files to a git repository. + +2. Navigate to the AWS Amplify console (https://us-east-1.console.aws.amazon.com/amplify/home?region=us-east-1#/) + +3. Click on "Try Amplify Gen 2" button. + +4. Choose "Option 2: Start with an existing app", and choose Github, and press Next. + +5. Find the repository and branch, and click Next + +6. Click "Save and deploy" and wait for deployment to finish. + +7. Generate the `amplify_outputs.json` configuration file + +``` +npx amplify generate config --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 +``` + diff --git a/AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/project.pbxproj index f3edf953b4..d5fc06bfce 100644 --- a/AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 21F762D12BD6B3A10048845A /* AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978B1D5E29515DEF0079E55A /* AsyncTesting.swift */; }; + 21F762D22BD6B3A10048845A /* AsyncExpectation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978B1D5F29515DEF0079E55A /* AsyncExpectation.swift */; }; + 21F762D32BD6B3A10048845A /* XCTestCase+AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978B1D6029515DEF0079E55A /* XCTestCase+AsyncTesting.swift */; }; + 21F762D42BD6B3A10048845A /* AWSLocationGeoPluginIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DB824628233A1D00FC2228 /* AWSLocationGeoPluginIntegrationTests.swift */; }; + 21F762D52BD6B3A10048845A /* TestConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DB82542823466800FC2228 /* TestConfigHelper.swift */; }; + 21F762D82BD6B3A10048845A /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 97DB8244282339D200FC2228 /* README.md */; }; 685777D92A3CC0AB001CE5C1 /* Amplify in Frameworks */ = {isa = PBXBuildFile; productRef = 685777D82A3CC0AB001CE5C1 /* Amplify */; }; 685777DB2A3CC0AB001CE5C1 /* AWSLocationGeoPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 685777DA2A3CC0AB001CE5C1 /* AWSLocationGeoPlugin */; }; 685777DD2A3CC0B0001CE5C1 /* AWSCognitoAuthPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 685777DC2A3CC0B0001CE5C1 /* AWSCognitoAuthPlugin */; }; @@ -43,6 +49,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 21F762CF2BD6B3A10048845A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97AD222628230B98001AFCC1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97AD222D28230B98001AFCC1; + remoteInfo = GeoHostApp; + }; 685778082A3CC0E1001CE5C1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97AD222628230B98001AFCC1 /* Project object */; @@ -67,6 +80,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 21F762DD2BD6B3A10048845A /* AWSLocationGeoPluginGen2IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSLocationGeoPluginGen2IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 21F762DE2BD6B3CE0048845A /* AWSLocationGeoPluginGen2IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AWSLocationGeoPluginGen2IntegrationTests.xctestplan; sourceTree = ""; }; 685777C32A3CC08B001CE5C1 /* GeoWatchApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GeoWatchApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 685778072A3CC0D8001CE5C1 /* AWSLocationGeoPluginIntegrationTestsWatch.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSLocationGeoPluginIntegrationTestsWatch.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 978B1D5E29515DEF0079E55A /* AsyncTesting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncTesting.swift; sourceTree = ""; }; @@ -88,6 +103,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 21F762D62BD6B3A10048845A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 685777C02A3CC08B001CE5C1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -175,6 +197,7 @@ 97914B8E295570E1002000EA /* GeoStressTests.xctest */, 685777C32A3CC08B001CE5C1 /* GeoWatchApp.app */, 685778072A3CC0D8001CE5C1 /* AWSLocationGeoPluginIntegrationTestsWatch.xctest */, + 21F762DD2BD6B3A10048845A /* AWSLocationGeoPluginGen2IntegrationTests.xctest */, ); name = Products; sourceTree = ""; @@ -202,6 +225,7 @@ 97DB823C282339B700FC2228 /* AWSLocationGeoPluginIntegrationTests */ = { isa = PBXGroup; children = ( + 21F762DE2BD6B3CE0048845A /* AWSLocationGeoPluginGen2IntegrationTests.xctestplan */, 97DB824628233A1D00FC2228 /* AWSLocationGeoPluginIntegrationTests.swift */, 97DB82542823466800FC2228 /* TestConfigHelper.swift */, 97DB8244282339D200FC2228 /* README.md */, @@ -227,6 +251,25 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 21F762CD2BD6B3A10048845A /* AWSLocationGeoPluginGen2IntegrationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 21F762DA2BD6B3A10048845A /* Build configuration list for PBXNativeTarget "AWSLocationGeoPluginGen2IntegrationTests" */; + buildPhases = ( + 21F762D02BD6B3A10048845A /* Sources */, + 21F762D62BD6B3A10048845A /* Frameworks */, + 21F762D72BD6B3A10048845A /* Resources */, + 21F762D92BD6B3A10048845A /* Copy Configuration Folder */, + ); + buildRules = ( + ); + dependencies = ( + 21F762CE2BD6B3A10048845A /* PBXTargetDependency */, + ); + name = AWSLocationGeoPluginGen2IntegrationTests; + productName = AWSLocationGeoPluginIntegrationTests; + productReference = 21F762DD2BD6B3A10048845A /* AWSLocationGeoPluginGen2IntegrationTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 685777C22A3CC08B001CE5C1 /* GeoWatchApp */ = { isa = PBXNativeTarget; buildConfigurationList = 685777D62A3CC08C001CE5C1 /* Build configuration list for PBXNativeTarget "GeoWatchApp" */; @@ -377,11 +420,20 @@ 97914B7E295570E1002000EA /* GeoStressTests */, 685777C22A3CC08B001CE5C1 /* GeoWatchApp */, 685777F72A3CC0D8001CE5C1 /* AWSLocationGeoPluginIntegrationTestsWatch */, + 21F762CD2BD6B3A10048845A /* AWSLocationGeoPluginGen2IntegrationTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 21F762D72BD6B3A10048845A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 21F762D82BD6B3A10048845A /* README.md in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 685777C12A3CC08B001CE5C1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -425,6 +477,24 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 21F762D92BD6B3A10048845A /* Copy Configuration Folder */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy Configuration Folder"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "TEMP_FILE=$HOME/.aws-amplify/amplify-ios/testconfiguration/.\nDEST_PATH=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/testconfiguration/\"\n\nif [[ ! -d $TEMP_FILE ]] ; then\n echo \"${TEMP_FILE} does not exist. Using empty configuration.\"\n exit 0\nfi\n \nif [[ -f $DEST_PATH ]] ; then\n rm $DEST_PATH\nfi\n \ncp -r $TEMP_FILE $DEST_PATH\n"; + }; 685778032A3CC0D8001CE5C1 /* Copy Configuration Folder */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -482,6 +552,18 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 21F762D02BD6B3A10048845A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 21F762D12BD6B3A10048845A /* AsyncTesting.swift in Sources */, + 21F762D22BD6B3A10048845A /* AsyncExpectation.swift in Sources */, + 21F762D32BD6B3A10048845A /* XCTestCase+AsyncTesting.swift in Sources */, + 21F762D42BD6B3A10048845A /* AWSLocationGeoPluginIntegrationTests.swift in Sources */, + 21F762D52BD6B3A10048845A /* TestConfigHelper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 685777BF2A3CC08B001CE5C1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -539,6 +621,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 21F762CE2BD6B3A10048845A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97AD222D28230B98001AFCC1 /* GeoHostApp */; + targetProxy = 21F762CF2BD6B3A10048845A /* PBXContainerItemProxy */; + }; 685778092A3CC0E1001CE5C1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 685777C22A3CC08B001CE5C1 /* GeoWatchApp */; @@ -557,6 +644,59 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 21F762DB2BD6B3A10048845A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazon.com.AWSLocationGeoPluginIntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GeoHostApp.app/GeoHostApp"; + }; + name = Debug; + }; + 21F762DC2BD6B3A10048845A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazon.com.AWSLocationGeoPluginIntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GeoHostApp.app/GeoHostApp"; + }; + name = Release; + }; 685777D42A3CC08C001CE5C1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -959,6 +1099,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 21F762DA2BD6B3A10048845A /* Build configuration list for PBXNativeTarget "AWSLocationGeoPluginGen2IntegrationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 21F762DB2BD6B3A10048845A /* Debug */, + 21F762DC2BD6B3A10048845A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 685777D62A3CC08C001CE5C1 /* Build configuration list for PBXNativeTarget "GeoWatchApp" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/xcshareddata/xcschemes/AWSLocationGeoPluginGen2IntegrationTests.xcscheme b/AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/xcshareddata/xcschemes/AWSLocationGeoPluginGen2IntegrationTests.xcscheme new file mode 100644 index 0000000000..e3fe4df89e --- /dev/null +++ b/AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/xcshareddata/xcschemes/AWSLocationGeoPluginGen2IntegrationTests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/xcshareddata/xcschemes/AWSLocationGeoPluginIntegrationTests.xcscheme b/AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/xcshareddata/xcschemes/AWSLocationGeoPluginIntegrationTests.xcscheme index bee577e0da..a4f42287d2 100644 --- a/AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/xcshareddata/xcschemes/AWSLocationGeoPluginIntegrationTests.xcscheme +++ b/AmplifyPlugins/Geo/Tests/GeoHostApp/GeoHostApp.xcodeproj/xcshareddata/xcschemes/AWSLocationGeoPluginIntegrationTests.xcscheme @@ -16,7 +16,7 @@ skipped = "NO"> diff --git a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Configuration/AWSPinpointPluginConfiguration.swift b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Configuration/AWSPinpointPluginConfiguration.swift index 7282dedf04..6c2969fa3d 100644 --- a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Configuration/AWSPinpointPluginConfiguration.swift +++ b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Configuration/AWSPinpointPluginConfiguration.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import AWSPinpoint import AWSClientRuntime import Foundation @@ -32,8 +32,8 @@ public struct AWSPinpointPluginConfiguration { ) } - private init(appId: String, - region: String) { + public init(appId: String, + region: String) { self.appId = appId self.region = region } diff --git a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginGen2IntegrationTests.xctestplan b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginGen2IntegrationTests.xctestplan new file mode 100644 index 0000000000..4eee184d86 --- /dev/null +++ b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginGen2IntegrationTests.xctestplan @@ -0,0 +1,33 @@ +{ + "configurations" : [ + { + "id" : "40B1C89B-1478-4A47-957B-CF7489CED04C", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "commandLineArgumentEntries" : [ + { + "argument" : "GEN2" + } + ] + }, + "testTargets" : [ + { + "skippedTests" : [ + "AWSCloudWatchLoggingPluginIntergrationTests\/testFlushLogWithMessages()", + "AWSCloudWatchLoggingPluginIntergrationTests\/testFlushLogWithVerboseMessageAfterDisablingPlugin()", + "AWSCloudWatchLoggingPluginIntergrationTests\/testFlushLogWithVerboseMessageAfterEnablingPlugin()" + ], + "target" : { + "containerPath" : "container:CloudWatchLoggingHostApp.xcodeproj", + "identifier" : "21F762DF2BD6B55F0048845A", + "name" : "AWSCloudWatchLoggingPluginGen2IntegrationTests" + } + } + ], + "version" : 1 +} diff --git a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginIntegrationTests.swift b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginIntegrationTests.swift index 98668ce038..77448d5fa3 100644 --- a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginIntegrationTests.swift +++ b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginIntegrationTests.swift @@ -13,6 +13,7 @@ import AWSCloudWatchLogs class AWSCloudWatchLoggingPluginIntergrationTests: XCTestCase { let amplifyConfigurationFile = "testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration" + let amplifyOutputsFile = "testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplify_outputs" #if os(tvOS) let amplifyConfigurationLoggingFile = "testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration_logging_tvOS" #elseif os(watchOS) @@ -22,6 +23,10 @@ class AWSCloudWatchLoggingPluginIntergrationTests: XCTestCase { #endif var loggingConfiguration: AWSCloudWatchLoggingPluginConfiguration? + var useGen2Configuration: Bool { + ProcessInfo.processInfo.arguments.contains("GEN2") + } + override func setUp() async throws { continueAfterFailure = false do { @@ -30,8 +35,15 @@ class AWSCloudWatchLoggingPluginIntergrationTests: XCTestCase { loggingConfiguration = try AWSCloudWatchLoggingPluginConfiguration.loadConfiguration(from: loggingConfigurationFile) let loggingPlugin = AWSCloudWatchLoggingPlugin(loggingPluginConfiguration: loggingConfiguration) try Amplify.add(plugin: loggingPlugin) - let configuration = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: amplifyConfigurationFile) - try Amplify.configure(configuration) + + if useGen2Configuration { + let data = try TestConfigHelper.retrieve(forResource: amplifyOutputsFile) + try Amplify.configure(with: .data(data)) + } else { + let configuration = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: amplifyConfigurationFile) + try Amplify.configure(configuration) + } + try await Task.sleep(seconds: 5) } catch { XCTFail("Failed to initialize and configure Amplify: \(error)") diff --git a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/README.md b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/README.md index fa4c128b6c..31956367b8 100644 --- a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/README.md +++ b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/README.md @@ -1,15 +1,110 @@ -## AWS CloudWatch Logging Integration Tests +# AWS CloudWatch Logging Integration Tests + +## Schema: AWSCloudWatchLoggingPluginIntegrationTests The following steps demonstrate how to set up Logging. Auth category is also required to allow unauthenticated and authenticated access. ### Set-up -1. Configure app with Auth category +1. Configure app with Auth category using Amplify CLI 2. Copy `amplifyconfiguration.json` to a new file named `AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/`. -3. Configure the `amplifyconfiguration-logging.json` file +3. Configure the `amplifyconfiguration-logging.json` file (https://docs.amplify.aws/swift/build-a-backend/more-features/logging/set-up-logging/#initialize-amplify-logging) 4. Copy `amplifyconfiguration-logging.json` to a new file named `AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration-logging.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/`. -3. You can now run all of the integration tests. +5. You can now run all of the integration tests. + +## Schema: AWSCloudWatchLoggingPluginGen2IntegrationTests + +The following steps demonstrate how to set up Logging. Auth category is also required to allow unauthenticated and authenticated access. + +### Set-up + +At the time this was written, it follows the steps from here https://docs.amplify.aws/gen2/deploy-and-host/fullstack-branching/mono-and-multi-repos/ + +1. From a new folder, run `npm create amplify@beta`. This uses the following versions of the Amplify CLI, see `package.json` file below. + +```json +{ + ... + "devDependencies": { + "@aws-amplify/backend": "^0.13.0-beta.14", + "@aws-amplify/backend-cli": "^0.12.0-beta.16", + "aws-cdk": "^2.134.0", + "aws-cdk-lib": "^2.134.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "tsx": "^4.7.1", + "typescript": "^5.4.3" + }, + "dependencies": { + "aws-amplify": "^6.0.25" + } +} + +``` + +2. Update `amplify/auth/resource.ts`. The resulting file should look like this + +```ts +import { defineAuth, defineFunction } from '@aws-amplify/backend'; + +/** + * Define and configure your auth resource + * @see https://docs.amplify.aws/gen2/build-a-backend/auth + */ +export const auth = defineAuth({ + loginWith: { + email: true + }, + triggers: { + // configure a trigger to point to a function definition + preSignUp: defineFunction({ + entry: './pre-sign-up-handler.ts' + }) + } +}); + +``` + +```ts +import type { PreSignUpTriggerHandler } from 'aws-lambda'; + +export const handler: PreSignUpTriggerHandler = async (event) => { + // your code here + event.response.autoConfirmUser = true + return event; +}; +``` + +3. Commit and push the files to a git repository. + +4. Navigate to the AWS Amplify console (https://us-east-1.console.aws.amazon.com/amplify/home?region=us-east-1#/) + +5. Click on "Try Amplify Gen 2" button. + +6. Choose "Option 2: Start with an existing app", and choose Github, and press Next. + +7. Find the repository and branch, and click Next + +8. Click "Save and deploy" and wait for deployment to finish. + +9. Generate the `amplify_outputs.json` configuration file + +``` +npx amplify generate config --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 +``` + +10. Copy the `amplify_outputs.json` file over to the test directory as `AWSCloudWatchLoggingPluginIntegrationTests-amplify_outputs.json`. The tests will automatically pick this file up. Create the directories in this path first if it currently doesn't exist. + +``` +cp amplify_outputs.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplify_outputs.json +``` + +11. Configure the `amplifyconfiguration-logging.json` file (https://docs.amplify.aws/swift/build-a-backend/more-features/logging/set-up-logging/#initialize-amplify-logging) + +12. Copy `amplifyconfiguration-logging.json` to a new file named `AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration-logging.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/`. + +13. You can now run all of the integration tests. diff --git a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/CloudWatchLoggingHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/CloudWatchLoggingHostApp.xcodeproj/project.pbxproj index 066f1c118a..04ffccb2ad 100644 --- a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/CloudWatchLoggingHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/CloudWatchLoggingHostApp.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 21F762E32BD6B55F0048845A /* AWSCloudWatchClientHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 730C2E762AAA8A4B00878E67 /* AWSCloudWatchClientHelper.swift */; }; + 21F762E42BD6B55F0048845A /* AWSCloudWatchLoggingPluginIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DB824628233A1D00FC2228 /* AWSCloudWatchLoggingPluginIntegrationTests.swift */; }; + 21F762E52BD6B55F0048845A /* TestConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DB82542823466800FC2228 /* TestConfigHelper.swift */; }; + 21F762E82BD6B55F0048845A /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 97DB8244282339D200FC2228 /* README.md */; }; 730C2E772AAA8A4B00878E67 /* AWSCloudWatchClientHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 730C2E762AAA8A4B00878E67 /* AWSCloudWatchClientHelper.swift */; }; 73578A2C2AAB945E00505FB3 /* CloudWatchLoggingApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AD223128230B98001AFCC1 /* CloudWatchLoggingApp.swift */; }; 73578A2D2AAB946300505FB3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AD223328230B98001AFCC1 /* ContentView.swift */; }; @@ -29,6 +33,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 21F762E12BD6B55F0048845A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97AD222628230B98001AFCC1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97AD222D28230B98001AFCC1; + remoteInfo = GeoHostApp; + }; 73578A362AAB94D100505FB3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97AD222628230B98001AFCC1 /* Project object */; @@ -46,6 +57,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 21F762ED2BD6B55F0048845A /* AWSCloudWatchLoggingPluginGen2IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSCloudWatchLoggingPluginGen2IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 21F762EE2BD6B5810048845A /* AWSCloudWatchLoggingPluginGen2IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AWSCloudWatchLoggingPluginGen2IntegrationTests.xctestplan; sourceTree = ""; }; 730C2E762AAA8A4B00878E67 /* AWSCloudWatchClientHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSCloudWatchClientHelper.swift; sourceTree = ""; }; 733390D32AAB8A3B006E3625 /* CloudWatchLoggingWatchApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CloudWatchLoggingWatchApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 73578A322AAB94D100505FB3 /* AWSCloudWatchLoggingPluginIntegrationTestsWatch.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSCloudWatchLoggingPluginIntegrationTestsWatch.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -62,6 +75,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 21F762E62BD6B55F0048845A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 733390D02AAB8A3B006E3625 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -117,6 +137,7 @@ 97DB823B282339B700FC2228 /* AWSCloudWatchLoggingPluginIntegrationTests.xctest */, 733390D32AAB8A3B006E3625 /* CloudWatchLoggingWatchApp.app */, 73578A322AAB94D100505FB3 /* AWSCloudWatchLoggingPluginIntegrationTestsWatch.xctest */, + 21F762ED2BD6B55F0048845A /* AWSCloudWatchLoggingPluginGen2IntegrationTests.xctest */, ); name = Products; sourceTree = ""; @@ -143,6 +164,7 @@ 97DB823C282339B700FC2228 /* AWSCloudWatchLoggingPluginIntegrationTests */ = { isa = PBXGroup; children = ( + 21F762EE2BD6B5810048845A /* AWSCloudWatchLoggingPluginGen2IntegrationTests.xctestplan */, 97DB824628233A1D00FC2228 /* AWSCloudWatchLoggingPluginIntegrationTests.swift */, 730C2E762AAA8A4B00878E67 /* AWSCloudWatchClientHelper.swift */, 97DB82542823466800FC2228 /* TestConfigHelper.swift */, @@ -169,6 +191,25 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 21F762DF2BD6B55F0048845A /* AWSCloudWatchLoggingPluginGen2IntegrationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 21F762EA2BD6B55F0048845A /* Build configuration list for PBXNativeTarget "AWSCloudWatchLoggingPluginGen2IntegrationTests" */; + buildPhases = ( + 21F762E22BD6B55F0048845A /* Sources */, + 21F762E62BD6B55F0048845A /* Frameworks */, + 21F762E72BD6B55F0048845A /* Resources */, + 21F762E92BD6B55F0048845A /* Copy Configuration Folder */, + ); + buildRules = ( + ); + dependencies = ( + 21F762E02BD6B55F0048845A /* PBXTargetDependency */, + ); + name = AWSCloudWatchLoggingPluginGen2IntegrationTests; + productName = AWSLocationGeoPluginIntegrationTests; + productReference = 21F762ED2BD6B55F0048845A /* AWSCloudWatchLoggingPluginGen2IntegrationTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 733390D22AAB8A3B006E3625 /* CloudWatchLoggingWatchApp */ = { isa = PBXNativeTarget; buildConfigurationList = 733390E62AAB8A3C006E3625 /* Build configuration list for PBXNativeTarget "CloudWatchLoggingWatchApp" */; @@ -297,11 +338,20 @@ 97DB823A282339B700FC2228 /* AWSCloudWatchLoggingPluginIntegrationTests */, 733390D22AAB8A3B006E3625 /* CloudWatchLoggingWatchApp */, 73578A312AAB94D100505FB3 /* AWSCloudWatchLoggingPluginIntegrationTestsWatch */, + 21F762DF2BD6B55F0048845A /* AWSCloudWatchLoggingPluginGen2IntegrationTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 21F762E72BD6B55F0048845A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 21F762E82BD6B55F0048845A /* README.md in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 733390D12AAB8A3B006E3625 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -336,6 +386,24 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 21F762E92BD6B55F0048845A /* Copy Configuration Folder */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy Configuration Folder"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "TEMP_FILE=$HOME/.aws-amplify/amplify-ios/testconfiguration/.\nDEST_PATH=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/testconfiguration/\"\n\nif [[ ! -d $TEMP_FILE ]] ; then\n echo \"${TEMP_FILE} does not exist. Using empty configuration.\"\n exit 0\nfi\n \nif [[ -f $DEST_PATH ]] ; then\n rm $DEST_PATH\nfi\n \ncp -r $TEMP_FILE $DEST_PATH\n"; + }; 73EB19C52ABB4669007455F5 /* Copy Configuration Folder */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -375,6 +443,16 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 21F762E22BD6B55F0048845A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 21F762E32BD6B55F0048845A /* AWSCloudWatchClientHelper.swift in Sources */, + 21F762E42BD6B55F0048845A /* AWSCloudWatchLoggingPluginIntegrationTests.swift in Sources */, + 21F762E52BD6B55F0048845A /* TestConfigHelper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 733390CF2AAB8A3B006E3625 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -416,6 +494,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 21F762E02BD6B55F0048845A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97AD222D28230B98001AFCC1 /* CloudWatchLoggingHostApp */; + targetProxy = 21F762E12BD6B55F0048845A /* PBXContainerItemProxy */; + }; 73578A372AAB94D100505FB3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 733390D22AAB8A3B006E3625 /* CloudWatchLoggingWatchApp */; @@ -429,6 +512,67 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 21F762EB2BD6B55F0048845A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W3DRXD72QU; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazon.com.AWSCloudWatchLoggingPluginIntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CloudWatchLoggingHostApp.app/CloudWatchLoggingHostApp"; + TVOS_DEPLOYMENT_TARGET = 16.4; + WATCHOS_DEPLOYMENT_TARGET = 9.4; + }; + name = Debug; + }; + 21F762EC2BD6B55F0048845A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W3DRXD72QU; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazon.com.AWSCloudWatchLoggingPluginIntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CloudWatchLoggingHostApp.app/CloudWatchLoggingHostApp"; + TVOS_DEPLOYMENT_TARGET = 16.4; + WATCHOS_DEPLOYMENT_TARGET = 9.4; + }; + name = Release; + }; 733390E42AAB8A3C006E3625 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -789,6 +933,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 21F762EA2BD6B55F0048845A /* Build configuration list for PBXNativeTarget "AWSCloudWatchLoggingPluginGen2IntegrationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 21F762EB2BD6B55F0048845A /* Debug */, + 21F762EC2BD6B55F0048845A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; 733390E62AAB8A3C006E3625 /* Build configuration list for PBXNativeTarget "CloudWatchLoggingWatchApp" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/CloudWatchLoggingHostApp.xcodeproj/xcshareddata/xcschemes/AWSCloudWatchLoggingPluginGen2IntegrationTests.xcscheme b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/CloudWatchLoggingHostApp.xcodeproj/xcshareddata/xcschemes/AWSCloudWatchLoggingPluginGen2IntegrationTests.xcscheme new file mode 100644 index 0000000000..a5d2bd74c8 --- /dev/null +++ b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/CloudWatchLoggingHostApp.xcodeproj/xcshareddata/xcschemes/AWSCloudWatchLoggingPluginGen2IntegrationTests.xcscheme @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyPlugins/Notifications/Push/Sources/AWSPinpointPushNotificationsPlugin/AWSPinpointPushNotificationsPlugin+Configure.swift b/AmplifyPlugins/Notifications/Push/Sources/AWSPinpointPushNotificationsPlugin/AWSPinpointPushNotificationsPlugin+Configure.swift index 94c765c449..7066e83b28 100644 --- a/AmplifyPlugins/Notifications/Push/Sources/AWSPinpointPushNotificationsPlugin/AWSPinpointPushNotificationsPlugin+Configure.swift +++ b/AmplifyPlugins/Notifications/Push/Sources/AWSPinpointPushNotificationsPlugin/AWSPinpointPushNotificationsPlugin+Configure.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import AmplifyUtilsNotifications import AWSPluginsCore import Foundation @@ -20,14 +20,27 @@ extension AWSPinpointPushNotificationsPlugin { /// - Throws: /// - PluginError.pluginConfigurationError: If one of the configuration values is invalid or empty public func configure(using configuration: Any?) throws { - guard let config = configuration as? JSONValue else { + let pluginConfiguration: AWSPinpointPluginConfiguration + if let config = configuration as? AmplifyOutputsData { + guard let notifications = config.notifications else { + throw PluginError.pluginConfigurationError( + PushNotificationsPluginErrorConstants.missinAmplifyOutputsPinpointNotificationsConfiguration.errorDescription, + PushNotificationsPluginErrorConstants.missinAmplifyOutputsPinpointNotificationsConfiguration.errorDescription + ) + } + + pluginConfiguration = AWSPinpointPluginConfiguration( + appId: notifications.amazonPinpointAppId, + region: notifications.awsRegion) + } else if let config = configuration as? JSONValue { + pluginConfiguration = try AWSPinpointPluginConfiguration(config) + } else { throw PluginError.pluginConfigurationError( PushNotificationsPluginErrorConstants.decodeConfigurationError.errorDescription, PushNotificationsPluginErrorConstants.decodeConfigurationError.recoverySuggestion ) } - let pluginConfiguration = try AWSPinpointPluginConfiguration(config) try configure(using: pluginConfiguration) } diff --git a/AmplifyPlugins/Notifications/Push/Sources/AWSPinpointPushNotificationsPlugin/Support/Constants/PushNotificationsPluginErrorConstants.swift b/AmplifyPlugins/Notifications/Push/Sources/AWSPinpointPushNotificationsPlugin/Support/Constants/PushNotificationsPluginErrorConstants.swift index 3860c0e0f5..2a1c5ff29a 100644 --- a/AmplifyPlugins/Notifications/Push/Sources/AWSPinpointPushNotificationsPlugin/Support/Constants/PushNotificationsPluginErrorConstants.swift +++ b/AmplifyPlugins/Notifications/Push/Sources/AWSPinpointPushNotificationsPlugin/Support/Constants/PushNotificationsPluginErrorConstants.swift @@ -26,6 +26,11 @@ struct PushNotificationsPluginErrorConstants { "Add the `PinpointPushNotifications` section to the plugin." ) + static let missinAmplifyOutputsPinpointNotificationsConfiguration: PushNotificationsPluginErrorString = ( + "Plugin is missing `notifications` category section.", + "Add the `notifications` category section in the configuration." + ) + static let deviceOffline: PushNotificationsPluginErrorString = ( "The device does not have internet access.", "Please ensure the device is online and try again." diff --git a/AmplifyPlugins/Notifications/Push/Tests/AWSPinpointPushNotificationsPluginUnitTests/AWSPinpointPushNotificationsPluginConfigureTests.swift b/AmplifyPlugins/Notifications/Push/Tests/AWSPinpointPushNotificationsPluginUnitTests/AWSPinpointPushNotificationsPluginConfigureTests.swift index 84afd80cbe..46b97b950b 100644 --- a/AmplifyPlugins/Notifications/Push/Tests/AWSPinpointPushNotificationsPluginUnitTests/AWSPinpointPushNotificationsPluginConfigureTests.swift +++ b/AmplifyPlugins/Notifications/Push/Tests/AWSPinpointPushNotificationsPluginUnitTests/AWSPinpointPushNotificationsPluginConfigureTests.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AmplifyTestCommon @_spi(InternalAWSPinpoint) @testable import InternalAWSPinpoint @testable import AWSPinpointPushNotificationsPlugin @@ -47,6 +47,16 @@ class AWSPinpointPushNotificationsPluginConfigureTests: AWSPinpointPushNotificat } } + func testConfigure_withValidAmplifyOutputsConfiguration_shouldSucceed() { + do { + try plugin.configure(using: createPushNotificationsPluginAmplifyOutputsConfig()) + XCTAssertNotNil(plugin.pinpoint) + XCTAssertEqual(plugin.options, authorizationOptions) + } catch { + XCTFail("Failed to configure Push Notifications plugin") + } + } + func testConfigure_withNotificationsPermissionsGranted_shouldRegisterForRemoteNotifications() throws { mockRemoteNotifications.mockedRequestAuthorizationResult = true mockRemoteNotifications.registerForRemoteNotificationsExpectation = expectation(description: "Permissions Granted") @@ -170,4 +180,11 @@ class AWSPinpointPushNotificationsPluginConfigureTests: AWSPinpointPushNotificat return pinpointConfiguration } + + private func createPushNotificationsPluginAmplifyOutputsConfig() -> AmplifyOutputsData { + .init(notifications: .init( + awsRegion: testRegion, + amazonPinpointAppId: testAppId, + channels: [.apns])) + } } diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationGen2HostApp.xctestplan b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationGen2HostApp.xctestplan new file mode 100644 index 0000000000..4f89e013f5 --- /dev/null +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationGen2HostApp.xctestplan @@ -0,0 +1,34 @@ +{ + "configurations" : [ + { + "id" : "AE2D8AAB-E43A-4E85-AD6E-0248BEF877FF", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "commandLineArgumentEntries" : [ + { + "argument" : "GEN2" + } + ], + "targetForVariableExpansion" : { + "containerPath" : "container:PushNotificationHostApp.xcodeproj", + "identifier" : "21F762EF2BD6B7410048845A", + "name" : "PushNotificationGen2HostApp" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:PushNotificationHostApp.xcodeproj", + "identifier" : "6084F1AB2967B87200434CBF", + "name" : "PushNotificationHostAppUITests" + } + } + ], + "version" : 1 +} diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp copy-Info.plist b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp copy-Info.plist new file mode 100644 index 0000000000..ab6894a37a --- /dev/null +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp copy-Info.plist @@ -0,0 +1,15 @@ + + + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + + + diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/project.pbxproj index 89acc762f5..ddd6cfb305 100644 --- a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/project.pbxproj @@ -7,6 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 21F762F62BD6B7410048845A /* TestConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6084F1BB2967CE5D00434CBF /* TestConfigHelper.swift */; }; + 21F762F72BD6B7410048845A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60F60D942965040600B2D13D /* ContentView.swift */; }; + 21F762F82BD6B7410048845A /* PushNotificationHostAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60F60D922965040600B2D13D /* PushNotificationHostAppApp.swift */; }; + 21F762FA2BD6B7410048845A /* Amplify in Frameworks */ = {isa = PBXBuildFile; productRef = 21F762F02BD6B7410048845A /* Amplify */; }; + 21F762FB2BD6B7410048845A /* AWSPinpointAnalyticsPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 21F762F22BD6B7410048845A /* AWSPinpointAnalyticsPlugin */; }; + 21F762FC2BD6B7410048845A /* AWSCognitoAuthPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 21F762F12BD6B7410048845A /* AWSCognitoAuthPlugin */; }; + 21F762FD2BD6B7410048845A /* AWSPinpointPushNotificationsPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 21F762F32BD6B7410048845A /* AWSPinpointPushNotificationsPlugin */; }; + 21F762FE2BD6B7410048845A /* AWSPluginsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 21F762F42BD6B7410048845A /* AWSPluginsCore */; }; + 21F763002BD6B7410048845A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 60F60D992965040800B2D13D /* Preview Assets.xcassets */; }; + 21F763012BD6B7410048845A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 60F60D962965040800B2D13D /* Assets.xcassets */; }; 5C4EA91129B91A2600ED7924 /* Amplify in Frameworks */ = {isa = PBXBuildFile; productRef = 5C4EA91029B91A2600ED7924 /* Amplify */; }; 5C4EA91329B91A2600ED7924 /* AWSCognitoAuthPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 5C4EA91229B91A2600ED7924 /* AWSCognitoAuthPlugin */; }; 5C4EA91529B91A2600ED7924 /* AWSPinpointAnalyticsPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 5C4EA91429B91A2600ED7924 /* AWSPinpointAnalyticsPlugin */; }; @@ -56,6 +66,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 21F763062BD6B7410048845A /* PushNotificationGen2HostApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PushNotificationGen2HostApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 21F763072BD6B7410048845A /* PushNotificationHostApp copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "PushNotificationHostApp copy-Info.plist"; path = "/Users/mdlaw/aws-amplify/amplify-swift/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp copy-Info.plist"; sourceTree = ""; }; + 21F763082BD6B7680048845A /* PushNotificationGen2HostApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationGen2HostApp.xctestplan; sourceTree = ""; }; 5C4EA90F29B919D800ED7924 /* amplify-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "amplify-swift"; path = ../../../../..; sourceTree = ""; }; 6084F1AC2967B87200434CBF /* PushNotificationHostAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PushNotificationHostAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 6084F1AE2967B87200434CBF /* PushNotificationHostAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationHostAppUITests.swift; sourceTree = ""; }; @@ -78,6 +91,18 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 21F762F92BD6B7410048845A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 21F762FA2BD6B7410048845A /* Amplify in Frameworks */, + 21F762FB2BD6B7410048845A /* AWSPinpointAnalyticsPlugin in Frameworks */, + 21F762FC2BD6B7410048845A /* AWSCognitoAuthPlugin in Frameworks */, + 21F762FD2BD6B7410048845A /* AWSPinpointPushNotificationsPlugin in Frameworks */, + 21F762FE2BD6B7410048845A /* AWSPluginsCore in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6084F1A92967B87200434CBF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -154,11 +179,13 @@ 68A3120C2A3D0DBA00D60A17 /* PushNotificationsWatchApp-Info.plist */, 68A3120B2A3D0AEF00D60A17 /* PushNotificationsWatchApp.entitlements */, 6875F9AA2A3CE1E9001C9AAF /* PushNotificationWatchTests.xctestplan */, + 21F763082BD6B7680048845A /* PushNotificationGen2HostApp.xctestplan */, 6079DFD22965094C00E8E9D0 /* Packages */, 60F60D912965040600B2D13D /* PushNotificationHostApp */, 6084F1AD2967B87200434CBF /* PushNotificationHostAppUITests */, 60F60D902965040600B2D13D /* Products */, 600B385C2966269B007897BD /* Frameworks */, + 21F763072BD6B7410048845A /* PushNotificationHostApp copy-Info.plist */, ); sourceTree = ""; }; @@ -169,6 +196,7 @@ 6084F1AC2967B87200434CBF /* PushNotificationHostAppUITests.xctest */, 6875F9702A3CCFCA001C9AAF /* PushNotificationsWatchApp.app */, 6875F9952A3CD258001C9AAF /* PushNotificationWatchTests.xctest */, + 21F763062BD6B7410048845A /* PushNotificationGen2HostApp.app */, ); name = Products; sourceTree = ""; @@ -198,6 +226,31 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 21F762EF2BD6B7410048845A /* PushNotificationGen2HostApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 21F763032BD6B7410048845A /* Build configuration list for PBXNativeTarget "PushNotificationGen2HostApp" */; + buildPhases = ( + 21F762F52BD6B7410048845A /* Sources */, + 21F762F92BD6B7410048845A /* Frameworks */, + 21F762FF2BD6B7410048845A /* Resources */, + 21F763022BD6B7410048845A /* Copy Test Config */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PushNotificationGen2HostApp; + packageProductDependencies = ( + 21F762F02BD6B7410048845A /* Amplify */, + 21F762F12BD6B7410048845A /* AWSCognitoAuthPlugin */, + 21F762F22BD6B7410048845A /* AWSPinpointAnalyticsPlugin */, + 21F762F32BD6B7410048845A /* AWSPinpointPushNotificationsPlugin */, + 21F762F42BD6B7410048845A /* AWSPluginsCore */, + ); + productName = PushNotificationHostApp; + productReference = 21F763062BD6B7410048845A /* PushNotificationGen2HostApp.app */; + productType = "com.apple.product-type.application"; + }; 6084F1AB2967B87200434CBF /* PushNotificationHostAppUITests */ = { isa = PBXNativeTarget; buildConfigurationList = 6084F1B42967B87300434CBF /* Build configuration list for PBXNativeTarget "PushNotificationHostAppUITests" */; @@ -331,11 +384,21 @@ 6084F1AB2967B87200434CBF /* PushNotificationHostAppUITests */, 6875F96F2A3CCFCA001C9AAF /* PushNotificationsWatchApp */, 6875F9882A3CD258001C9AAF /* PushNotificationWatchTests */, + 21F762EF2BD6B7410048845A /* PushNotificationGen2HostApp */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 21F762FF2BD6B7410048845A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 21F763002BD6B7410048845A /* Preview Assets.xcassets in Resources */, + 21F763012BD6B7410048845A /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6084F1AA2967B87200434CBF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -371,6 +434,24 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 21F763022BD6B7410048845A /* Copy Test Config */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy Test Config"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nSOURCE_DIR=$HOME/.aws-amplify/amplify-ios/testconfiguration\nDESTINATION_DIR=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/testconfiguration/\"\n\nif [ ! -d \"$SOURCE_DIR\" ]; then\n echo \"error: Test configuration directory does not exist: ${SOURCE_DIR}\" && exit 1\nfi\n\nmkdir -p \"$DESTINATION_DIR\"\ncp -r \"$SOURCE_DIR\"/*.json $DESTINATION_DIR\n\nexit 0\n"; + }; 6084F1BA2967CB3000434CBF /* Copy Test Config */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -410,6 +491,16 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 21F762F52BD6B7410048845A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 21F762F62BD6B7410048845A /* TestConfigHelper.swift in Sources */, + 21F762F72BD6B7410048845A /* ContentView.swift in Sources */, + 21F762F82BD6B7410048845A /* PushNotificationHostAppApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6084F1A82967B87200434CBF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -466,6 +557,76 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 21F763042BD6B7410048845A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = PushNotificationHostApp/PushNotificationHostApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"PushNotificationHostApp/Preview Content\""; + DEVELOPMENT_TEAM = W3DRXD72QU; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "PushNotificationHostApp copy-Info.plist"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.notification.PushNotificationHostApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3"; + }; + name = Debug; + }; + 21F763052BD6B7410048845A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = PushNotificationHostApp/PushNotificationHostApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"PushNotificationHostApp/Preview Content\""; + DEVELOPMENT_TEAM = W3DRXD72QU; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "PushNotificationHostApp copy-Info.plist"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.notification.PushNotificationHostApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3"; + }; + name = Release; + }; 6084F1B52967B87300434CBF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -799,6 +960,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 21F763032BD6B7410048845A /* Build configuration list for PBXNativeTarget "PushNotificationGen2HostApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 21F763042BD6B7410048845A /* Debug */, + 21F763052BD6B7410048845A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 6084F1B42967B87300434CBF /* Build configuration list for PBXNativeTarget "PushNotificationHostAppUITests" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -847,6 +1017,26 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 21F762F02BD6B7410048845A /* Amplify */ = { + isa = XCSwiftPackageProductDependency; + productName = Amplify; + }; + 21F762F12BD6B7410048845A /* AWSCognitoAuthPlugin */ = { + isa = XCSwiftPackageProductDependency; + productName = AWSCognitoAuthPlugin; + }; + 21F762F22BD6B7410048845A /* AWSPinpointAnalyticsPlugin */ = { + isa = XCSwiftPackageProductDependency; + productName = AWSPinpointAnalyticsPlugin; + }; + 21F762F32BD6B7410048845A /* AWSPinpointPushNotificationsPlugin */ = { + isa = XCSwiftPackageProductDependency; + productName = AWSPinpointPushNotificationsPlugin; + }; + 21F762F42BD6B7410048845A /* AWSPluginsCore */ = { + isa = XCSwiftPackageProductDependency; + productName = AWSPluginsCore; + }; 5C4EA91029B91A2600ED7924 /* Amplify */ = { isa = XCSwiftPackageProductDependency; productName = Amplify; diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationGen2HostApp.xcscheme b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationGen2HostApp.xcscheme new file mode 100644 index 0000000000..97cd654730 --- /dev/null +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationGen2HostApp.xcscheme @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationHostApp copy.xcscheme b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationHostApp copy.xcscheme new file mode 100644 index 0000000000..2d57b01c49 --- /dev/null +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationHostApp copy.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationHostApp.xcscheme b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationHostApp.xcscheme new file mode 100644 index 0000000000..4aa810bccc --- /dev/null +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationHostApp.xcscheme @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp/ContentView.swift b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp/ContentView.swift index a62472db7e..0dbe33d01f 100644 --- a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp/ContentView.swift +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp/ContentView.swift @@ -12,7 +12,8 @@ import AWSCognitoAuthPlugin import AWSPinpointPushNotificationsPlugin import AWSPinpointAnalyticsPlugin -let configFilePath = "testconfiguration/AWSPushNotificationPluginIntegrationTest-amplifyconfiguration" +let amplifyConfigurationFilePath = "testconfiguration/AWSPushNotificationPluginIntegrationTest-amplifyconfiguration" +let amplifyOutputsFilePath = "testconfiguration/AWSPushNotificationPluginIntegrationTest-amplifyconfiguration" var pushNotificationHubSubscription: UnsubscribeToken? var analyticsHubSubscription: UnsubscribeToken? @@ -23,6 +24,10 @@ struct ContentView: View { @State var showIdentifyUserDone: Bool = false @State var showRegisterTokenDone: Bool = false + var useGen2Configuration: Bool { + ProcessInfo.processInfo.arguments.contains("GEN2") + } + var body: some View { ScrollView { VStack { @@ -47,12 +52,17 @@ struct ContentView: View { func initAmplify() { do { - let config = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: configFilePath) try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: AWSPinpointAnalyticsPlugin()) try Amplify.add(plugin: AWSPinpointPushNotificationsPlugin(options: [.alert, .badge, .sound])) - try Amplify.configure(config) + if useGen2Configuration { + let data = try TestConfigHelper.retrieve(forResource: amplifyOutputsFilePath) + try Amplify.configure(with: .data(data)) + } else { + let config = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: amplifyConfigurationFilePath) + try Amplify.configure(config) + } listenHubEvent() } catch { print("Failed to init Amplify", error) diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/PushNotificationHostAppUITests.swift b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/PushNotificationHostAppUITests.swift index 4fdef0e315..ab504d95d2 100644 --- a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/PushNotificationHostAppUITests.swift +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/PushNotificationHostAppUITests.swift @@ -29,6 +29,11 @@ final class PushNotificationHostAppUITests: XCTestCase { #if os(iOS) XCUIDevice.shared.orientation = .portrait #endif + + if ProcessInfo.processInfo.arguments.contains("GEN2") { + app.launchArguments.append("GEN2") + } + app.launch() } diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/README.md b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/README.md index f08b293b40..9e2c14f541 100644 --- a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/README.md +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/README.md @@ -1,8 +1,10 @@ # Push Notification plugin Integration Test -The following steps demostrate how to set up Push Notification Category. Auth category is also required for signing with AWS Pinpoint service and requesting with IAM credentials to allow unauthenticated and authenticated access. +## Schema: PushNotificationsHostApp -## Set up Amplify +The following steps demonstrate how to set up Push Notification Category. Auth category is also required for signing with AWS Pinpoint service and requesting with IAM credentials to allow unauthenticated and authenticated access. + +### Set up Amplify 1. `amplify init` @@ -31,6 +33,17 @@ The following steps demostrate how to set up Push Notification Category. Auth ca cp amplifyconfiguration.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSPushNotificationPluginIntegrationTest-amplifyconfiguration.json ``` +## Schema: PushNotificationsGen2HostApp + +The following steps demonstrate to set up the same as above with Amplify CLI Gen2. + + +1. Copy `amplify_outputs.json` to `AWSPushNotificationPluginIntegrationTest-amplify_outputs.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/` +``` +cp amplify_outputs.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSPushNotificationPluginIntegrationTest-amplify_outputs.json +``` + + ## Run Integration Tests 1. Start local server diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/AWSPredictionsPlugin+Configure.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/AWSPredictionsPlugin+Configure.swift index 27bc74625e..42ddda79bb 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/AWSPredictionsPlugin+Configure.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/AWSPredictionsPlugin+Configure.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import Foundation import AWSPluginsCore @@ -19,21 +19,25 @@ extension AWSPredictionsPlugin { /// - Throws: /// - PluginError.pluginConfigurationError: If one of the configuration values is invalid or empty public func configure(using configuration: Any?) throws { - - guard let jsonValueConfiguration = configuration as? JSONValue else { + let predictionsConfiguration: PredictionsPluginConfiguration + if configuration is AmplifyOutputsData { + throw PluginError.pluginConfigurationError( + PluginErrorMessage.amplifyOutputsConfigurationNotSupportedError.errorDescription, + PluginErrorMessage.amplifyOutputsConfigurationNotSupportedError.recoverySuggestion + ) + } else if let jsonValueConfiguration = configuration as? JSONValue { + let configurationData = try JSONEncoder().encode(jsonValueConfiguration) + predictionsConfiguration = try JSONDecoder().decode( + PredictionsPluginConfiguration.self, + from: configurationData + ) + } else { throw PluginError.pluginConfigurationError( PluginErrorMessage.decodeConfigurationError.errorDescription, PluginErrorMessage.decodeConfigurationError.recoverySuggestion ) } - let configurationData = try JSONEncoder().encode(jsonValueConfiguration) - - let predictionsConfiguration = try JSONDecoder().decode( - PredictionsPluginConfiguration.self, - from: configurationData - ) - let authService = AWSAuthService() let credentialsProvider = authService.getCredentialsProvider() let coremlService: CoreMLPredictionBehavior? diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Support/Internal/ErrorHandling/PluginErrorMessage.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Support/Internal/ErrorHandling/PluginErrorMessage.swift index 861a3d781b..e0e8c4ce0e 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Support/Internal/ErrorHandling/PluginErrorMessage.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Support/Internal/ErrorHandling/PluginErrorMessage.swift @@ -54,4 +54,9 @@ struct PluginErrorMessage { "Could not initialize service configuration", "This should not happen" ) + + static let amplifyOutputsConfigurationNotSupportedError: PluginErrorString = ( + "Configuring with Amplify CLI Gen2 configuration is currently not supported.", + "Do not add the predictions plugin to Amplify, remove call to add predictions via `Amplify.add(plugin:)`." + ) } diff --git a/AmplifyPlugins/Predictions/CoreMLPredictionsPlugin/CoreMLPredictionsPlugin+Configure.swift b/AmplifyPlugins/Predictions/CoreMLPredictionsPlugin/CoreMLPredictionsPlugin+Configure.swift index f4bbb91e17..19edc1c7d2 100644 --- a/AmplifyPlugins/Predictions/CoreMLPredictionsPlugin/CoreMLPredictionsPlugin+Configure.swift +++ b/AmplifyPlugins/Predictions/CoreMLPredictionsPlugin/CoreMLPredictionsPlugin+Configure.swift @@ -7,12 +7,12 @@ #if canImport(Speech) && canImport(Vision) import Foundation -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify extension CoreMLPredictionsPlugin { public func configure(using configuration: Any?) throws { - guard configuration is JSONValue else { + guard configuration is JSONValue || configuration is AmplifyOutputsData else { let errorDescription = CoreMLPluginErrorString.decodeConfigurationError.errorDescription let recoverySuggestion = CoreMLPluginErrorString.decodeConfigurationError.recoverySuggestion throw PluginError.pluginConfigurationError(errorDescription, recoverySuggestion) diff --git a/AmplifyPlugins/Predictions/Tests/AWSPredictionsPluginUnitTests/ConfigurationTests/PredictionsPluginConfigurationTests.swift b/AmplifyPlugins/Predictions/Tests/AWSPredictionsPluginUnitTests/ConfigurationTests/PredictionsPluginConfigurationTests.swift index d8522f23ac..942712c8f6 100644 --- a/AmplifyPlugins/Predictions/Tests/AWSPredictionsPluginUnitTests/ConfigurationTests/PredictionsPluginConfigurationTests.swift +++ b/AmplifyPlugins/Predictions/Tests/AWSPredictionsPluginUnitTests/ConfigurationTests/PredictionsPluginConfigurationTests.swift @@ -6,11 +6,15 @@ // import XCTest -import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AWSPredictionsPlugin class PredictionsPluginConfigurationTests: XCTestCase { + override func setUp() async throws { + await Amplify.reset() + } + /// Test basic configuration parsing works /// /// - Given: A valid json data for predictions @@ -197,6 +201,22 @@ class PredictionsPluginConfigurationTests: XCTestCase { } } + func testThrowsOnAmplifyOutputsConfiguration() throws { + let plugin = AWSPredictionsPlugin() + try Amplify.add(plugin: plugin) + + let amplifyConfig = AmplifyOutputsData() + do { + try Amplify.configure(amplifyConfig) + XCTFail("Should have thrown a pluginConfigurationError if not supplied with a plugin-specific config.") + } catch { + guard case PluginError.pluginConfigurationError = error else { + XCTFail("Should have thrown a pluginConfigurationError if not supplied with a plugin-specific config.") + return + } + } + } + func testConfigureFailureForNilConfiguration() throws { let plugin = AWSPredictionsPlugin() do { diff --git a/AmplifyPlugins/Predictions/Tests/CoreMLPredictionsPluginUnitTests/CoreMLPredictionsPluginConfigTests.swift b/AmplifyPlugins/Predictions/Tests/CoreMLPredictionsPluginUnitTests/CoreMLPredictionsPluginConfigTests.swift index 1ccd33250d..68c0ed2afc 100644 --- a/AmplifyPlugins/Predictions/Tests/CoreMLPredictionsPluginUnitTests/CoreMLPredictionsPluginConfigTests.swift +++ b/AmplifyPlugins/Predictions/Tests/CoreMLPredictionsPluginUnitTests/CoreMLPredictionsPluginConfigTests.swift @@ -7,11 +7,15 @@ #if canImport(Speech) && canImport(Vision) import XCTest -import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify import CoreMLPredictionsPlugin class CoreMLPredictionsPluginConfigTests: XCTestCase { + override func setUp() async throws { + await Amplify.reset() + } + func testThrowsOnMissingConfig() throws { let plugin = CoreMLPredictionsPlugin() try Amplify.add(plugin: plugin) @@ -29,5 +33,12 @@ class CoreMLPredictionsPluginConfigTests: XCTestCase { } } + func testConfigureWithAmplifyOutputs() throws { + let plugin = CoreMLPredictionsPlugin() + try Amplify.add(plugin: plugin) + + let amplifyConfig = AmplifyOutputsData() + try Amplify.configure(amplifyConfig) + } } #endif diff --git a/AmplifyPlugins/Predictions/Tests/PredictionsHostApp/AWSPredictionsPluginIntegrationTests/README.md b/AmplifyPlugins/Predictions/Tests/PredictionsHostApp/AWSPredictionsPluginIntegrationTests/README.md index 029a17b423..c15ce97049 100644 --- a/AmplifyPlugins/Predictions/Tests/PredictionsHostApp/AWSPredictionsPluginIntegrationTests/README.md +++ b/AmplifyPlugins/Predictions/Tests/PredictionsHostApp/AWSPredictionsPluginIntegrationTests/README.md @@ -1,5 +1,7 @@ # AWSPredictionsPluginIntegrationTests +## Schema: AWSPredictionsPluginIntegrationTests + The following steps demonstrate how to set up DataStore with a conflict resolution enabled API through amplify CLI, with API key authentication mode. ### Set-up @@ -75,4 +77,10 @@ You should now be able to run all of the tests 1. testImageText.jpg [sketchbook-comp-4-text-and-image](https://mir-s3-cdn-cf.behance.net/project_modules/disp/44ccbf15338381.5628facc26f03.jpg) by [Ana Curado e Silva](https://www.behance.net/gallery/15338381/Sketchbook-Comp-4-Text-and-Image) is licensed under [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/?ref=ccsearch) ![](https://search.creativecommons.org/static/img/cc_icon.svg)![](https://search.creativecommons.org/static/img/cc-by_icon.svg)![](https://search.creativecommons.org/static/img/cc-nc_icon.svg)![](https://search.creativecommons.org/static/img/cc-nd_icon.svg) 2. testImageCeleb.jpg [celebrities and politicians](https://mir-s3-cdn-cf.behance.net/project_modules/disp/fdd0b142234581.560716afcda7d.jpg) by [William Coupon](https://www.behance.net/gallery/5346285/celebrities-politicians) is licensed under [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/?ref=ccsearch&atype=html) ![](https://search.creativecommons.org/static/img/cc_icon.svg) ![](https://search.creativecommons.org/static/img/cc-by_icon.svg) ![](https://search.creativecommons.org/static/img/cc-nc_icon.svg) ![](https://search.creativecommons.org/static/img/cc-nd_icon.svg) -3. testimageTextAll.jpg [amazon-textract-code-samples-files](https://raw.githubusercontent.com/aws-samples/amazon-textract-code-samples/master/src-csharp/test-files/employmentapp.png) \ No newline at end of file +3. testimageTextAll.jpg [amazon-textract-code-samples-files](https://raw.githubusercontent.com/aws-samples/amazon-textract-code-samples/master/src-csharp/test-files/employmentapp.png) + + +## Schema: AWSPredictionsPluginGen2IntegrationTests + +Predictions configuration is added to the custom section of `amplify_outputs.json`. Currently this cannot be configured in the library yet. + diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+Configure.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+Configure.swift index a0dc00a8e5..f6ee4f1a7d 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+Configure.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+Configure.swift @@ -6,7 +6,7 @@ // import Foundation -import Amplify +@_spi(InternalAmplifyConfiguration) import Amplify import AWSPluginsCore extension AWSS3StoragePlugin { @@ -18,35 +18,34 @@ extension AWSS3StoragePlugin { /// /// - Parameter configuration: The configuration specified for this plugin /// - Throws: - /// - PluginError.pluginConfigurationError: If one of the configuration values is invalid or empty + /// - `PluginError` is thrown if the AmplifyConfiguration is an invalid JSON, or AmplifyOutputsData's `storage` category is missing. + /// - `PluginError` is wrapped as the underlying error of a `StorageError` for other validation logic related to retrieving + /// configuration fields such as `region` and `bucket`. /// /// - Tag: AWSS3StoragePlugin.configure public func configure(using configuration: Any?) throws { - guard let config = configuration as? JSONValue else { - throw PluginError.pluginConfigurationError(PluginErrorConstants.decodeConfigurationError.errorDescription, - PluginErrorConstants.decodeConfigurationError.recoverySuggestion) - } - - guard case let .object(configObject) = config else { - throw StorageError.configuration( - PluginErrorConstants.configurationObjectExpected.errorDescription, - PluginErrorConstants.configurationObjectExpected.recoverySuggestion) + let configClosures: ConfigurationClosures + if let config = configuration as? AmplifyOutputsData { + configClosures = try retrieveConfiguration(config) + } else if let config = configuration as? JSONValue { + configClosures = try retrieveConfiguration(config) + } else { + throw PluginError.pluginConfigurationError( + PluginErrorConstants.decodeConfigurationError.errorDescription, + PluginErrorConstants.decodeConfigurationError.recoverySuggestion) } do { let authService = AWSAuthService() - - let region = try AWSS3StoragePlugin.getRegion(configObject) - let bucket = try AWSS3StoragePlugin.getBucket(configObject) - let defaultAccessLevel = try AWSS3StoragePlugin.getDefaultAccessLevel(configObject) - let storageService = try AWSS3StorageService(authService: authService, - region: region, - bucket: bucket, + region: configClosures.retrieveRegion(), + bucket: configClosures.retrieveBucket(), httpClientEngineProxy: self.httpClientEngineProxy) storageService.urlRequestDelegate = self.urlRequestDelegate - configure(storageService: storageService, authService: authService, defaultAccessLevel: defaultAccessLevel) + configure(storageService: storageService, + authService: authService, + defaultAccessLevel: try configClosures.retrieveDefaultAccessLevel()) } catch let storageError as StorageError { throw storageError } catch { @@ -79,11 +78,54 @@ extension AWSS3StoragePlugin { self.authService = authService self.queue = queue self.defaultAccessLevel = defaultAccessLevel - } // MARK: Private helper methods + private struct ConfigurationClosures { + let retrieveRegion: () throws -> String + let retrieveBucket: () throws -> String + let retrieveDefaultAccessLevel: () throws -> StorageAccessLevel + } + + private func retrieveConfiguration(_ configuration: AmplifyOutputsData) throws -> ConfigurationClosures { + guard let storage = configuration.storage else { + throw PluginError.pluginConfigurationError( + PluginErrorConstants.missingStorageCategoryConfiguration.errorDescription, + PluginErrorConstants.missingStorageCategoryConfiguration.recoverySuggestion) + } + + let regionClosure = { + try AWSS3StoragePlugin.validateRegionNonEmpty(storage.awsRegion) + return storage.awsRegion + } + + let bucketClosure = { + try AWSS3StoragePlugin.validateBucketNonEmpty(storage.bucketName) + return storage.bucketName + } + + return ConfigurationClosures(retrieveRegion: regionClosure, + retrieveBucket: bucketClosure, + retrieveDefaultAccessLevel: { .guest }) + } + + private func retrieveConfiguration(_ configuration: JSONValue) throws -> ConfigurationClosures { + guard case let .object(configObject) = configuration else { + throw StorageError.configuration( + PluginErrorConstants.configurationObjectExpected.errorDescription, + PluginErrorConstants.configurationObjectExpected.recoverySuggestion) + } + + let regionClosure = { try AWSS3StoragePlugin.getRegion(configObject) } + let bucketClosure = { try AWSS3StoragePlugin.getBucket(configObject) } + let defaultAccessLevelClosure = { try AWSS3StoragePlugin.getDefaultAccessLevel(configObject) } + + return ConfigurationClosures(retrieveRegion: regionClosure, + retrieveBucket: bucketClosure, + retrieveDefaultAccessLevel: defaultAccessLevelClosure) + } + /// Retrieves the region from configuration, validates, and returns it. private static func getRegion(_ configuration: [String: JSONValue]) throws -> String { guard let region = configuration[PluginConstants.region] else { @@ -96,12 +138,16 @@ extension AWSS3StoragePlugin { PluginErrorConstants.invalidRegion.recoverySuggestion) } - if regionValue.isEmpty { + try validateRegionNonEmpty(regionValue) + + return regionValue + } + + private static func validateRegionNonEmpty(_ region: String) throws { + if region.isEmpty { throw PluginError.pluginConfigurationError(PluginErrorConstants.emptyRegion.errorDescription, PluginErrorConstants.emptyRegion.recoverySuggestion) } - - return regionValue } /// Retrieves the bucket from configuration, validates, and returns it. @@ -116,12 +162,16 @@ extension AWSS3StoragePlugin { PluginErrorConstants.invalidBucket.recoverySuggestion) } - if bucketValue.isEmpty { + try validateBucketNonEmpty(bucketValue) + + return bucketValue + } + + private static func validateBucketNonEmpty(_ bucket: String) throws { + if bucket.isEmpty { throw PluginError.pluginConfigurationError(PluginErrorConstants.emptyBucket.errorDescription, PluginErrorConstants.emptyBucket.recoverySuggestion) } - - return bucketValue } /// Checks if the access level is specified in the configurationand and retrieves it. Returns the default diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin.swift index 66fb690978..cd029024a7 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin.swift @@ -47,7 +47,7 @@ final public class AWSS3StoragePlugin: StorageCategoryPlugin { /// /// - Tag: AWSS3StoragePlugin.init public init(configuration - storageConfiguration: AWSS3StoragePluginConfiguration = AWSS3StoragePluginConfiguration()) { + storageConfiguration: AWSS3StoragePluginConfiguration = AWSS3StoragePluginConfiguration()) { self.storageConfiguration = storageConfiguration } } diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Constants/PluginErrorConstants.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Constants/PluginErrorConstants.swift index 18c0748bbf..24f3224916 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Constants/PluginErrorConstants.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Support/Constants/PluginErrorConstants.swift @@ -19,6 +19,10 @@ struct PluginErrorConstants { "Configuration was not a dictionary literal", "Make sure the value for the plugin is a dictionary literal with keys 'Bucket' and 'Region'") + static let missingStorageCategoryConfiguration: PluginErrorString = ( + "Plugin is missing `Storage` category in configuration.", + "Add the `Storage` section to the configuration.") + static let missingBucket: PluginErrorString = ( "The 'Bucket' key is missing from the configuration", "Make sure 'Bucket' is in the dictionary for the plugin configuration") diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/AWSS3StoragePluginAmplifyOutputsConfigurationTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/AWSS3StoragePluginAmplifyOutputsConfigurationTests.swift new file mode 100644 index 0000000000..e7cf698a01 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/AWSS3StoragePluginAmplifyOutputsConfigurationTests.swift @@ -0,0 +1,113 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +@testable @_spi(InternalAmplifyConfiguration) import Amplify +@testable import AWSS3StoragePlugin + +class AWSS3StoragePluginAmplifyOutputsConfigurationTests: AWSS3StoragePluginTests { + + func testConfigureSuccess() throws { + do { + let config = AmplifyOutputsData(storage: .init( + awsRegion: testRegion, + bucketName: testBucket)) + try storagePlugin.configure(using: config) + } catch { + XCTFail("Failed to configure storage plugin") + } + } + + func testConfigureThrowsErrorForMissingStorageCategoryConfiguration() { + let config = AmplifyOutputsData() + XCTAssertThrowsError(try storagePlugin.configure(using: config)) { error in + guard case let PluginError.pluginConfigurationError(errorDescription, _, _) = error else { + XCTFail("Expected PluginError pluginConfigurationError, got: \(error)") + return + } + + XCTAssertEqual(errorDescription, PluginErrorConstants.missingStorageCategoryConfiguration.errorDescription) + } + } + + func testConfigureThrowsForEmptyBucketValue() { + let config = AmplifyOutputsData(storage: .init( + awsRegion: testRegion, + bucketName: "")) + XCTAssertThrowsError(try storagePlugin.configure(using: config)) { error in + guard case let StorageError.configuration(_, _, underlyingError) = error else { + XCTFail("Expected PluginError pluginConfigurationError, got: \(error)") + return + } + + guard let resolvedUnderlyingError = underlyingError else { + XCTFail("No underlying error in error: \(error)") + return + } + + guard let amplifyError = resolvedUnderlyingError as? AmplifyError else { + XCTFail("Underlying error is not an AmplifyError: \(resolvedUnderlyingError)") + return + } + + XCTAssertEqual(amplifyError.errorDescription, PluginErrorConstants.emptyBucket.errorDescription) + } + } + + func testConfigureThrowsForEmptyRegionValue() { + let config = AmplifyOutputsData(storage: .init( + awsRegion: "", + bucketName: testBucket)) + XCTAssertThrowsError(try storagePlugin.configure(using: config)) { error in + guard case let StorageError.configuration(_, _, underlyingError) = error else { + XCTFail("Expected PluginError pluginConfigurationError, got: \(error)") + return + } + + guard let resolvedUnderlyingError = underlyingError else { + XCTFail("No underlying error in error: \(error)") + return + } + + guard let amplifyError = resolvedUnderlyingError as? AmplifyError else { + XCTFail("Underlying error is not an AmplifyError: \(resolvedUnderlyingError)") + return + } + + XCTAssertEqual(amplifyError.errorDescription, PluginErrorConstants.emptyRegion.errorDescription) + } + } + + let isValidationRegionConfig = false + + func testConfigureThrowsForInvalidRegionType() throws { + try XCTSkipIf(!isValidationRegionConfig, "Skipping until region validation is enabled") + let config = AmplifyOutputsData(storage: .init( + awsRegion: "invalidRegionType", + bucketName: testBucket)) + + XCTAssertThrowsError(try storagePlugin.configure(using: config)) { error in + guard case let StorageError.configuration(_, _, underlyingError) = error else { + XCTFail("Expected PluginError pluginConfigurationError, got: \(error)") + return + } + + guard let resolvedUnderlyingError = underlyingError else { + XCTFail("No underlying error in error: \(error)") + return + } + + guard let amplifyError = resolvedUnderlyingError as? AmplifyError else { + XCTFail("Underlying error is not an AmplifyError: \(resolvedUnderlyingError)") + return + } + + XCTAssertEqual(amplifyError.errorDescription, PluginErrorConstants.invalidRegion.errorDescription) + } + } +} + diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/AWSS3StoragePluginBaseConfigTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/AWSS3StoragePluginBaseConfigTests.swift index 95f75a8bec..0900d740e4 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/AWSS3StoragePluginBaseConfigTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/AWSS3StoragePluginBaseConfigTests.swift @@ -22,7 +22,7 @@ class AWSS3StoragePluginBaseConfigTests: XCTestCase { XCTFail("Should have thrown a pluginConfigurationError if not supplied with a plugin-specific config.") } catch { guard case PluginError.pluginConfigurationError = error else { - XCTFail("Should have thrown a pluginConfigurationError if not supplied with a plugin-specific config.") + XCTFail("Should have thrown a pluginConfigurationError if not supplied with a plugin-specific config, but threw error: \(error)") return } } diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/.gitignore b/AmplifyPlugins/Storage/Tests/StorageHostApp/.gitignore index 90fb3c0cc1..4b4e5628c3 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/.gitignore +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/.gitignore @@ -17,4 +17,6 @@ amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample -#amplify-do-not-edit-end \ No newline at end of file +#amplify-do-not-edit-end + +amplify_outputs.json \ No newline at end of file diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginAccessLevelTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginAccessLevelTests.swift index ba6f883e4b..00b6fd6147 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginAccessLevelTests.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginAccessLevelTests.swift @@ -16,6 +16,8 @@ class AWSS3StoragePluginAccessLevelTests: AWSS3StoragePluginTestBase { let label: String let key: String let accessLevel: StorageAccessLevel + let user1: (String, String) // (username/email and password) + let user2: (String, String) // (username/email and password) } /// Given: An unauthenticated user @@ -49,8 +51,17 @@ class AWSS3StoragePluginAccessLevelTests: AWSS3StoragePluginTestBase { func testUploadAndRemoveForGuestOnly() async throws { let logger = Amplify.Logging.logger(forCategory: "Storage", logLevel: .verbose) - let username = AWSS3StoragePluginTestBase.user1.lowercased() - let password = AWSS3StoragePluginTestBase.password + let username: String + let password: String + if useGen2Configuration { + username = "\(UUID().uuidString)@amazon.com" + password = "Pp123!@\(UUID().uuidString)" + _ = try await Amplify.Auth.signUp(username: username, password: password) + } else { + username = AWSS3StoragePluginTestBase.user1.lowercased() + password = AWSS3StoragePluginTestBase.password + } + let accessLevel: StorageAccessLevel = .guest do { @@ -101,14 +112,21 @@ class AWSS3StoragePluginAccessLevelTests: AWSS3StoragePluginTestBase { .guest ] - let username = AWSS3StoragePluginTestBase.user1.lowercased() - let password = AWSS3StoragePluginTestBase.password - + let username: String + let password: String + if useGen2Configuration { + username = "\(UUID().uuidString)@amazon.com" + password = "Pp123!@\(UUID().uuidString)" + _ = try await Amplify.Auth.signUp(username: username, password: password) + } else { + username = AWSS3StoragePluginTestBase.user1.lowercased() + password = AWSS3StoragePluginTestBase.password + } logger.debug("Signing in as user1") let result = try await Amplify.Auth.signIn(username: username, password: password) XCTAssertTrue(result.isSignedIn) let currentUser = try await Amplify.Auth.getCurrentUser() - XCTAssertEqual(username, currentUser.username) + XCTAssertEqual(username, currentUser.username) let isSignedIn = result.isSignedIn // must be signed in to continue @@ -158,13 +176,39 @@ class AWSS3StoragePluginAccessLevelTests: AWSS3StoragePluginTestBase { func testAccessLevelsBetweenTwoUsers() async throws { let logger = Amplify.Logging.logger(forCategory: "Storage", logLevel: .verbose) + let username1: String + let username2: String + let password: String + if useGen2Configuration { + username1 = "\(UUID().uuidString)@amazon.com" + password = "Pp123!@\(UUID().uuidString)" + _ = try await Amplify.Auth.signUp(username: username1, password: password) + username2 = "\(UUID().uuidString)@amazon.com" + _ = try await Amplify.Auth.signUp(username: username2, password: password) + } else { + username1 = AWSS3StoragePluginTestBase.user1 + username2 = AWSS3StoragePluginTestBase.user2 + password = AWSS3StoragePluginTestBase.password + } let testRuns: [StorageAccessLevelsTestRun] = [ // user 2 can read upload by user 1 with guest access - .init(label: "Guest", key: UUID().uuidString, accessLevel: .guest), + .init(label: "Guest", + key: UUID().uuidString, + accessLevel: .guest, + user1: (username1, password), + user2: (username2, password)), // user 2 can read upload by user 1 with protected access - .init(label: "Protected", key: UUID().uuidString, accessLevel: .protected), + .init(label: "Protected", + key: UUID().uuidString, + accessLevel: .protected, + user1: (username1, password), + user2: (username2, password)), // user 2 can get access denied error from upload by user 1 with private access - .init(label: "Private", key: UUID().uuidString, accessLevel: .private) + .init(label: "Private", + key: UUID().uuidString, + accessLevel: .private, + user1: (username1, password), + user2: (username2, password)), ] for testRun in testRuns { @@ -174,7 +218,8 @@ class AWSS3StoragePluginAccessLevelTests: AWSS3StoragePluginTestBase { await signOut() logger.debug("Signing in user1") - let user1SignedIn = try await Amplify.Auth.signIn(username: AWSS3StoragePluginTestBase.user1, password: AWSS3StoragePluginTestBase.password).isSignedIn + let user1SignedIn = try await Amplify.Auth.signIn(username: testRun.user1.0, + password: testRun.user1.1).isSignedIn XCTAssertTrue(user1SignedIn) logger.debug("Getting identity for user1") @@ -197,7 +242,8 @@ class AWSS3StoragePluginAccessLevelTests: AWSS3StoragePluginTestBase { await signOut() logger.debug("Signing in as user2") - let user2SignedIn = try await Amplify.Auth.signIn(username: AWSS3StoragePluginTestBase.user2, password: AWSS3StoragePluginTestBase.password).isSignedIn + let user2SignedIn = try await Amplify.Auth.signIn(username: testRun.user2.0, + password: testRun.user2.1).isSignedIn XCTAssertTrue(user2SignedIn) logger.debug("Getting identity for user2") diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGen2IntegrationTests.xctestplan b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGen2IntegrationTests.xctestplan new file mode 100644 index 0000000000..4c904c4b76 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGen2IntegrationTests.xctestplan @@ -0,0 +1,28 @@ +{ + "configurations" : [ + { + "id" : "0FBDC955-0CBB-4E06-B071-1A55189542CE", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "commandLineArgumentEntries" : [ + { + "argument" : "GEN2" + } + ] + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:StorageHostApp.xcodeproj", + "identifier" : "21F763092BD6B8640048845A", + "name" : "AWSS3StoragePluginGen2IntegrationTests" + } + } + ], + "version" : 1 +} diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift index ae72f1825a..2aedb7a1b6 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift @@ -22,7 +22,7 @@ class AWSS3StoragePluginTestBase: XCTestCase { static var user1: String = "integTest\(UUID().uuidString)" static var user2: String = "integTest\(UUID().uuidString)" - static var password: String = "P123@\(UUID().uuidString)" + static var password: String = "Pp123@\(UUID().uuidString)" static var email1 = UUID().uuidString + "@" + UUID().uuidString + ".com" static var email2 = UUID().uuidString + "@" + UUID().uuidString + ".com" @@ -31,6 +31,10 @@ class AWSS3StoragePluginTestBase: XCTestCase { var requestRecorder: AWSS3StoragePluginRequestRecorder! + var useGen2Configuration: Bool { + ProcessInfo.processInfo.arguments.contains("GEN2") + } + override func setUp() async throws { Self.logger.debug("setUp") self.requestRecorder = AWSS3StoragePluginRequestRecorder() @@ -43,7 +47,11 @@ class AWSS3StoragePluginTestBase: XCTestCase { try Amplify.add(plugin: AWSCognitoAuthPlugin()) try Amplify.add(plugin: storagePlugin) - try Amplify.configure() + if useGen2Configuration { + try Amplify.configure(with: .amplifyOutputs) + } else { + try Amplify.configure() + } if (try? await Amplify.Auth.getCurrentUser()) != nil { await signOut() } @@ -107,18 +115,30 @@ class AWSS3StoragePluginTestBase: XCTestCase { XCTAssertNotNil(result) } - static func getBucketFromConfig(forResource: String) throws -> String { + func getBucketFromConfig(forResource: String) throws -> String { let data = try TestConfigHelper.retrieve(forResource: forResource) let json = try JSONDecoder().decode(JSONValue.self, from: data) - guard let bucket = json["storage"]?["plugins"]?["awsS3StoragePlugin"]?["bucket"] else { - throw "Could not retrieve bucket from config" - } + if useGen2Configuration { + guard let bucket = json["storage"]?["bucket_name"] else { + throw "Could not retrieve bucket from config" + } - guard case let .string(bucketValue) = bucket else { - throw "bucket is not a string value" - } + guard case let .string(bucketValue) = bucket else { + throw "bucket is not a string value" + } + + return bucketValue + } else { + guard let bucket = json["storage"]?["plugins"]?["awsS3StoragePlugin"]?["bucket"] else { + throw "Could not retrieve bucket from config" + } - return bucketValue + guard case let .string(bucketValue) = bucket else { + throw "bucket is not a string value" + } + + return bucketValue + } } func signUp() async { @@ -129,9 +149,15 @@ class AWSS3StoragePluginTestBase: XCTestCase { let registerFirstUserComplete = expectation(description: "register firt user completed") Task { do { - try await AuthSignInHelper.signUpUser(username: AWSS3StoragePluginTestBase.user1, - password: AWSS3StoragePluginTestBase.password, - email: AWSS3StoragePluginTestBase.email1) + if useGen2Configuration { + try await AuthSignInHelper.signUpUser(username: AWSS3StoragePluginTestBase.email1, + password: AWSS3StoragePluginTestBase.password, + email: AWSS3StoragePluginTestBase.email1) + } else { + try await AuthSignInHelper.signUpUser(username: AWSS3StoragePluginTestBase.user1, + password: AWSS3StoragePluginTestBase.password, + email: AWSS3StoragePluginTestBase.email1) + } Self.isFirstUserSignedUp = true registerFirstUserComplete.fulfill() } catch { @@ -143,9 +169,15 @@ class AWSS3StoragePluginTestBase: XCTestCase { let registerSecondUserComplete = expectation(description: "register second user completed") Task { do { - try await AuthSignInHelper.signUpUser(username: AWSS3StoragePluginTestBase.user2, - password: AWSS3StoragePluginTestBase.password, - email: AWSS3StoragePluginTestBase.email2) + if useGen2Configuration { + try await AuthSignInHelper.signUpUser(username: AWSS3StoragePluginTestBase.email2, + password: AWSS3StoragePluginTestBase.password, + email: AWSS3StoragePluginTestBase.email2) + } else { + try await AuthSignInHelper.signUpUser(username: AWSS3StoragePluginTestBase.user2, + password: AWSS3StoragePluginTestBase.password, + email: AWSS3StoragePluginTestBase.email2) + } Self.isSecondUserSignedUp = true registerSecondUserComplete.fulfill() } catch { diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginUploadMetadataTestCase.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginUploadMetadataTestCase.swift index 9eacb41364..0fda026a51 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginUploadMetadataTestCase.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginUploadMetadataTestCase.swift @@ -238,9 +238,16 @@ class AWSS3StoragePluginUploadMetadataTestCase: AWSS3StoragePluginTestBase { "Cast to `AWSS3StoragePlugin` failed" ) let s3Client = storagePlugin.getEscapeHatch() - let bucket = try AWSS3StoragePluginTestBase.getBucketFromConfig( - forResource: "amplifyconfiguration" - ) + let bucket: String + if useGen2Configuration { + bucket = try getBucketFromConfig( + forResource: "amplify_outputs" + ) + } else { + bucket = try getBucketFromConfig( + forResource: "amplifyconfiguration" + ) + } let input = HeadObjectInput( bucket: bucket, key: key diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md index 788456d7b5..75f4dd97a2 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md @@ -1,4 +1,6 @@ -## Storage Integration Tests +# Storage Integration Tests + +## Schema: AWSS3StoragePluginIntegrationTests The following steps demonstrate how to set up Storage with unauthenticated and authenticated access.In the case of authenticated access, we will be using Cognito UserPools. Both unauthenticated and authenticated configurations are used to execute the AWSS3StoragePluginFunctionalTests. This set up is used to run the tests in AWSS3StoragePluginFunctionalTests @@ -99,3 +101,145 @@ cp amplifyconfiguration.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSS3S ``` You should now be able to run all of the tests from AWSS3StoragePluginAccessLevelTests + +## Schema: AWSS3StoragePluginGen2IntegrationTests + + +### Set-up + +At the time this was written, it follows the steps from here https://docs.amplify.aws/gen2/deploy-and-host/fullstack-branching/mono-and-multi-repos/ + +1. From a new folder, run `npm create amplify@beta`. This uses the following versions of the Amplify CLI, see `package.json` file below. + +```json +{ + ... + "devDependencies": { + "@aws-amplify/backend": "^0.13.0-beta.14", + "@aws-amplify/backend-cli": "^0.12.0-beta.16", + "aws-cdk": "^2.134.0", + "aws-cdk-lib": "^2.134.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "tsx": "^4.7.1", + "typescript": "^5.4.3" + }, + "dependencies": { + "aws-amplify": "^6.0.25" + } +} + + +2. Update `amplify/storage/resource.ts`. The resulting file should look like this + +```ts +import { defineStorage } from '@aws-amplify/backend'; + +export const storage = defineStorage({ + name: 'myProjectFiles', + access: (allow) => ({ + 'public/*': [ + allow.guest.to(['read', 'write', 'delete']), + allow.authenticated.to(['read', 'write', 'delete']), + ], + 'protected/{entity_id}/*': [ + allow.guest.to(['read']), + allow.authenticated.to(['read']), + allow.entity('identity').to(['read', 'write', 'delete']) + ], + 'private/{entity_id}/*': [allow.entity('identity').to(['read', 'write', 'delete'])] + }) + }); +``` + +Update `amplify/auth/resource.ts`. The resulting file should look like this + +```ts +import { defineAuth, defineFunction } from '@aws-amplify/backend'; + +/** + * Define and configure your auth resource + * @see https://docs.amplify.aws/gen2/build-a-backend/auth + */ +export const auth = defineAuth({ + loginWith: { + email: true + }, + triggers: { + // configure a trigger to point to a function definition + preSignUp: defineFunction({ + entry: './pre-sign-up-handler.ts' + }) + } +}); + +``` + +`pre-sign-up-handler.ts` + +```ts +import type { PreSignUpTriggerHandler } from 'aws-lambda'; + +export const handler: PreSignUpTriggerHandler = async (event) => { + // your code here + event.response.autoConfirmUser = true + return event; +}; +``` + +`backend.ts` + +```ts +const { cfnUserPool } = backend.auth.resources.cfnResources +cfnUserPool.usernameAttributes = [] + +cfnUserPool.addPropertyOverride( + "Policies", + { + PasswordPolicy: { + MinimumLength: 10, + RequireLowercase: false, + RequireNumbers: true, + RequireSymbols: true, + RequireUppercase: true, + TemporaryPasswordValidityDays: 20, + }, + } +); +``` + +4. Deploy the backend with npx amplify sandbox + +For example, this deploys to a sandbox env and generates the amplify_outputs.json file. + +``` +npx amplify sandbox --config-out-dir ./config --config-version 1 --profile [PROFILE] +``` + +5. Copy the `amplify_outputs.json` file over to the test directory as `AWSS3StoragePluginTests-amplify_outputs.json`. The tests will automatically pick this file up. Create the directories in this path first if it currently doesn't exist. + +``` +cp amplify_outputs.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSS3StoragePluginTests-amplify_outputs.json +``` + +### Deploying from a branch (Optional) + +If you want to be able utilize Git commits for deployments + +4. Commit and push the files to a git repository. + +5. Navigate to the AWS Amplify console (https://us-east-1.console.aws.amazon.com/amplify/home?region=us-east-1#/) + +6. Click on "Try Amplify Gen 2" button. + +7. Choose "Option 2: Start with an existing app", and choose Github, and press Next. + +8. Find the repository and branch, and click Next + +9. Click "Save and deploy" and wait for deployment to finish. + +10. Generate the `amplify_outputs.json` configuration file + +``` +npx amplify generate config --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 +``` diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj index da28c91c22..9ead18462e 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/project.pbxproj @@ -9,6 +9,28 @@ /* 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 */; }; + 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 */; }; + 21F7630E2BD6B8640048845A /* AuthSignInHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB0C228BEB45600C8A6EB /* AuthSignInHelper.swift */; }; + 21F7630F2BD6B8640048845A /* AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFEAF28E748270000C36A /* AsyncTesting.swift */; }; + 21F763102BD6B8640048845A /* AWSS3StoragePluginGetDataResumabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08B28BEAF8E00C8A6EB /* AWSS3StoragePluginGetDataResumabilityTests.swift */; }; + 21F763112BD6B8640048845A /* AWSS3StoragePluginUploadMetadataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901AB3E82AE2C2DC000F825B /* AWSS3StoragePluginUploadMetadataTestCase.swift */; }; + 21F763122BD6B8640048845A /* AsyncExpectation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFEB028E748270000C36A /* AsyncExpectation.swift */; }; + 21F763132BD6B8640048845A /* AWSS3StoragePluginProgressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08C28BEAF8E00C8A6EB /* AWSS3StoragePluginProgressTests.swift */; }; + 21F763142BD6B8640048845A /* AWSS3StoragePluginAccessLevelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08628BEAF8E00C8A6EB /* AWSS3StoragePluginAccessLevelTests.swift */; }; + 21F763152BD6B8640048845A /* AWSS3StoragePluginDownloadFileResumabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08928BEAF8E00C8A6EB /* AWSS3StoragePluginDownloadFileResumabilityTests.swift */; }; + 21F763162BD6B8640048845A /* AWSS3StoragePluginPrefixKeyResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB07F28BEAF8E00C8A6EB /* AWSS3StoragePluginPrefixKeyResolverTests.swift */; }; + 21F763172BD6B8640048845A /* AWSS3StoragePluginTestBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB07E28BEAF8E00C8A6EB /* AWSS3StoragePluginTestBase.swift */; }; + 21F763182BD6B8640048845A /* AWSS3StoragePluginUploadFileResumabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08A28BEAF8E00C8A6EB /* AWSS3StoragePluginUploadFileResumabilityTests.swift */; }; + 21F763192BD6B8640048845A /* AWSS3StoragePluginRequestRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */; }; + 21F7631A2BD6B8640048845A /* AWSS3StoragePluginConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08028BEAF8E00C8A6EB /* AWSS3StoragePluginConfigurationTests.swift */; }; + 21F7631B2BD6B8640048845A /* AWSS3StoragePluginPutDataResumabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08828BEAF8E00C8A6EB /* AWSS3StoragePluginPutDataResumabilityTests.swift */; }; + 21F7631C2BD6B8640048845A /* AWSS3StoragePluginNegativeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08328BEAF8E00C8A6EB /* AWSS3StoragePluginNegativeTests.swift */; }; + 21F7631D2BD6B8640048845A /* AWSS3StoragePluginBasicIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08428BEAF8E00C8A6EB /* AWSS3StoragePluginBasicIntegrationTests.swift */; }; + 21F7631E2BD6B8640048845A /* XCTestCase+AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFEB128E748270000C36A /* XCTestCase+AsyncTesting.swift */; }; + 21F7631F2BD6B8640048845A /* AWSS3StoragePluginOptionsUsabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB08128BEAF8E00C8A6EB /* AWSS3StoragePluginOptionsUsabilityTests.swift */; }; + 21F763202BD6B8640048845A /* TestConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684FB09D28BEAFE700C8A6EB /* TestConfigHelper.swift */; }; 56043E9329FC4D33003E3424 /* amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = D5C0382101A0E23943FDF4CB /* amplifyconfiguration.json */; }; 562B9AA42A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */; }; 562B9AA52A0D734E00A96FC6 /* AWSS3StoragePluginRequestRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */; }; @@ -74,6 +96,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 21F7630B2BD6B8640048845A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 684FB06228BEAF1500C8A6EB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 684FB06928BEAF1500C8A6EB; + remoteInfo = StorageHostApp; + }; 681D7D732A42648C00F7C310 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 684FB06228BEAF1500C8A6EB /* Project object */; @@ -101,6 +130,10 @@ 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 = ""; }; + 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; }; + 21F763272BD6B8950048845A /* AWSS3StoragePluginGen2IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AWSS3StoragePluginGen2IntegrationTests.xctestplan; sourceTree = ""; }; 562B9AA32A0D703700A96FC6 /* AWSS3StoragePluginRequestRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginRequestRecorder.swift; sourceTree = ""; }; 565DF16F2953BAEA000DCCF7 /* AWSS3StoragePluginAccelerateIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSS3StoragePluginAccelerateIntegrationTests.swift; sourceTree = ""; }; 681D7D392A42637700F7C310 /* StorageWatchApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StorageWatchApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -138,6 +171,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 21F763212BD6B8640048845A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 681D7D362A42637700F7C310 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -227,6 +267,7 @@ 97914BB92955798D002000EA /* StorageStressTests.xctest */, 681D7D392A42637700F7C310 /* StorageWatchApp.app */, 681D7D6C2A4263E500F7C310 /* AWSS3StoragePluginIntegrationTestsWatch.xctest */, + 21F763262BD6B8640048845A /* AWSS3StoragePluginGen2IntegrationTests.xctest */, ); name = Products; sourceTree = ""; @@ -256,6 +297,8 @@ 684FB07D28BEAF8E00C8A6EB /* AWSS3StoragePluginIntegrationTests */ = { isa = PBXGroup; children = ( + 21F763272BD6B8950048845A /* AWSS3StoragePluginGen2IntegrationTests.xctestplan */, + 21D165C02BBEDF0A001E3D4B /* README.md */, 684FB0C128BEB44700C8A6EB /* Helpers */, 684FB08628BEAF8E00C8A6EB /* AWSS3StoragePluginAccessLevelTests.swift */, 565DF16F2953BAEA000DCCF7 /* AWSS3StoragePluginAccelerateIntegrationTests.swift */, @@ -303,6 +346,7 @@ 830883E72D40B8E1A9AFB5F0 /* AmplifyConfig */ = { isa = PBXGroup; children = ( + 21D165C22BBEF329001E3D4B /* amplify_outputs.json */, D5C0382101A0E23943FDF4CB /* amplifyconfiguration.json */, ); name = AmplifyConfig; @@ -320,6 +364,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 21F763092BD6B8640048845A /* AWSS3StoragePluginGen2IntegrationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 21F763232BD6B8640048845A /* Build configuration list for PBXNativeTarget "AWSS3StoragePluginGen2IntegrationTests" */; + buildPhases = ( + 21F7630C2BD6B8640048845A /* Sources */, + 21F763212BD6B8640048845A /* Frameworks */, + 21F763222BD6B8640048845A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 21F7630A2BD6B8640048845A /* PBXTargetDependency */, + ); + name = AWSS3StoragePluginGen2IntegrationTests; + productName = AWSS3StoragePluginIntegrationTests; + productReference = 21F763262BD6B8640048845A /* AWSS3StoragePluginGen2IntegrationTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 681D7D382A42637700F7C310 /* StorageWatchApp */ = { isa = PBXNativeTarget; buildConfigurationList = 681D7D472A42637900F7C310 /* Build configuration list for PBXNativeTarget "StorageWatchApp" */; @@ -370,7 +432,7 @@ isa = PBXNativeTarget; buildConfigurationList = 684FB07828BEAF1600C8A6EB /* Build configuration list for PBXNativeTarget "StorageHostApp" */; buildPhases = ( - 56B54B1F29FC365C0000DF7D /* Copy amplifyconfiguration */, + 56B54B1F29FC365C0000DF7D /* Copy amplifyconfiguration and amplify_outputs */, 684FB06628BEAF1500C8A6EB /* Sources */, 684FB06728BEAF1500C8A6EB /* Frameworks */, 684FB06828BEAF1500C8A6EB /* Resources */, @@ -471,16 +533,25 @@ 97914B9E2955798D002000EA /* StorageStressTests */, 681D7D382A42637700F7C310 /* StorageWatchApp */, 681D7D512A4263E500F7C310 /* AWSS3StoragePluginIntegrationTestsWatch */, + 21F763092BD6B8640048845A /* AWSS3StoragePluginGen2IntegrationTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 21F763222BD6B8640048845A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 681D7D372A42637700F7C310 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 681D7D852A426FF500F7C310 /* amplifyconfiguration.json in Resources */, + 21D165C42BBEF329001E3D4B /* amplify_outputs.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -496,6 +567,7 @@ buildActionMask = 2147483647; files = ( 031BC3F328EC9B2C0047B2E8 /* AppIcon.xcassets in Resources */, + 21D165C32BBEF329001E3D4B /* amplify_outputs.json in Resources */, 56043E9329FC4D33003E3424 /* amplifyconfiguration.json in Resources */, 0311113528EBED6500D58441 /* Tests.xcconfig in Resources */, ); @@ -519,7 +591,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 56B54B1F29FC365C0000DF7D /* Copy amplifyconfiguration */ = { + 56B54B1F29FC365C0000DF7D /* Copy amplifyconfiguration and amplify_outputs */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -528,7 +600,7 @@ ); inputPaths = ( ); - name = "Copy amplifyconfiguration"; + name = "Copy amplifyconfiguration and amplify_outputs"; outputFileListPaths = ( ); outputPaths = ( @@ -560,6 +632,33 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 21F7630C2BD6B8640048845A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 21F7630D2BD6B8640048845A /* AWSS3StoragePluginAccelerateIntegrationTests.swift in Sources */, + 21F7630E2BD6B8640048845A /* AuthSignInHelper.swift in Sources */, + 21F7630F2BD6B8640048845A /* AsyncTesting.swift in Sources */, + 21F763102BD6B8640048845A /* AWSS3StoragePluginGetDataResumabilityTests.swift in Sources */, + 21F763112BD6B8640048845A /* AWSS3StoragePluginUploadMetadataTestCase.swift in Sources */, + 21F763122BD6B8640048845A /* AsyncExpectation.swift in Sources */, + 21F763132BD6B8640048845A /* AWSS3StoragePluginProgressTests.swift in Sources */, + 21F763142BD6B8640048845A /* AWSS3StoragePluginAccessLevelTests.swift in Sources */, + 21F763152BD6B8640048845A /* AWSS3StoragePluginDownloadFileResumabilityTests.swift in Sources */, + 21F763162BD6B8640048845A /* AWSS3StoragePluginPrefixKeyResolverTests.swift in Sources */, + 21F763172BD6B8640048845A /* AWSS3StoragePluginTestBase.swift in Sources */, + 21F763182BD6B8640048845A /* AWSS3StoragePluginUploadFileResumabilityTests.swift in Sources */, + 21F763192BD6B8640048845A /* AWSS3StoragePluginRequestRecorder.swift in Sources */, + 21F7631A2BD6B8640048845A /* AWSS3StoragePluginConfigurationTests.swift in Sources */, + 21F7631B2BD6B8640048845A /* AWSS3StoragePluginPutDataResumabilityTests.swift in Sources */, + 21F7631C2BD6B8640048845A /* AWSS3StoragePluginNegativeTests.swift in Sources */, + 21F7631D2BD6B8640048845A /* AWSS3StoragePluginBasicIntegrationTests.swift in Sources */, + 21F7631E2BD6B8640048845A /* XCTestCase+AsyncTesting.swift in Sources */, + 21F7631F2BD6B8640048845A /* AWSS3StoragePluginOptionsUsabilityTests.swift in Sources */, + 21F763202BD6B8640048845A /* TestConfigHelper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 681D7D352A42637700F7C310 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -660,6 +759,11 @@ isa = PBXTargetDependency; productRef = 03257C1728EBF994005DF425 /* Amplify */; }; + 21F7630A2BD6B8640048845A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 684FB06928BEAF1500C8A6EB /* StorageHostApp */; + targetProxy = 21F7630B2BD6B8640048845A /* PBXContainerItemProxy */; + }; 681D7D742A42648C00F7C310 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 681D7D382A42637700F7C310 /* StorageWatchApp */; @@ -678,6 +782,44 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 21F763242BD6B8640048845A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0311113428EBED6500D58441 /* Tests.xcconfig */; + buildSettings = { + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W3DRXD72QU; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.AWSS3StoragePluginIntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,3"; + }; + name = Debug; + }; + 21F763252BD6B8640048845A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0311113428EBED6500D58441 /* Tests.xcconfig */; + buildSettings = { + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W3DRXD72QU; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.AWSS3StoragePluginIntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,3"; + }; + name = Release; + }; 681D7D482A42637900F7C310 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1054,6 +1196,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 21F763232BD6B8640048845A /* Build configuration list for PBXNativeTarget "AWSS3StoragePluginGen2IntegrationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 21F763242BD6B8640048845A /* Debug */, + 21F763252BD6B8640048845A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 681D7D472A42637900F7C310 /* Build configuration list for PBXNativeTarget "StorageWatchApp" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/xcshareddata/xcschemes/AWSS3StoragePluginGen2IntegrationTests.xcscheme b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/xcshareddata/xcschemes/AWSS3StoragePluginGen2IntegrationTests.xcscheme new file mode 100644 index 0000000000..2acdbc2753 --- /dev/null +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/StorageHostApp.xcodeproj/xcshareddata/xcschemes/AWSS3StoragePluginGen2IntegrationTests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh b/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh index 66e00e5706..170210bc9c 100755 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh @@ -21,12 +21,18 @@ mkdir -p "$DESTINATION_DIR" if [ -f "$SOURCE_DIR/AWSAmplifyStressTests-amplifyconfiguration.json" ]; then cp "$SOURCE_DIR/AWSAmplifyStressTests-amplifyconfiguration.json" "$DESTINATION_DIR/amplifyconfiguration.json" exit 0 +else + touch "$DESTINATION_DIR/amplifyconfiguration.json" fi -if [ -f "$SOURCE_DIR/AWSS3StoragePluginTests-amplifyconfiguration.json" ]; then - cp "$SOURCE_DIR/AWSS3StoragePluginTests-amplifyconfiguration.json" "$DESTINATION_DIR/amplifyconfiguration.json" +if [ -f "$SOURCE_DIR/AWSS3StoragePluginTests-amplify_outputs.json" ]; then + cp "$SOURCE_DIR/AWSS3StoragePluginTests-amplify_outputs.json" "$DESTINATION_DIR/amplify_outputs.json" exit 0 +else + touch "$DESTINATION_DIR/amplify_outputs.json" fi + + exit 0 diff --git a/AmplifyTests/CategoryTests/API/APICategoryConfigurationTests.swift b/AmplifyTests/CategoryTests/API/APICategoryConfigurationTests.swift index c15801e609..8b62beda65 100644 --- a/AmplifyTests/CategoryTests/API/APICategoryConfigurationTests.swift +++ b/AmplifyTests/CategoryTests/API/APICategoryConfigurationTests.swift @@ -7,7 +7,7 @@ import XCTest -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AmplifyTestCommon class APICategoryConfigurationTests: XCTestCase { @@ -36,6 +36,19 @@ class APICategoryConfigurationTests: XCTestCase { XCTAssertNotNil(try Amplify.API.getPlugin(for: "MockAPICategoryPlugin")) } + func testCanConfigureAPIPluginWithAmplifyOutputs() throws { + let plugin = MockAPICategoryPlugin() + try Amplify.add(plugin: plugin) + + let config = AmplifyOutputsData() + + try Amplify.configure(config) + + XCTAssertNotNil(Amplify.API) + XCTAssertNotNil(try Amplify.API.getPlugin(for: "MockAPICategoryPlugin")) + } + + func testCanResetAPIPlugin() async throws { let plugin = MockAPICategoryPlugin() let resetWasInvoked = expectation(description: "reset() was invoked") @@ -57,6 +70,23 @@ class APICategoryConfigurationTests: XCTestCase { await fulfillment(of: [resetWasInvoked], timeout: 1.0) } + func testCanResetAPIPluginFromAmplifyOutputs() async throws { + let plugin = MockAPICategoryPlugin() + let resetWasInvoked = expectation(description: "reset() was invoked") + plugin.listeners.append { message in + if message == "reset" { + resetWasInvoked.fulfill() + } + } + try Amplify.add(plugin: plugin) + + let config = AmplifyOutputsData() + + try Amplify.configure(config) + await Amplify.reset() + await fulfillment(of: [resetWasInvoked], timeout: 1.0) + } + func testResetRemovesAddedPlugin() async throws { let plugin = MockAPICategoryPlugin() try Amplify.add(plugin: plugin) diff --git a/AmplifyTests/CategoryTests/Analytics/AnalyticsCategoryConfigurationTests.swift b/AmplifyTests/CategoryTests/Analytics/AnalyticsCategoryConfigurationTests.swift index 9e1b04cb7a..a3a46e4312 100644 --- a/AmplifyTests/CategoryTests/Analytics/AnalyticsCategoryConfigurationTests.swift +++ b/AmplifyTests/CategoryTests/Analytics/AnalyticsCategoryConfigurationTests.swift @@ -7,7 +7,7 @@ import XCTest -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AmplifyTestCommon class AnalyticsCategoryConfigurationTests: XCTestCase { @@ -36,6 +36,18 @@ class AnalyticsCategoryConfigurationTests: XCTestCase { XCTAssertNotNil(try Amplify.Analytics.getPlugin(for: "MockAnalyticsCategoryPlugin")) } + func testCanConfigureAnalyticsPluginWithAmplifyOutputs() throws { + let plugin = MockAnalyticsCategoryPlugin() + try Amplify.add(plugin: plugin) + + let config = AmplifyOutputsData() + + try Amplify.configure(config) + + XCTAssertNotNil(Amplify.Analytics) + XCTAssertNotNil(try Amplify.Analytics.getPlugin(for: "MockAnalyticsCategoryPlugin")) + } + func testCanResetAnalyticsPlugin() async throws { let plugin = MockAnalyticsCategoryPlugin() let resetWasInvoked = expectation(description: "reset() was invoked") @@ -57,6 +69,23 @@ class AnalyticsCategoryConfigurationTests: XCTestCase { await fulfillment(of: [resetWasInvoked], timeout: 1.0) } + func testCanResetAnalyticsPluginFromAmplifyOutputs() async throws { + let plugin = MockAnalyticsCategoryPlugin() + let resetWasInvoked = expectation(description: "reset() was invoked") + plugin.listeners.append { message in + if message == "reset" { + resetWasInvoked.fulfill() + } + } + try Amplify.add(plugin: plugin) + + let config = AmplifyOutputsData() + + try Amplify.configure(config) + await Amplify.reset() + await fulfillment(of: [resetWasInvoked], timeout: 1.0) + } + func testResetRemovesAddedPlugin() async throws { let plugin = MockAnalyticsCategoryPlugin() try Amplify.add(plugin: plugin) diff --git a/AmplifyTests/CategoryTests/Auth/AuthCategoryConfigurationTests.swift b/AmplifyTests/CategoryTests/Auth/AuthCategoryConfigurationTests.swift index f9c3b38696..5035213a5a 100644 --- a/AmplifyTests/CategoryTests/Auth/AuthCategoryConfigurationTests.swift +++ b/AmplifyTests/CategoryTests/Auth/AuthCategoryConfigurationTests.swift @@ -7,7 +7,7 @@ import XCTest -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AmplifyTestCommon class AuthCategoryConfigurationTests: XCTestCase { @@ -53,6 +53,25 @@ class AuthCategoryConfigurationTests: XCTestCase { XCTAssertNotNil(try Amplify.Auth.getPlugin(for: "MockAuthCategoryPlugin")) } + /// Test if Auth plugin can be configured with AmplifyOutputs + /// + /// - Given: UnConfigured Amplify framework + /// - When: + /// - I add a new Auth plugin and add configuration + /// - Then: + /// - Auth plugin should be configured correctly + /// + func testCanConfigureCategoryWithAmplifyOutputs() throws { + let plugin = MockAuthCategoryPlugin() + try Amplify.add(plugin: plugin) + + let config = AmplifyOutputsData() + try Amplify.configure(config) + + XCTAssertNotNil(Amplify.Auth) + XCTAssertNotNil(try Amplify.Auth.getPlugin(for: "MockAuthCategoryPlugin")) + } + /// Test if resetting Auth category works /// /// - Given: Amplify framework configured with Auth plugin diff --git a/AmplifyTests/CategoryTests/DataStore/DataStoreCategoryConfigurationTests.swift b/AmplifyTests/CategoryTests/DataStore/DataStoreCategoryConfigurationTests.swift index 871f372c46..7333c2654b 100644 --- a/AmplifyTests/CategoryTests/DataStore/DataStoreCategoryConfigurationTests.swift +++ b/AmplifyTests/CategoryTests/DataStore/DataStoreCategoryConfigurationTests.swift @@ -7,7 +7,7 @@ import XCTest -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AmplifyTestCommon class DataStoreCategoryConfigurationTests: XCTestCase { @@ -38,6 +38,24 @@ class DataStoreCategoryConfigurationTests: XCTestCase { wait(for: [methodInvokedOnDefaultPlugin], timeout: 1.0) } + func testCanConfigureWithAmplifyOutputs() throws { + let plugin = MockDataStoreCategoryPlugin() + let methodInvokedOnDefaultPlugin = expectation(description: "test method invoked on default plugin") + plugin.listeners.append { message in + if message == "configure(using:)" { + methodInvokedOnDefaultPlugin.fulfill() + } + } + + try Amplify.add(plugin: plugin) + let amplifyOutputs = AmplifyOutputsData() + try Amplify.configure(amplifyOutputs) + + XCTAssertNotNil(Amplify.DataStore) + XCTAssertNotNil(Amplify.DataStore.plugin) + wait(for: [methodInvokedOnDefaultPlugin], timeout: 1.0) + } + func testCanConfigureDataStorePlugin() throws { let plugin = MockDataStoreCategoryPlugin() try Amplify.add(plugin: plugin) diff --git a/AmplifyTests/CategoryTests/Geo/GeoCategoryConfigurationTests.swift b/AmplifyTests/CategoryTests/Geo/GeoCategoryConfigurationTests.swift index 7233623ffc..0540cb719e 100644 --- a/AmplifyTests/CategoryTests/Geo/GeoCategoryConfigurationTests.swift +++ b/AmplifyTests/CategoryTests/Geo/GeoCategoryConfigurationTests.swift @@ -7,7 +7,7 @@ import XCTest -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AmplifyTestCommon class GeoCategoryConfigurationTests: XCTestCase { @@ -36,6 +36,17 @@ class GeoCategoryConfigurationTests: XCTestCase { XCTAssertNotNil(try Amplify.Geo.getPlugin(for: "MockGeoCategoryPlugin")) } + func testCanConfigureGeoPluginWithAmplifyOutputs() throws { + let plugin = MockGeoCategoryPlugin() + try Amplify.add(plugin: plugin) + + let config = AmplifyOutputsData() + try Amplify.configure(config) + + XCTAssertNotNil(Amplify.Geo) + XCTAssertNotNil(try Amplify.Geo.getPlugin(for: "MockGeoCategoryPlugin")) + } + func testCanResetGeoPlugin() async throws { let plugin = MockGeoCategoryPlugin() let resetWasInvoked = expectation(description: "reset() was invoked") diff --git a/AmplifyTests/CategoryTests/Hub/HubCategoryConfigurationTests.swift b/AmplifyTests/CategoryTests/Hub/HubCategoryConfigurationTests.swift index a2d8f2d903..ffaddccf74 100644 --- a/AmplifyTests/CategoryTests/Hub/HubCategoryConfigurationTests.swift +++ b/AmplifyTests/CategoryTests/Hub/HubCategoryConfigurationTests.swift @@ -7,7 +7,7 @@ import XCTest -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AmplifyTestCommon class HubCategoryConfigurationTests: XCTestCase { @@ -36,6 +36,18 @@ class HubCategoryConfigurationTests: XCTestCase { XCTAssertNotNil(try Amplify.Hub.getPlugin(for: "MockHubCategoryPlugin")) } + func testCanConfigureHubPluginWithAmplifyOutputs() throws { + let plugin = MockHubCategoryPlugin() + try Amplify.add(plugin: plugin) + + let config = AmplifyOutputsData() + + try Amplify.configure(config) + + XCTAssertNotNil(Amplify.Hub) + XCTAssertNotNil(try Amplify.Hub.getPlugin(for: "MockHubCategoryPlugin")) + } + func testCanResetHubPlugin() async throws { let plugin = MockHubCategoryPlugin() let resetWasInvoked = expectation(description: "reset() was invoked") @@ -57,6 +69,23 @@ class HubCategoryConfigurationTests: XCTestCase { await fulfillment(of: [resetWasInvoked], timeout: 1.0) } + func testCanResetHubPluginFromAmplifyOutputs() async throws { + let plugin = MockHubCategoryPlugin() + let resetWasInvoked = expectation(description: "reset() was invoked") + plugin.listeners.append { message in + if message == "reset" { + resetWasInvoked.fulfill() + } + } + try Amplify.add(plugin: plugin) + + let config = AmplifyOutputsData() + + try Amplify.configure(config) + await Amplify.reset() + await fulfillment(of: [resetWasInvoked], timeout: 1.0) + } + func testResetRemovesAddedPlugin() async throws { let plugin = MockHubCategoryPlugin() try Amplify.add(plugin: plugin) diff --git a/AmplifyTests/CategoryTests/Logging/LoggingCategoryConfigurationTests.swift b/AmplifyTests/CategoryTests/Logging/LoggingCategoryConfigurationTests.swift index 72d8db4939..78e96f98ff 100644 --- a/AmplifyTests/CategoryTests/Logging/LoggingCategoryConfigurationTests.swift +++ b/AmplifyTests/CategoryTests/Logging/LoggingCategoryConfigurationTests.swift @@ -7,7 +7,7 @@ import XCTest -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AmplifyTestCommon class LoggingCategoryConfigurationTests: XCTestCase { @@ -36,6 +36,19 @@ class LoggingCategoryConfigurationTests: XCTestCase { XCTAssertNotNil(try Amplify.Logging.getPlugin(for: "MockLoggingCategoryPlugin")) } + func testCanConfigureLoggingPluginWithAmplifyOutputs() throws { + let plugin = MockLoggingCategoryPlugin() + try Amplify.add(plugin: plugin) + + let config = AmplifyOutputsData() + + try Amplify.configure(config) + + XCTAssertNotNil(Amplify.Logging) + XCTAssertNotNil(try Amplify.Logging.getPlugin(for: "MockLoggingCategoryPlugin")) + } + + func testCanResetLoggingPlugin() async throws { let plugin = MockLoggingCategoryPlugin() let resetWasInvoked = expectation(description: "reset() was invoked") diff --git a/AmplifyTests/CategoryTests/Notifications/Push/PushNotificationsCategoryConfigurationTests.swift b/AmplifyTests/CategoryTests/Notifications/Push/PushNotificationsCategoryConfigurationTests.swift index 8f843a41ac..9631f0cd42 100644 --- a/AmplifyTests/CategoryTests/Notifications/Push/PushNotificationsCategoryConfigurationTests.swift +++ b/AmplifyTests/CategoryTests/Notifications/Push/PushNotificationsCategoryConfigurationTests.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AmplifyTestCommon import XCTest @@ -128,6 +128,22 @@ class PushNotificationsCategoryConfigurationTests: XCTestCase { // MARK: - Category tests + func testUsingAmplifyOutputs_withConfiguredPlugin_shouldSucceed() async throws { + let plugin = MockPushNotificationsCategoryPlugin() + let methodInvokedOnDefaultPlugin = expectation(description: "test method invoked on default plugin") + plugin.listeners.append { message in + if message == "identifyUser(userId:test)" { + methodInvokedOnDefaultPlugin.fulfill() + } + } + try Amplify.add(plugin: plugin) + let config = AmplifyOutputsData() + try Amplify.configure(config) + + try await Amplify.Notifications.Push.identifyUser(userId: "test") + await fulfillment(of: [methodInvokedOnDefaultPlugin], timeout: 1.0) + } + func testUsingCategory_withConfiguredPlugin_shouldSucceed() async throws { let plugin = MockPushNotificationsCategoryPlugin() let methodInvokedOnDefaultPlugin = expectation(description: "test method invoked on default plugin") diff --git a/AmplifyTests/CategoryTests/Predictions/PredictionsCategoryConfigurationTests.swift b/AmplifyTests/CategoryTests/Predictions/PredictionsCategoryConfigurationTests.swift index 7e80de040e..7553caae0e 100644 --- a/AmplifyTests/CategoryTests/Predictions/PredictionsCategoryConfigurationTests.swift +++ b/AmplifyTests/CategoryTests/Predictions/PredictionsCategoryConfigurationTests.swift @@ -7,7 +7,7 @@ import XCTest -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AmplifyTestCommon class PredictionsCategoryConfigurationTests: XCTestCase { @@ -53,6 +53,26 @@ class PredictionsCategoryConfigurationTests: XCTestCase { XCTAssertNotNil(try Amplify.Predictions.getPlugin(for: "MockPredictionsCategoryPlugin")) } + /// Test if Prediction plugin can be configured with AmplifyOutputs + /// + /// - Given: UnConfigured Amplify framework + /// - When: + /// - I add a new Prediction plugin and add configuration for the plugin + /// - Then: + /// - Prediction plugin should be configured correctly + /// + func testCanConfigurePluginWithAmplifyOutputs() throws { + let plugin = MockPredictionsCategoryPlugin() + try Amplify.add(plugin: plugin) + + let config = AmplifyOutputsData() + + try Amplify.configure(config) + + XCTAssertNotNil(Amplify.Predictions) + XCTAssertNotNil(try Amplify.Predictions.getPlugin(for: "MockPredictionsCategoryPlugin")) + } + /// Test if resetting Prediction category works /// /// - Given: Amplify framework configured with Prediction plugin diff --git a/AmplifyTests/CategoryTests/Storage/StorageCategoryConfigurationTests.swift b/AmplifyTests/CategoryTests/Storage/StorageCategoryConfigurationTests.swift index cccf40b9a1..1c27a6779e 100644 --- a/AmplifyTests/CategoryTests/Storage/StorageCategoryConfigurationTests.swift +++ b/AmplifyTests/CategoryTests/Storage/StorageCategoryConfigurationTests.swift @@ -7,7 +7,7 @@ import XCTest -@testable import Amplify +@_spi(InternalAmplifyConfiguration) @testable import Amplify @testable import AmplifyTestCommon class StorageCategoryConfigurationTests: XCTestCase { @@ -36,6 +36,18 @@ class StorageCategoryConfigurationTests: XCTestCase { XCTAssertNotNil(try Amplify.Storage.getPlugin(for: "MockStorageCategoryPlugin")) } + func testCanConfigureStoragePluginWithAmplifyOutputs() throws { + let plugin = MockStorageCategoryPlugin() + try Amplify.add(plugin: plugin) + + let config = AmplifyOutputsData() + + try Amplify.configure(config) + + XCTAssertNotNil(Amplify.Storage) + XCTAssertNotNil(try Amplify.Storage.getPlugin(for: "MockStorageCategoryPlugin")) + } + func testCanResetStoragePlugin() async throws { let plugin = MockStorageCategoryPlugin() let resetWasInvoked = expectation(description: "reset() was invoked") @@ -115,6 +127,7 @@ class StorageCategoryConfigurationTests: XCTestCase { try Amplify.configure(amplifyConfig) _ = Amplify.Storage.downloadData(key: "", options: nil) + await fulfillment(of: [methodInvokedOnDefaultPlugin], timeout: 1.0) } func testCanUseSpecifiedPlugin() async throws { diff --git a/AmplifyTests/CoreTests/AmplifyOutputsInitializationTests.swift b/AmplifyTests/CoreTests/AmplifyOutputsInitializationTests.swift new file mode 100644 index 0000000000..8d34e3b768 --- /dev/null +++ b/AmplifyTests/CoreTests/AmplifyOutputsInitializationTests.swift @@ -0,0 +1,143 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +@_spi(InternalAmplifyConfiguration) @testable import Amplify +@testable import AmplifyTestCommon + +/// Uses internal methods of the Amplify configuration system to ensure we are throwing expected errors in exceptional +/// circumstances +class AmplifyOutputsInitializationTests: XCTestCase { + + static var tempDir: URL = { + let fileManager = FileManager.default + let tempDir = fileManager.temporaryDirectory.appendingPathComponent("ConfigurationInternalsTests") + return tempDir + }() + + override func setUp() { + do { + try AmplifyOutputsInitializationTests.makeTempDir() + } catch { + XCTFail("Could not make test bundle container directory: \(error.localizedDescription)") + } + } + + override func tearDown() async throws { + do { + await Amplify.reset() + try AmplifyOutputsInitializationTests.removeTempDir() + } catch { + XCTFail("Could not remove temporary directory: \(error.localizedDescription)") + } + } + + /// Given: A bundle that doesn't contain the file specified by `resource` + /// When: Amplify.configure(with: .resource(named:) is invoked + /// Then: The system throws a ConfigurationError.amplifyConfigurationFileNotFound error + func testFileNotFoundInBundle() { + guard let testBundle = try? AmplifyOutputsInitializationTests.makeTestBundle() else { + XCTFail("Unable to create testBundle") + return + } + + XCTAssertThrowsError(try AmplifyOutputsData.init(bundle: testBundle, resource: "invalidFile")) { error in + if case ConfigurationError.invalidAmplifyOutputsFile = error { + return + } + XCTFail("Expected ConfigurationError.invalidAmplifyOutputsFile, got \(error)") + } + } + + /// Given: An data object with bad UTF8 data + /// When: Amplify.configure(with: .data(:)) is invoked + /// Then: The system throws a ConfigurationError.unableToDecode error + func testInvalidUTF8Data() throws { + // A unicode character whose bit pattern begins with a "1" is supposed to be part of a multibyte sequence + let badUTF8Bytes = Data([0xc0, 0x20]) + + XCTAssertThrowsError(try AmplifyOutputs.data(badUTF8Bytes).resolveConfiguration()) { error in + if case ConfigurationError.unableToDecode = error { + return + } + XCTFail("Expected ConfigurationError.unableToDecode, got \(error)") + } + } + + /// Given: A data object with invalid JSON data + /// When: Amplify.configure(with: .data(:)) is invoked + /// Then: The system throws a ConfigurationError.unableToDecode error + func testInvalidJSON() throws { + let poorlyFormedJSON = #"{"foo"}"#.data(using: .utf8)! + + XCTAssertThrowsError(try AmplifyOutputs.data(poorlyFormedJSON).resolveConfiguration()) { error in + if case ConfigurationError.unableToDecode = error { + return + } + XCTFail("Expected ConfigurationError.unableToDecode, got \(error)") + } + } + + + /// Given: A data object with valid AmplifyOutputs JSON + /// When: Amplify.configure(with: .data(:)) is invoked + /// Then: Decoded data should contain the correct data, decoding snake case to camel case. + func testValidAmplifyOutputsJSON() throws { + let validAmplifyOutputsJSON = #"{"version": "1", "analytics": { "amazon_pinpoint": { "aws_region": "us-east-1", "app_id": "app123"}}}"# + let configData = Data(validAmplifyOutputsJSON.utf8) + + try Amplify.configure(with: .data(configData)) + let config = try AmplifyOutputsData.decodeAmplifyOutputsData(from: configData) + XCTAssertEqual(config.version, "1") + XCTAssertEqual(config.analytics?.amazonPinpoint?.appId, "app123") + XCTAssertEqual(config.analytics?.amazonPinpoint?.awsRegion, "us-east-1") + } + + /// - Given: A valid configuration + /// - When: + /// - Amplify is finished configuring its plugins + /// - Then: + /// - I receive a Hub event + func testConfigurationNotification() async throws { + let notificationReceived = expectation(description: "Configured notification received") + let listeningPlugin = NotificationListeningAnalyticsPlugin(notificationReceived: notificationReceived) + await Amplify.reset() + try Amplify.add(plugin: listeningPlugin) + let config = AmplifyOutputsData() + try Amplify.configure(config) + + await fulfillment(of: [notificationReceived], timeout: 1.0) + } + + // MARK: - Utilities + + /// Creates the directory used as the container for the test bundle; each test will need this. + static func makeTempDir() throws { + try FileManager.default.createDirectory(at: tempDir, + withIntermediateDirectories: true) + } + + /// Creates a Bundle object from the container directory + static func makeTestBundle() throws -> Bundle { + let customBundleDir = tempDir.appendingPathComponent("TestBundle.bundle") + + try FileManager.default.createDirectory(at: customBundleDir, + withIntermediateDirectories: true) + + guard let testBundle = Bundle(path: customBundleDir.path) else { + throw "Could not create test bundle at \(customBundleDir.path)" + } + + return testBundle + } + + /// Removes the container directory used for the test bundle + static func removeTempDir() throws { + try FileManager.default.removeItem(at: tempDir) + } +} + From 690dbc452c4d1fa1f75340c4b761cd91fba294f7 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:24:58 -0400 Subject: [PATCH 14/26] test(storage): fix copy_configuration script (#3635) --- .../Storage/Tests/StorageHostApp/copy_configuration.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh b/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh index 170210bc9c..520c4bb49b 100755 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh @@ -25,6 +25,13 @@ else touch "$DESTINATION_DIR/amplifyconfiguration.json" fi +if [ -f "$SOURCE_DIR/AWSS3StoragePluginTests-amplifyconfiguration.json" ]; then + cp "$SOURCE_DIR/AWSS3StoragePluginTests-amplifyconfiguration.json" "$DESTINATION_DIR/amplifyconfiguration.json" + exit 0 +fi + touch "$DESTINATION_DIR/amplifyconfiguration.json" +fi + if [ -f "$SOURCE_DIR/AWSS3StoragePluginTests-amplify_outputs.json" ]; then cp "$SOURCE_DIR/AWSS3StoragePluginTests-amplify_outputs.json" "$DESTINATION_DIR/amplify_outputs.json" exit 0 From ad06953e35fcb3ea7525ad937cd18902350a5897 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Thu, 25 Apr 2024 18:38:12 -0400 Subject: [PATCH 15/26] test(storage): fix copy_configuration script early exit (#3637) * test(storage): fix copy_configuration script early exit * typo --- .../Storage/Tests/StorageHostApp/copy_configuration.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh b/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh index 520c4bb49b..aa49ac5497 100755 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh @@ -27,19 +27,15 @@ fi if [ -f "$SOURCE_DIR/AWSS3StoragePluginTests-amplifyconfiguration.json" ]; then cp "$SOURCE_DIR/AWSS3StoragePluginTests-amplifyconfiguration.json" "$DESTINATION_DIR/amplifyconfiguration.json" - exit 0 -fi +else touch "$DESTINATION_DIR/amplifyconfiguration.json" fi if [ -f "$SOURCE_DIR/AWSS3StoragePluginTests-amplify_outputs.json" ]; then cp "$SOURCE_DIR/AWSS3StoragePluginTests-amplify_outputs.json" "$DESTINATION_DIR/amplify_outputs.json" - exit 0 else touch "$DESTINATION_DIR/amplify_outputs.json" fi - - exit 0 From 038733fd266081a794c2e935bdae2fd3e4f0e51d Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Thu, 25 Apr 2024 19:29:16 -0400 Subject: [PATCH 16/26] test(storage): fix copy_configuration script for stress test (#3639) --- .../Storage/Tests/StorageHostApp/copy_configuration.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh b/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh index aa49ac5497..06bc88963e 100755 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/copy_configuration.sh @@ -20,9 +20,8 @@ mkdir -p "$DESTINATION_DIR" if [ -f "$SOURCE_DIR/AWSAmplifyStressTests-amplifyconfiguration.json" ]; then cp "$SOURCE_DIR/AWSAmplifyStressTests-amplifyconfiguration.json" "$DESTINATION_DIR/amplifyconfiguration.json" + touch "$DESTINATION_DIR/amplify_outputs.json" exit 0 -else - touch "$DESTINATION_DIR/amplifyconfiguration.json" fi if [ -f "$SOURCE_DIR/AWSS3StoragePluginTests-amplifyconfiguration.json" ]; then From 71eb6e7d215b65be0e0dfd96a56d3cb6a967cd7e Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Fri, 26 Apr 2024 04:15:12 +0000 Subject: [PATCH 17/26] chore: release 2.30.0 [skip ci] --- .../ServiceConfiguration/AmplifyAWSServiceConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift b/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift index 4982582d64..c8b51bfa62 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift @@ -15,7 +15,7 @@ import Amplify public class AmplifyAWSServiceConfiguration { /// - Tag: AmplifyAWSServiceConfiguration.amplifyVersion - public static let amplifyVersion = "2.29.3" + public static let amplifyVersion = "2.30.0" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From a86dc2e2ec7bbc3b368628bda64b871beaec4441 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Fri, 26 Apr 2024 04:20:16 +0000 Subject: [PATCH 18/26] chore: finalize release 2.30.0 [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e35310cc92..613503d0a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.30.0 (2024-04-26) + +### Features + +- **all**: Configure plugins with AmplifyOutputs (#3567) + ## 2.29.3 (2024-04-22) ### Bug Fixes From 3ffde2b688da605a3b1e7591883bdc381a4e7600 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Fri, 26 Apr 2024 00:40:56 -0400 Subject: [PATCH 19/26] feat(api): add authorizationMode to GraphQLRequest (#3630) * feat(api): add authorizationMode to GraphQLRequest * add unit tests * finalize API * remove appSync api * Delete AmplifyPlugins/Storage/Tests/StorageHostApp/amplify_outputs.json * Delete AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/AWSAPIPluginGen2LazyLoadTests.xcscheme --- .../API/Request/GraphQLOperationRequest.swift | 5 + .../API/Request/GraphQLRequest.swift | 8 ++ .../Operation/AWSGraphQLOperation.swift | 9 +- .../AWSGraphQLSubscriptionTaskRunner.swift | 22 ++- .../GraphQLRequest+toOperationRequest.swift | 1 + ...ICategoryPlugin+GraphQLBehaviorTests.swift | 10 +- .../Operation/AWSGraphQLOperationTests.swift | 42 ++++++ .../Operation/OperationTestBase.swift | 4 +- .../Auth/AWSAuthorizationType.swift | 3 +- .../GraphQLRequest/GraphQLRequest+Model.swift | 135 +++++++++++++----- .../GraphQLRequestModelTests.swift | 38 +++-- 11 files changed, 219 insertions(+), 58 deletions(-) diff --git a/Amplify/Categories/API/Request/GraphQLOperationRequest.swift b/Amplify/Categories/API/Request/GraphQLOperationRequest.swift index 99115342bb..2f5ebf1ed2 100644 --- a/Amplify/Categories/API/Request/GraphQLOperationRequest.swift +++ b/Amplify/Categories/API/Request/GraphQLOperationRequest.swift @@ -25,6 +25,9 @@ public struct GraphQLOperationRequest: AmplifyOperationRequest { /// The path to traverse before decoding to `responseType`. public let decodePath: String? + /// The authorization mode + public let authMode: AuthorizationMode? + /// Options to adjust the behavior of this request, including plugin-options public let options: Options @@ -35,6 +38,7 @@ public struct GraphQLOperationRequest: AmplifyOperationRequest { variables: [String: Any]? = nil, responseType: R.Type, decodePath: String? = nil, + authMode: AuthorizationMode? = nil, options: Options) { self.apiName = apiName self.operationType = operationType @@ -42,6 +46,7 @@ public struct GraphQLOperationRequest: AmplifyOperationRequest { self.variables = variables self.responseType = responseType self.decodePath = decodePath + self.authMode = authMode self.options = options } } diff --git a/Amplify/Categories/API/Request/GraphQLRequest.swift b/Amplify/Categories/API/Request/GraphQLRequest.swift index 5c566d2fca..ba0086de66 100644 --- a/Amplify/Categories/API/Request/GraphQLRequest.swift +++ b/Amplify/Categories/API/Request/GraphQLRequest.swift @@ -5,6 +5,9 @@ // SPDX-License-Identifier: Apache-2.0 // +/// Empty protocol for plugins to define specific `AuthorizationMode` types for the request. +public protocol AuthorizationMode { } + /// GraphQL Request public struct GraphQLRequest { @@ -21,6 +24,9 @@ public struct GraphQLRequest { /// Type to decode the graphql response data object to public let responseType: R.Type + /// The authorization mode + public let authMode: AuthorizationMode? + /// The path to decode to the graphQL response data to `responseType`. Delimited by `.` The decode path /// "listTodos.items" will traverse to the object at `listTodos`, and decode the object at `items` to `responseType` /// The data at that decode path is a list of Todo objects so `responseType` should be `[Todo].self` @@ -34,11 +40,13 @@ public struct GraphQLRequest { variables: [String: Any]? = nil, responseType: R.Type, decodePath: String? = nil, + authMode: AuthorizationMode? = nil, options: GraphQLRequest.Options? = nil) { self.apiName = apiName self.document = document self.variables = variables self.responseType = responseType + self.authMode = authMode self.decodePath = decodePath self.options = options } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift index d57c2ba1c4..b3a61608fb 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift @@ -46,7 +46,7 @@ final public class AWSGraphQLOperation: GraphQLOperation { } let urlRequest = validateRequest(request).flatMap(buildURLRequest(from:)) - let finalRequest = await getEndpointInterceptors(from: request).flatMapAsync { requestInterceptors in + let finalRequest = await getEndpointInterceptors().flatMapAsync { requestInterceptors in let preludeInterceptors = requestInterceptors?.preludeInterceptors ?? [] let customerInterceptors = requestInterceptors?.interceptors ?? [] let postludeInterceptors = requestInterceptors?.postludeInterceptors ?? [] @@ -150,7 +150,7 @@ final public class AWSGraphQLOperation: GraphQLOperation { } } - private func getEndpointInterceptors(from request: GraphQLOperationRequest) -> Result { + func getEndpointInterceptors() -> Result { getEndpointConfig(from: request).flatMap { endpointConfig in do { if let pluginOptions = request.options.pluginOptions as? AWSAPIPluginDataStoreOptions, @@ -159,6 +159,11 @@ final public class AWSGraphQLOperation: GraphQLOperation { withConfig: endpointConfig, authType: authType )) + } else if let authType = request.authMode as? AWSAuthorizationType { + return .success(try pluginConfig.interceptorsForEndpoint( + withConfig: endpointConfig, + authType: authType + )) } else { return .success(pluginConfig.interceptorsForEndpoint(withConfig: endpointConfig)) } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift index 12427ad9ab..44e2cf378d 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift @@ -91,14 +91,21 @@ public class AWSGraphQLSubscriptionTaskRunner: InternalTaskRunner, return } - let pluginOptions = request.options.pluginOptions as? AWSAPIPluginDataStoreOptions + let authType: AWSAuthorizationType? + if let pluginOptions = request.options.pluginOptions as? AWSAPIPluginDataStoreOptions { + authType = pluginOptions.authType + } else if let authorizationMode = request.authMode as? AWSAuthorizationType { + authType = authorizationMode + } else { + authType = nil + } // Retrieve the subscription connection do { self.appSyncClient = try await appSyncClientFactory.getAppSyncRealTimeClient( for: endpointConfig, endpoint: endpointConfig.baseURL, authService: authService, - authType: pluginOptions?.authType, + authType: authType, apiAuthProviderFactory: apiAuthProviderFactory ) @@ -262,14 +269,21 @@ final public class AWSGraphQLSubscriptionOperation: GraphQLSubscri return } - let pluginOptions = request.options.pluginOptions as? AWSAPIPluginDataStoreOptions + let authType: AWSAuthorizationType? + if let pluginOptions = request.options.pluginOptions as? AWSAPIPluginDataStoreOptions { + authType = pluginOptions.authType + } else if let authorizationMode = request.authMode as? AWSAuthorizationType { + authType = authorizationMode + } else { + authType = nil + } Task { do { appSyncRealTimeClient = try await appSyncRealTimeClientFactory.getAppSyncRealTimeClient( for: endpointConfig, endpoint: endpointConfig.baseURL, authService: authService, - authType: pluginOptions?.authType, + authType: authType, apiAuthProviderFactory: apiAuthProviderFactory ) diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/GraphQLRequest+toOperationRequest.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/GraphQLRequest+toOperationRequest.swift index 1079685b66..ba518bbe6e 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/GraphQLRequest+toOperationRequest.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/GraphQLRequest+toOperationRequest.swift @@ -16,6 +16,7 @@ extension GraphQLRequest { variables: variables, responseType: responseType, decodePath: decodePath, + authMode: authMode, options: requestOptions) } } diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+GraphQLBehaviorTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+GraphQLBehaviorTests.swift index 098d3ae447..16b4ff573c 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+GraphQLBehaviorTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+GraphQLBehaviorTests.swift @@ -8,6 +8,7 @@ import XCTest import Amplify @testable import AWSAPIPlugin +import AWSPluginsCore class AWSAPICategoryPluginGraphQLBehaviorTests: AWSAPICategoryPluginTestBase { @@ -15,10 +16,11 @@ class AWSAPICategoryPluginGraphQLBehaviorTests: AWSAPICategoryPluginTestBase { func testQuery() { let operationFinished = expectation(description: "Operation should finish") - let request = GraphQLRequest(apiName: apiName, - document: testDocument, - variables: nil, - responseType: JSONValue.self) + let request = GraphQLRequest(apiName: apiName, + document: testDocument, + variables: nil, + responseType: JSONValue.self, + authMode: AWSAuthorizationType.apiKey) let operation = apiPlugin.query(request: request) { _ in operationFinished.fulfill() } diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLOperationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLOperationTests.swift index 93af2539b2..717a87f5ab 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLOperationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLOperationTests.swift @@ -9,6 +9,8 @@ import XCTest @testable import Amplify @testable import AmplifyTestCommon @testable import AWSAPIPlugin +@testable import AWSPluginsTestCommon +import AWSPluginsCore class AWSGraphQLOperationTests: AWSAPICategoryPluginTestBase { @@ -37,4 +39,44 @@ class AWSGraphQLOperationTests: AWSAPICategoryPluginTestBase { XCTAssertNil(task) } + + /// Request for `.amazonCognitoUserPool` at runtime with `request` while passing in what + /// is configured as `.apiKey`. Expect that the interceptor is the token interceptor + func testGetEndpointInterceptors() throws { + let request = GraphQLRequest(apiName: apiName, + document: testDocument, + variables: nil, + responseType: JSONValue.self, + authMode: AWSAuthorizationType.amazonCognitoUserPools) + let task = try OperationTestBase.makeSingleValueErrorMockTask() + let mockSession = MockURLSession(onTaskForRequest: { _ in task }) + let pluginConfig = AWSAPICategoryPluginConfiguration( + endpoints: [ + apiName: try .init( + name: apiName, + baseURL: URL(string: "url")!, + region: "us-test-1", + authorizationType: .apiKey, + endpointType: .graphQL, + apiKey: "apiKey", + apiAuthProviderFactory: .init())], + apiAuthProviderFactory: .init(), + authService: MockAWSAuthService()) + let operation = AWSGraphQLOperation(request: request.toOperationRequest(operationType: .query), + session: mockSession, + mapper: OperationTaskMapper(), + pluginConfig: pluginConfig, + resultListener: { _ in }) + + // Act + let results = operation.getEndpointInterceptors() + + // Assert + guard case let .success(interceptors) = results, + let interceptor = interceptors?.preludeInterceptors.first, + (interceptor as? AuthTokenURLRequestInterceptor) != nil else { + XCTFail("Should be token interceptor for Cognito User Pool") + return + } + } } diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/OperationTestBase.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/OperationTestBase.swift index 26d9014091..b36ea8ccf0 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/OperationTestBase.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/OperationTestBase.swift @@ -59,7 +59,7 @@ class OperationTestBase: XCTestCase { } func setUpPluginForSingleError(for endpointType: AWSAPICategoryPluginEndpointType) throws { - let task = try makeSingleValueErrorMockTask() + let task = try Self.makeSingleValueErrorMockTask() let mockSession = MockURLSession(onTaskForRequest: { _ in task }) let sessionFactory = MockSessionFactory(returning: mockSession) try setUpPlugin(sessionFactory: sessionFactory, endpointType: endpointType) @@ -102,7 +102,7 @@ class OperationTestBase: XCTestCase { return task } - func makeSingleValueErrorMockTask() throws -> MockURLSessionTask { + static func makeSingleValueErrorMockTask() throws -> MockURLSessionTask { var mockTask: MockURLSessionTask! mockTask = MockURLSessionTask(onResume: { guard let mockSession = mockTask.mockSession, diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthorizationType.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthorizationType.swift index 0530183e3c..465cc35337 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthorizationType.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthorizationType.swift @@ -6,6 +6,7 @@ // import Foundation +import Amplify // swiftlint:disable line_length @@ -13,7 +14,7 @@ import Foundation /// GraphQL backend, or an Amazon API Gateway endpoint. /// /// - SeeAlso: [https://docs.aws.amazon.com/appsync/latest/devguide/security.html](AppSync Security) -public enum AWSAuthorizationType: String { +public enum AWSAuthorizationType: String, AuthorizationMode { /// For public APIs case none = "NONE" diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLRequest/GraphQLRequest+Model.swift b/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLRequest/GraphQLRequest+Model.swift index 7229c4ea1b..7338fab830 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLRequest/GraphQLRequest+Model.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLRequest/GraphQLRequest+Model.swift @@ -35,7 +35,8 @@ protocol ModelGraphQLRequestFactory { static func list(_ modelType: M.Type, where predicate: QueryPredicate?, includes: IncludedAssociations, - limit: Int?) -> GraphQLRequest> + limit: Int?, + authMode: AWSAuthorizationType?) -> GraphQLRequest> /// Creates a `GraphQLRequest` that represents a query that expects a single value as a result. /// The request will be created with the correct correct document based on the `ModelSchema` and @@ -50,16 +51,19 @@ protocol ModelGraphQLRequestFactory { /// - seealso: `GraphQLQuery`, `GraphQLQueryType.get` static func get(_ modelType: M.Type, byId id: String, - includes: IncludedAssociations) -> GraphQLRequest + includes: IncludedAssociations, + authMode: AWSAuthorizationType?) -> GraphQLRequest static func get(_ modelType: M.Type, byIdentifier id: String, - includes: IncludedAssociations) -> GraphQLRequest + includes: IncludedAssociations, + authMode: AWSAuthorizationType?) -> GraphQLRequest where M: ModelIdentifiable, M.IdentifierFormat == ModelIdentifierFormat.Default static func get(_ modelType: M.Type, byIdentifier id: ModelIdentifier, - includes: IncludedAssociations) -> GraphQLRequest + includes: IncludedAssociations, + authMode: AWSAuthorizationType?) -> GraphQLRequest where M: ModelIdentifiable // MARK: Mutation @@ -76,7 +80,8 @@ protocol ModelGraphQLRequestFactory { modelSchema: ModelSchema, where predicate: QueryPredicate?, includes: IncludedAssociations, - type: GraphQLMutationType) -> GraphQLRequest + type: GraphQLMutationType, + authMode: AWSAuthorizationType?) -> GraphQLRequest /// Creates a `GraphQLRequest` that represents a create mutation /// for a given `model` instance. @@ -85,7 +90,9 @@ protocol ModelGraphQLRequestFactory { /// - model: the model instance populated with values /// - Returns: a valid `GraphQLRequest` instance /// - seealso: `GraphQLRequest.mutation(of:where:type:)` - static func create(_ model: M, includes: IncludedAssociations) -> GraphQLRequest + static func create(_ model: M, + includes: IncludedAssociations, + authMode: AWSAuthorizationType?) -> GraphQLRequest /// Creates a `GraphQLRequest` that represents an update mutation /// for a given `model` instance. @@ -97,7 +104,8 @@ protocol ModelGraphQLRequestFactory { /// - seealso: `GraphQLRequest.mutation(of:where:type:)` static func update(_ model: M, where predicate: QueryPredicate?, - includes: IncludedAssociations) -> GraphQLRequest + includes: IncludedAssociations, + authMode: AWSAuthorizationType?) -> GraphQLRequest /// Creates a `GraphQLRequest` that represents a delete mutation /// for a given `model` instance. @@ -109,7 +117,8 @@ protocol ModelGraphQLRequestFactory { /// - seealso: `GraphQLRequest.mutation(of:where:type:)` static func delete(_ model: M, where predicate: QueryPredicate?, - includes: IncludedAssociations) -> GraphQLRequest + includes: IncludedAssociations, + authMode: AWSAuthorizationType?) -> GraphQLRequest // MARK: Subscription @@ -125,7 +134,8 @@ protocol ModelGraphQLRequestFactory { /// - seealso: `GraphQLSubscription`, `GraphQLSubscriptionType` static func subscription(of: M.Type, type: GraphQLSubscriptionType, - includes: IncludedAssociations) -> GraphQLRequest + includes: IncludedAssociations, + authMode: AWSAuthorizationType?) -> GraphQLRequest } // MARK: - Extension @@ -141,52 +151,97 @@ extension GraphQLRequest: ModelGraphQLRequestFactory { return modelType.schema } - public static func create(_ model: M, includes: IncludedAssociations = { _ in [] }) -> GraphQLRequest { - return create(model, modelSchema: modelSchema(for: model), includes: includes) + public static func create( + _ model: M, + includes: IncludedAssociations = { _ in [] }, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest { + return create( + model, + modelSchema: modelSchema(for: model), + includes: includes, + authMode: authMode) } public static func update(_ model: M, where predicate: QueryPredicate? = nil, - includes: IncludedAssociations = { _ in [] }) -> GraphQLRequest { - return update(model, modelSchema: modelSchema(for: model), where: predicate, includes: includes) + includes: IncludedAssociations = { _ in [] }, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest { + return update( + model, + modelSchema: modelSchema(for: model), + where: predicate, + includes: includes, + authMode: authMode) } public static func delete(_ model: M, where predicate: QueryPredicate? = nil, - includes: IncludedAssociations = { _ in [] }) -> GraphQLRequest { - return delete(model, modelSchema: modelSchema(for: model), where: predicate, includes: includes) + includes: IncludedAssociations = { _ in [] }, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest { + return delete( + model, + modelSchema: modelSchema(for: model), + where: predicate, + includes: includes, + authMode: authMode) } - public static func create(_ model: M, modelSchema: ModelSchema, includes: IncludedAssociations = { _ in [] }) -> GraphQLRequest { - return mutation(of: model, modelSchema: modelSchema, includes: includes, type: .create) + public static func create(_ model: M, + modelSchema: ModelSchema, + includes: IncludedAssociations = { _ in [] }, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest { + return mutation(of: model, + modelSchema: modelSchema, + includes: includes, + type: .create, + authMode: authMode) } public static func update(_ model: M, modelSchema: ModelSchema, where predicate: QueryPredicate? = nil, - includes: IncludedAssociations = { _ in [] }) -> GraphQLRequest { - return mutation(of: model, modelSchema: modelSchema, where: predicate, includes: includes, type: .update) + includes: IncludedAssociations = { _ in [] }, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest { + return mutation(of: model, + modelSchema: modelSchema, + where: predicate, + includes: includes, + type: .update, + authMode: authMode) } public static func delete(_ model: M, modelSchema: ModelSchema, where predicate: QueryPredicate? = nil, - includes: IncludedAssociations = { _ in [] }) -> GraphQLRequest { - return mutation(of: model, modelSchema: modelSchema, where: predicate, includes: includes, type: .delete) + includes: IncludedAssociations = { _ in [] }, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest { + return mutation(of: model, + modelSchema: modelSchema, + where: predicate, + includes: includes, + type: .delete, + authMode: authMode) } public static func mutation(of model: M, where predicate: QueryPredicate? = nil, includes: IncludedAssociations = { _ in [] }, - type: GraphQLMutationType) -> GraphQLRequest { - mutation(of: model, modelSchema: model.schema, where: predicate, includes: includes, type: type) + type: GraphQLMutationType, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest { + mutation(of: model, + modelSchema: model.schema, + where: predicate, + includes: includes, + type: type, + authMode: authMode) } public static func mutation(of model: M, modelSchema: ModelSchema, where predicate: QueryPredicate? = nil, includes: IncludedAssociations = { _ in [] }, - type: GraphQLMutationType) -> GraphQLRequest { + type: GraphQLMutationType, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest { var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: modelSchema, operationType: .mutation) documentBuilder.add(decorator: DirectiveNameDecorator(type: type)) @@ -216,12 +271,14 @@ extension GraphQLRequest: ModelGraphQLRequestFactory { return GraphQLRequest(document: document.stringValue, variables: document.variables, responseType: M.self, - decodePath: document.name) + decodePath: document.name, + authMode: authMode) } public static func get(_ modelType: M.Type, byId id: String, - includes: IncludedAssociations = { _ in [] }) -> GraphQLRequest { + includes: IncludedAssociations = { _ in [] }, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest { var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: modelType.schema, operationType: .query) documentBuilder.add(decorator: DirectiveNameDecorator(type: .get)) @@ -237,19 +294,22 @@ extension GraphQLRequest: ModelGraphQLRequestFactory { return GraphQLRequest(document: document.stringValue, variables: document.variables, responseType: M?.self, - decodePath: document.name) + decodePath: document.name, + authMode: authMode) } public static func get(_ modelType: M.Type, byIdentifier id: String, - includes: IncludedAssociations = { _ in [] }) -> GraphQLRequest + includes: IncludedAssociations = { _ in [] }, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest where M: ModelIdentifiable, M.IdentifierFormat == ModelIdentifierFormat.Default { - return .get(modelType, byId: id, includes: includes) + return .get(modelType, byId: id, includes: includes, authMode: authMode) } public static func get(_ modelType: M.Type, byIdentifier id: ModelIdentifier, - includes: IncludedAssociations = { _ in [] }) -> GraphQLRequest + includes: IncludedAssociations = { _ in [] }, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest where M: ModelIdentifiable { var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: modelType.schema, operationType: .query) @@ -265,13 +325,15 @@ extension GraphQLRequest: ModelGraphQLRequestFactory { return GraphQLRequest(document: document.stringValue, variables: document.variables, responseType: M?.self, - decodePath: document.name) + decodePath: document.name, + authMode: authMode) } public static func list(_ modelType: M.Type, where predicate: QueryPredicate? = nil, includes: IncludedAssociations = { _ in [] }, - limit: Int? = nil) -> GraphQLRequest> { + limit: Int? = nil, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest> { let primaryKeysOnly = (M.rootPath != nil) ? true : false var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: modelType.schema, operationType: .query) @@ -292,12 +354,14 @@ extension GraphQLRequest: ModelGraphQLRequestFactory { return GraphQLRequest>(document: document.stringValue, variables: document.variables, responseType: List.self, - decodePath: document.name) + decodePath: document.name, + authMode: authMode) } public static func subscription(of modelType: M.Type, type: GraphQLSubscriptionType, - includes: IncludedAssociations = { _ in [] }) -> GraphQLRequest { + includes: IncludedAssociations = { _ in [] }, + authMode: AWSAuthorizationType? = nil) -> GraphQLRequest { var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: modelType.schema, operationType: .subscription) documentBuilder.add(decorator: DirectiveNameDecorator(type: type)) @@ -312,6 +376,7 @@ extension GraphQLRequest: ModelGraphQLRequestFactory { return GraphQLRequest(document: document.stringValue, variables: document.variables, responseType: modelType, - decodePath: document.name) + decodePath: document.name, + authMode: authMode) } } diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLRequest/GraphQLRequestModelTests.swift b/AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLRequest/GraphQLRequestModelTests.swift index 63ff15e50a..332ec0b328 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLRequest/GraphQLRequestModelTests.swift +++ b/AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLRequest/GraphQLRequestModelTests.swift @@ -29,11 +29,12 @@ class GraphQLRequestModelTest: XCTestCase { documentBuilder.add(decorator: ModelDecorator(model: post, mutationType: .create)) let document = documentBuilder.build() - let request = GraphQLRequest.create(post) + let request = GraphQLRequest.create(post, authMode: .amazonCognitoUserPools) XCTAssertEqual(document.stringValue, request.document) XCTAssert(request.responseType == Post.self) XCTAssert(request.variables != nil) + assertEquals(actualAuthMode: request.authMode, expectedAuthMode: .amazonCognitoUserPools) } func testUpdateMutationGraphQLRequest() { @@ -43,11 +44,12 @@ class GraphQLRequestModelTest: XCTestCase { documentBuilder.add(decorator: ModelDecorator(model: post, mutationType: .update)) let document = documentBuilder.build() - let request = GraphQLRequest.update(post) + let request = GraphQLRequest.update(post, authMode: .amazonCognitoUserPools) XCTAssertEqual(document.stringValue, request.document) XCTAssert(request.responseType == Post.self) XCTAssert(request.variables != nil) + assertEquals(actualAuthMode: request.authMode, expectedAuthMode: .amazonCognitoUserPools) } func testDeleteMutationGraphQLRequest() { @@ -57,11 +59,12 @@ class GraphQLRequestModelTest: XCTestCase { documentBuilder.add(decorator: ModelDecorator(model: post, mutationType: .delete)) let document = documentBuilder.build() - let request = GraphQLRequest.delete(post) + let request = GraphQLRequest.delete(post, authMode: .amazonCognitoUserPools) XCTAssertEqual(document.stringValue, request.document) XCTAssert(request.responseType == Post.self) XCTAssert(request.variables != nil) + assertEquals(actualAuthMode: request.authMode, expectedAuthMode: .amazonCognitoUserPools) } func testQueryByIdGraphQLRequest() { @@ -70,11 +73,12 @@ class GraphQLRequestModelTest: XCTestCase { documentBuilder.add(decorator: ModelIdDecorator(id: "id")) let document = documentBuilder.build() - let request = GraphQLRequest.get(Post.self, byId: "id") + let request = GraphQLRequest.get(Post.self, byId: "id", authMode: .amazonCognitoUserPools) XCTAssertEqual(document.stringValue, request.document) XCTAssert(request.responseType == Post?.self) XCTAssert(request.variables != nil) + assertEquals(actualAuthMode: request.authMode, expectedAuthMode: .amazonCognitoUserPools) } func testListQueryGraphQLRequest() { @@ -87,11 +91,12 @@ class GraphQLRequestModelTest: XCTestCase { documentBuilder.add(decorator: PaginationDecorator()) let document = documentBuilder.build() - let request = GraphQLRequest.list(Post.self, where: predicate) + let request = GraphQLRequest.list(Post.self, where: predicate, authMode: .amazonCognitoUserPools) XCTAssertEqual(document.stringValue, request.document) XCTAssert(request.responseType == List.self) XCTAssertNotNil(request.variables) + assertEquals(actualAuthMode: request.authMode, expectedAuthMode: .amazonCognitoUserPools) } func testPaginatedListQueryGraphQLRequest() { @@ -104,11 +109,12 @@ class GraphQLRequestModelTest: XCTestCase { documentBuilder.add(decorator: PaginationDecorator(limit: 10)) let document = documentBuilder.build() - let request = GraphQLRequest.list(Post.self, where: predicate, limit: 10) + let request = GraphQLRequest.list(Post.self, where: predicate, limit: 10, authMode: .amazonCognitoUserPools) XCTAssertEqual(document.stringValue, request.document) XCTAssert(request.responseType == List.self) XCTAssertNotNil(request.variables) + assertEquals(actualAuthMode: request.authMode, expectedAuthMode: .amazonCognitoUserPools) } func testOnCreateSubscriptionGraphQLRequest() { @@ -116,11 +122,11 @@ class GraphQLRequestModelTest: XCTestCase { documentBuilder.add(decorator: DirectiveNameDecorator(type: .onCreate)) let document = documentBuilder.build() - let request = GraphQLRequest.subscription(of: Post.self, type: .onCreate) + let request = GraphQLRequest.subscription(of: Post.self, type: .onCreate, authMode: .amazonCognitoUserPools) XCTAssertEqual(document.stringValue, request.document) XCTAssert(request.responseType == Post.self) - + assertEquals(actualAuthMode: request.authMode, expectedAuthMode: .amazonCognitoUserPools) } func testOnUpdateSubscriptionGraphQLRequest() { @@ -128,10 +134,11 @@ class GraphQLRequestModelTest: XCTestCase { documentBuilder.add(decorator: DirectiveNameDecorator(type: .onUpdate)) let document = documentBuilder.build() - let request = GraphQLRequest.subscription(of: Post.self, type: .onUpdate) + let request = GraphQLRequest.subscription(of: Post.self, type: .onUpdate, authMode: .amazonCognitoUserPools) XCTAssertEqual(document.stringValue, request.document) XCTAssert(request.responseType == Post.self) + assertEquals(actualAuthMode: request.authMode, expectedAuthMode: .amazonCognitoUserPools) } func testOnDeleteSubscriptionGraphQLRequest() { @@ -139,9 +146,20 @@ class GraphQLRequestModelTest: XCTestCase { documentBuilder.add(decorator: DirectiveNameDecorator(type: .onDelete)) let document = documentBuilder.build() - let request = GraphQLRequest.subscription(of: Post.self, type: .onDelete) + let request = GraphQLRequest.subscription(of: Post.self, type: .onDelete, authMode: .amazonCognitoUserPools) XCTAssertEqual(document.stringValue, request.document) XCTAssert(request.responseType == Post.self) + assertEquals(actualAuthMode: request.authMode, expectedAuthMode: .amazonCognitoUserPools) + } + + // MARK: - Helpers + + func assertEquals(actualAuthMode: AuthorizationMode?, expectedAuthMode: AWSAuthorizationType) { + guard let authMode = actualAuthMode as? AWSAuthorizationType else { + XCTFail("Missing authorizationMode on request") + return + } + XCTAssertEqual(authMode, expectedAuthMode) } } From 84c1cc1ae12fd14eadef0fc42cc54c2a49ef19e4 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:26:54 -0400 Subject: [PATCH 20/26] chore: update set-up ruby 1.171.0 (#3643) * chore: update set-up ruby 1.171.0 * revert canary workflow since it uses older bundler --- .github/workflows/deploy_package.yml | 2 +- .github/workflows/deploy_release.yml | 2 +- .github/workflows/deploy_unstable.yml | 2 +- .github/workflows/release_doc.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy_package.yml b/.github/workflows/deploy_package.yml index 6141a074ae..a9083cc9ab 100644 --- a/.github/workflows/deploy_package.yml +++ b/.github/workflows/deploy_package.yml @@ -66,7 +66,7 @@ jobs: token: ${{steps.retrieve-token.outputs.token}} - name: Setup Ruby - uses: ruby/setup-ruby@250fcd6a742febb1123a77a841497ccaa8b9e939 # v1.152.0 + uses: ruby/setup-ruby@22fdc77bf4148f810455b226c90fb81b5cbc00a7 # v1.171.0 with: ruby-version: '3.2.1' bundler-cache: true diff --git a/.github/workflows/deploy_release.yml b/.github/workflows/deploy_release.yml index 289e12ab45..c15e30ab78 100644 --- a/.github/workflows/deploy_release.yml +++ b/.github/workflows/deploy_release.yml @@ -62,7 +62,7 @@ jobs: token: ${{steps.retrieve-token.outputs.token}} - name: Setup Ruby - uses: ruby/setup-ruby@250fcd6a742febb1123a77a841497ccaa8b9e939 # v1.152.0 + uses: ruby/setup-ruby@22fdc77bf4148f810455b226c90fb81b5cbc00a7 # v1.171.0 with: ruby-version: '3.2.1' bundler-cache: true diff --git a/.github/workflows/deploy_unstable.yml b/.github/workflows/deploy_unstable.yml index a91c4e7d06..3280627c49 100644 --- a/.github/workflows/deploy_unstable.yml +++ b/.github/workflows/deploy_unstable.yml @@ -62,7 +62,7 @@ jobs: token: ${{steps.retrieve-token.outputs.token}} - name: Setup Ruby - uses: ruby/setup-ruby@250fcd6a742febb1123a77a841497ccaa8b9e939 # v1.152.0 + uses: ruby/setup-ruby@22fdc77bf4148f810455b226c90fb81b5cbc00a7 # v1.171.0 with: ruby-version: '3.2.1' bundler-cache: true diff --git a/.github/workflows/release_doc.yml b/.github/workflows/release_doc.yml index 309d9dca89..dea8bbd525 100644 --- a/.github/workflows/release_doc.yml +++ b/.github/workflows/release_doc.yml @@ -36,7 +36,7 @@ jobs: token: ${{steps.retrieve-token.outputs.token}} - name: Setup Ruby - uses: ruby/setup-ruby@250fcd6a742febb1123a77a841497ccaa8b9e939 # v1.152.0 + uses: ruby/setup-ruby@22fdc77bf4148f810455b226c90fb81b5cbc00a7 # v1.171.0 with: ruby-version: '3.2.1' bundler-cache: true From 84346ba7444201fa0a1267d9ecf2a683bfa9138e Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Fri, 26 Apr 2024 15:40:12 +0000 Subject: [PATCH 21/26] chore: release 2.31.0 [skip ci] --- .../ServiceConfiguration/AmplifyAWSServiceConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift b/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift index c8b51bfa62..6f5a690e67 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift @@ -15,7 +15,7 @@ import Amplify public class AmplifyAWSServiceConfiguration { /// - Tag: AmplifyAWSServiceConfiguration.amplifyVersion - public static let amplifyVersion = "2.30.0" + public static let amplifyVersion = "2.31.0" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From 8c66a72b98ee1747c6c936a25756c7624e897e96 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Fri, 26 Apr 2024 15:41:57 +0000 Subject: [PATCH 22/26] chore: finalize release 2.31.0 [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 613503d0a3..62bc65dc5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.31.0 (2024-04-26) + +### Features + +- **api**: add authorizationMode to GraphQLRequest (#3630) + ## 2.30.0 (2024-04-26) ### Features From 7caf11c989f91d1496491e7955744a3a98de58dc Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Fri, 26 Apr 2024 12:03:50 -0400 Subject: [PATCH 23/26] fix(storage): retrieve accesslevel before storage service (#3641) --- .../AWSS3StoragePlugin/AWSS3StoragePlugin+Configure.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+Configure.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+Configure.swift index f6ee4f1a7d..9b40e63906 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+Configure.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/AWSS3StoragePlugin+Configure.swift @@ -37,6 +37,7 @@ extension AWSS3StoragePlugin { do { let authService = AWSAuthService() + let defaultAccessLevel = try configClosures.retrieveDefaultAccessLevel() let storageService = try AWSS3StorageService(authService: authService, region: configClosures.retrieveRegion(), bucket: configClosures.retrieveBucket(), @@ -45,7 +46,7 @@ extension AWSS3StoragePlugin { configure(storageService: storageService, authService: authService, - defaultAccessLevel: try configClosures.retrieveDefaultAccessLevel()) + defaultAccessLevel: defaultAccessLevel) } catch let storageError as StorageError { throw storageError } catch { From a82fcd4cb8e4e64b28c0d380cfc21d52bb72790a Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Fri, 26 Apr 2024 17:16:57 +0000 Subject: [PATCH 24/26] chore: release 2.31.1 [skip ci] --- .../ServiceConfiguration/AmplifyAWSServiceConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift b/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift index 6f5a690e67..de46ed476c 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift @@ -15,7 +15,7 @@ import Amplify public class AmplifyAWSServiceConfiguration { /// - Tag: AmplifyAWSServiceConfiguration.amplifyVersion - public static let amplifyVersion = "2.31.0" + public static let amplifyVersion = "2.31.1" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From da563bef3c8d908df5dd7e71d7bd11df0e1ea29c Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Fri, 26 Apr 2024 17:18:59 +0000 Subject: [PATCH 25/26] chore: finalize release 2.31.1 [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62bc65dc5a..ce92ffe40b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.31.1 (2024-04-26) + +### Bug Fixes + +- **storage**: retrieve accesslevel before storage service (#3641) + ## 2.31.0 (2024-04-26) ### Features From bb7f7475dee0db808842ac707963a3ed12ad85c5 Mon Sep 17 00:00:00 2001 From: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> Date: Fri, 26 Apr 2024 13:25:00 -0400 Subject: [PATCH 26/26] chore: Adding workflow to build Amplify for minimum Xcode (#3633) --- .../get_platform_parameters/action.yml | 17 +- .../build_amplify_swift_platforms.yml | 3 +- ...uild_minimum_supported_swift_platforms.yml | 19 +- ...ild_amplify_swift.yml => build_scheme.yml} | 31 +-- .../xcschemes/Amplify-Build.xcscheme | 211 ++++++++++++++++++ 5 files changed, 256 insertions(+), 25 deletions(-) rename .github/workflows/{build_amplify_swift.yml => build_scheme.yml} (78%) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/Amplify-Build.xcscheme 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/.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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +