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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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 235b49f3c3ed441776c10d38b8ecf74501e05e56 Mon Sep 17 00:00:00 2001 From: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> Date: Mon, 8 Apr 2024 12:08:26 -0400 Subject: [PATCH 11/86] fix(DataStore): Fixing a crash when attempting to create a model with a predicate. (#3600) --- .../Storage/StorageEngine.swift | 1 + .../Storage/StorageEngineTestsHasOne.swift | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine.swift index 078bf60624..53c213e4fb 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine.swift @@ -206,6 +206,7 @@ final class StorageEngine: StorageEngineBehavior { "Cannot apply a condition on model which does not exist.", "Save the model instance without a condition first.") completion(.failure(causedBy: dataStoreError)) + return } do { diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Storage/StorageEngineTestsHasOne.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Storage/StorageEngineTestsHasOne.swift index 14133c3701..2627faa3a0 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Storage/StorageEngineTestsHasOne.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Storage/StorageEngineTestsHasOne.swift @@ -70,6 +70,28 @@ class StorageEngineTestsHasOne: StorageEngineTestsBase { } } + /// Given: A model that does not exist + /// When: save is called with a predicate + /// Then: A DataStoreError.invalidCondition error is returned + func testSaveModelWithPredicate_shouldFail() { + let team = Team(name: "Team") + let saveFinished = expectation(description: "Save finished") + storageEngine.save(team, condition: Team.keys.name.beginsWith("T")) { result in + defer { + saveFinished.fulfill() + } + guard case .failure(let error) = result, + case . invalidCondition(let errorDescription, let recoverySuggestion, _) = error else { + XCTFail("Expected failure with .invalidCondition, got \(result)") + return + } + + XCTAssertEqual(errorDescription, "Cannot apply a condition on model which does not exist.") + XCTAssertEqual(recoverySuggestion, "Save the model instance without a condition first.") + } + wait(for: [saveFinished], timeout: defaultTimeout) + } + func testBelongsToRelationshipWithoutOwner() { let teamA = Team(name: "A-Team") let projectA = Project(name: "ProjectA", team: teamA) From 68c7eff671217fe3938d6756b00310849a33d49a Mon Sep 17 00:00:00 2001 From: Harsh <6162866+harsh62@users.noreply.github.com> Date: Mon, 8 Apr 2024 13:53:07 -0400 Subject: [PATCH 12/86] fix(Auth): throw correct error for an unknown session error (#3591) --- .../FetchAuthSessionOperationHelper.swift | 20 +++++-- ...AuthFetchSignInSessionOperationTests.swift | 57 +++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/FetchAuthSessionOperationHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/FetchAuthSessionOperationHelper.swift index 7aa1997b72..d39751956a 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/FetchAuthSessionOperationHelper.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/FetchAuthSessionOperationHelper.swift @@ -157,13 +157,21 @@ class FetchAuthSessionOperationHelper: DefaultLogger { } case .service(let error): - if let authError = (error as? AuthErrorConvertible)?.authError { - let session = AWSAuthCognitoSession(isSignedIn: isSignedIn, - identityIdResult: .failure(authError), - awsCredentialsResult: .failure(authError), - cognitoTokensResult: .failure(authError)) - return session + var authError: AuthError + if let convertedAuthError = (error as? AuthErrorConvertible)?.authError { + authError = convertedAuthError + } else { + authError = AuthError.service( + "Unknown service error occurred", + "See the attached error for more details", + error) } + let session = AWSAuthCognitoSession( + isSignedIn: isSignedIn, + identityIdResult: .failure(authError), + awsCredentialsResult: .failure(authError), + cognitoTokensResult: .failure(authError)) + return session default: break } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift index ed107d34d7..9bee6c05b1 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift @@ -736,4 +736,61 @@ class AWSAuthFetchSignInSessionOperationTests: BaseAuthorizationTests { let identityId = try? (session as? AuthCognitoIdentityProvider)?.getIdentityId().get() XCTAssertNotNil(identityId) } + + /// Test signedIn session with invalid response for aws credentials + /// + /// - Given: Given an auth plugin with signedIn state + /// - When: + /// - I invoke fetchAuthSession and service throws NSError + /// - Then: + /// - I should get an a valid session with the following details: + /// - isSignedIn = true + /// - aws credentails = service error + /// - identity id = service error + /// - cognito tokens = service error + /// + func testSignInSessionWithNSError() async throws { + let initialState = AuthState.configured( + AuthenticationState.signedIn(.testData), + AuthorizationState.sessionEstablished( + AmplifyCredentials.testDataWithExpiredTokens)) + + let initAuth: MockIdentityProvider.MockInitiateAuthResponse = { _ in + return InitiateAuthOutput(authenticationResult: .init(accessToken: "accessToken", + expiresIn: 1000, + idToken: "idToken", + refreshToken: "refreshToke")) + } + + let awsCredentials: MockIdentity.MockGetCredentialsResponse = { _ in + throw NSError(domain: NSURLErrorDomain, code: 1, userInfo: nil) + } + let plugin = configurePluginWith( + userPool: { MockIdentityProvider(mockInitiateAuthResponse: initAuth) }, + identityPool: { MockIdentity(mockGetCredentialsResponse: awsCredentials) }, + initialState: initialState) + + let session = try await plugin.fetchAuthSession(options: AuthFetchSessionRequest.Options()) + + XCTAssertTrue(session.isSignedIn) + let credentialsResult = (session as? AuthAWSCredentialsProvider)?.getAWSCredentials() + guard case .failure(let error) = credentialsResult, case .service = error else { + XCTFail("Should return service error") + return + } + + let identityIdResult = (session as? AuthCognitoIdentityProvider)?.getIdentityId() + guard case .failure(let identityIdError) = identityIdResult, + case .service = identityIdError else { + XCTFail("Should return service error") + return + } + + let tokensResult = (session as? AuthCognitoTokensProvider)?.getCognitoTokens() + guard case .failure(let tokenError) = tokensResult, + case .service = tokenError else { + XCTFail("Should return service error") + return + } + } } From 50e001f465f031eca0f895d75a5ad0e9dc33df68 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 10 Apr 2024 10:41:10 -0700 Subject: [PATCH 13/86] fix(api): add collection type casting in swift 5.7 (#3602) * fix(api): add collection type casting in swift 5.7 * add unit test cases * add descriptions to test cases --- .../AWSGraphQLSubscriptionTaskRunner.swift | 27 +++++++- .../Support/Utils/Array+Error+TypeCast.swift | 21 +++++++ .../Utils/Array+Error+TypeCastTests.swift | 62 +++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/Array+Error+TypeCast.swift create mode 100644 AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/Array+Error+TypeCastTests.swift diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift index 3e70654298..3f3889566a 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift @@ -378,6 +378,31 @@ fileprivate func toAPIError(_ errors: [Error], type: R.Type) -> AP (hasAuthorizationError ? ": \(APIError.UnauthorizedMessageString)" : "") } +#if swift(<5.8) + if let errors = errors.cast(to: AppSyncRealTimeRequest.Error.self) { + let hasAuthorizationError = errors.contains(where: { $0 == .unauthorized}) + return APIError.operationError( + errorDescription(hasAuthorizationError), + "", + errors.first + ) + } else if let errors = errors.cast(to: GraphQLError.self) { + let hasAuthorizationError = errors.map(\.extensions) + .compactMap { $0.flatMap { $0["errorType"]?.stringValue } } + .contains(where: { AppSyncErrorType($0) == .unauthorized }) + return APIError.operationError( + errorDescription(hasAuthorizationError), + "", + GraphQLResponseError.error(errors) + ) + } else { + return APIError.operationError( + errorDescription(), + "", + errors.first + ) + } +#else switch errors { case let errors as [AppSyncRealTimeRequest.Error]: let hasAuthorizationError = errors.contains(where: { $0 == .unauthorized}) @@ -402,5 +427,5 @@ fileprivate func toAPIError(_ errors: [Error], type: R.Type) -> AP errors.first ) } - +#endif } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/Array+Error+TypeCast.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/Array+Error+TypeCast.swift new file mode 100644 index 0000000000..3592791dc2 --- /dev/null +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/Array+Error+TypeCast.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + + +import Foundation + +@_spi(AmplifyAPI) +extension Array where Element == Error { + func cast(to type: T.Type) -> [T]? { + self.reduce([]) { partialResult, ele in + if let partialResult, let ele = ele as? T { + return partialResult + [ele] + } + return nil + } + } +} diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/Array+Error+TypeCastTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/Array+Error+TypeCastTests.swift new file mode 100644 index 0000000000..d1d6861a74 --- /dev/null +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/Array+Error+TypeCastTests.swift @@ -0,0 +1,62 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + + +import XCTest +@testable @_spi(AmplifyAPI) import AWSAPIPlugin + +class ArrayWithErrorElementExtensionTests: XCTestCase { + + /** + Given: errors with generic protocol type + When: cast to the correct underlying concrete type + Then: successfully casted to underlying concrete type + */ + func testCast_toCorrectErrorType_returnCastedErrorType() { + let errors: [Error] = [ + Error1(), Error1(), Error1() + ] + + let error1s = errors.cast(to: Error1.self) + XCTAssertNotNil(error1s) + XCTAssertTrue(!error1s!.isEmpty) + XCTAssertEqual(errors.count, error1s!.count) + } + + /** + Given: errors with generic protocol type + When: cast to the wong underlying concrete type + Then: return nil + */ + func testCast_toWrongErrorType_returnNil() { + let errors: [Error] = [ + Error1(), Error1(), Error1() + ] + + let error2s = errors.cast(to: Error2.self) + XCTAssertNil(error2s) + } + + /** + Given: errors with generic protocol type + When: some of the elements failed to cast to the underlying concrete type + Then: return nil + */ + + func testCast_partiallyToWrongErrorType_returnNil() { + let errors: [Error] = [ + Error2(), Error2(), Error1() + ] + + let error2s = errors.cast(to: Error2.self) + XCTAssertNil(error2s) + } + + struct Error1: Error { } + + struct Error2: Error { } +} From 3d5b3a04e3a36185626ee6d8808ac3d773736081 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 10 Apr 2024 20:56:43 +0000 Subject: [PATCH 14/86] chore: release 2.29.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 1eaf51d7a0..df385323c4 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.0" + public static let amplifyVersion = "2.29.1" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From dc32a9d2b8152782273a7a226fc3c51f63a5663c Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 10 Apr 2024 20:59:36 +0000 Subject: [PATCH 15/86] chore: finalize release 2.29.1 [skip ci] --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45bb058c6c..e8447deb95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.29.1 (2024-04-10) + +### Bug Fixes + +- **api**: add collection type casting in swift 5.7 (#3602) +- **Auth**: throw correct error for an unknown session error (#3591) +- **DataStore**: Fixing a crash when attempting to create a model with a predicate. (#3600) + ## 2.29.0 (2024-04-03) ### Features 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 16/86] 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 17/86] 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 4eecd945f59c15a3161eb1ea3aafec4c3242539e Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 16 Apr 2024 10:01:53 -0700 Subject: [PATCH 18/86] fix(Core): TaskQueue async execution (#3611) * fix(Core): TaskQueue async execution is not serial #3556 (#3557) * fix(Core): make TaskQueue take async task throwable * fix(core): resolve swift lint warnings --------- Co-authored-by: Tomasz Trela --- Amplify/Core/Support/TaskQueue.swift | 51 +++++++++++++++---- .../CoreTests/AmplifyTaskQueueTests.swift | 16 ++++++ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/Amplify/Core/Support/TaskQueue.swift b/Amplify/Core/Support/TaskQueue.swift index f281c57e1c..a09bfcc4d8 100644 --- a/Amplify/Core/Support/TaskQueue.swift +++ b/Amplify/Core/Support/TaskQueue.swift @@ -8,10 +8,25 @@ import Foundation /// A helper for executing asynchronous work serially. -public actor TaskQueue { - private var previousTask: Task? +public class TaskQueue { + typealias Block = @Sendable () async -> Void + private var streamContinuation: AsyncStream.Continuation! - public init() {} + public init() { + let stream = AsyncStream.init { continuation in + streamContinuation = continuation + } + + Task { + for await block in stream { + _ = await block() + } + } + } + + deinit { + streamContinuation.finish() + } /// Serializes asynchronous requests made from an async context /// @@ -25,17 +40,31 @@ public actor TaskQueue { /// TaskQueue serializes this work so that `doAsync1` is performed before `doAsync2`, /// which is performed before `doAsync3`. public func sync(block: @Sendable @escaping () async throws -> Success) async throws -> Success { - let currentTask: Task = Task { [previousTask] in - _ = await previousTask?.result - return try await block() + try await withCheckedThrowingContinuation { continuation in + streamContinuation.yield { + do { + let value = try await block() + continuation.resume(returning: value) + } catch { + continuation.resume(throwing: error) + } + } } - previousTask = currentTask - return try await currentTask.value } - public nonisolated func async(block: @Sendable @escaping () async throws -> Success) rethrows { - Task { - try await sync(block: block) + public func async(block: @Sendable @escaping () async throws -> Success) { + streamContinuation.yield { + do { + _ = try await block() + } catch { + Self.log.warn("Failed to handle async task in TaskQueue<\(Success.self)> with error: \(error)") + } } } } + +extension TaskQueue { + public static var log: Logger { + Amplify.Logging.logger(forNamespace: String(describing: self)) + } +} diff --git a/AmplifyTests/CoreTests/AmplifyTaskQueueTests.swift b/AmplifyTests/CoreTests/AmplifyTaskQueueTests.swift index a3c92f8dcc..48c19c9182 100644 --- a/AmplifyTests/CoreTests/AmplifyTaskQueueTests.swift +++ b/AmplifyTests/CoreTests/AmplifyTaskQueueTests.swift @@ -66,4 +66,20 @@ final class AmplifyTaskQueueTests: XCTestCase { await fulfillment(of: [expectation1, expectation2, expectation3], enforceOrder: true) } + func testAsync() async throws { + let taskCount = 1_000 + let expectations: [XCTestExpectation] = (0..() + + for i in 0.. Date: Tue, 16 Apr 2024 17:19:14 -0500 Subject: [PATCH 19/86] fix(api): AWSGraphQLSubscriptionTaskRunner failing to cancel AppSyncRealTimeSubscription (#3615) --- .../Operation/AWSGraphQLSubscriptionTaskRunner.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift index 3f3889566a..13cba6b888 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift @@ -46,10 +46,7 @@ public class AWSGraphQLSubscriptionTaskRunner: InternalTaskRunner, public func cancel() { self.send(GraphQLSubscriptionEvent.connection(.disconnected)) - Task { [weak self] in - guard let self else { - return - } + Task { guard let appSyncClient = self.appSyncClient else { return } From 2112ad95d585861833da5ea04bab43a43536fc4a Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 17 Apr 2024 22:46:03 +0000 Subject: [PATCH 20/86] chore: release 2.29.2 [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 df385323c4..3d00f35cb4 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.1" + public static let amplifyVersion = "2.29.2" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From 1f3bcfe570ec1b9520d9afa12d683d58e617d843 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 17 Apr 2024 22:48:53 +0000 Subject: [PATCH 21/86] chore: finalize release 2.29.2 [skip ci] --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8447deb95..469c7b0f80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2.29.2 (2024-04-17) + +### Bug Fixes + +- **api**: AWSGraphQLSubscriptionTaskRunner failing to cancel AppSyncRealTimeSubscription (#3615) +- **Core**: TaskQueue async execution (#3611) + ## 2.29.1 (2024-04-10) ### Bug Fixes From e38ba5316f1f0ccc3e673edd62cf4464d54101d8 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 18 Apr 2024 10:53:30 -0700 Subject: [PATCH 22/86] fix(API): make sure unsubscribe is invoked when subscription cancelled (#3619) * fix(API): make sure unsubscribe is invoked when subscription cancelled * resolve comments --- .../AppSyncRealTimeClient.swift | 4 ++ .../AWSGraphQLSubscriptionTaskRunner.swift | 10 ++- .../GraphQLModelBasedTests.swift | 63 +++++++++++++++++++ 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeClient.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeClient.swift index c8bf7efcab..25a695f9b5 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeClient.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeClient.swift @@ -54,6 +54,10 @@ actor AppSyncRealTimeClient: AppSyncRealTimeClientProtocol { self.state.value == .connected } + internal var numberOfSubscriptions: Int { + self.subscriptions.count + } + /** Creates a new AppSyncRealTimeClient with endpoint, requestInterceptor and webSocketClient. - Parameters: diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift index 13cba6b888..12427ad9ab 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift @@ -44,6 +44,9 @@ public class AWSGraphQLSubscriptionTaskRunner: InternalTaskRunner, self.apiAuthProviderFactory = apiAuthProviderFactory } + /// When the top-level AmplifyThrowingSequence is canceled, this cancel method is invoked. + /// In this situation, we need to send the disconnected event because + /// the top-level AmplifyThrowingSequence is terminated immediately upon cancellation. public func cancel() { self.send(GraphQLSubscriptionEvent.connection(.disconnected)) Task { @@ -210,12 +213,7 @@ final public class AWSGraphQLSubscriptionOperation: GraphQLSubscri override public func cancel() { super.cancel() - - Task { [weak self] in - guard let self else { - return - } - + Task { guard let appSyncRealTimeClient = self.appSyncRealTimeClient else { return } diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests.swift index 770f598a7a..c5c6b87cb4 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests.swift @@ -448,6 +448,65 @@ class GraphQLModelBasedTests: XCTestCase { await fulfillment(of: [progressInvoked], timeout: TestCommonConstants.networkTimeout) } + + /// Given: Several subscriptions with Amplify API plugin + /// When: Cancel subscriptions + /// Then: AppSync real time client automatically unsubscribe and remove the subscription + func testCancelledSubscription_automaticallyUnsubscribeAndRemoved() async throws { + let numberOfSubscription = 5 + let allSubscribedExpectation = expectation(description: "All subscriptions are subscribed") + allSubscribedExpectation.expectedFulfillmentCount = numberOfSubscription + + let subscriptions = (0..<5).map { _ in + Amplify.API.subscribe(request: .subscription(of: Comment.self, type: .onCreate)) + } + subscriptions.forEach { subscription in + Task { + do { + for try await subscriptionEvent in subscription { + switch subscriptionEvent { + case .connection(let state): + switch state { + case .connecting: + break + case .connected: + allSubscribedExpectation.fulfill() + case .disconnected: + break + } + case .data(let result): + switch result { + case .success: break + case .failure(let error): + XCTFail("\(error)") + } + } + } + } catch { + XCTFail("Unexpected subscription failure") + } + } + } + + await fulfillment(of: [allSubscribedExpectation], timeout: 3) + if let appSyncRealTimeClientFactory = + getUnderlyingAPIPlugin()?.appSyncRealTimeClientFactory as? AppSyncRealTimeClientFactory, + let appSyncRealTimeClient = + await appSyncRealTimeClientFactory.apiToClientCache.values.first as? AppSyncRealTimeClient + { + var appSyncSubscriptions = await appSyncRealTimeClient.numberOfSubscriptions + XCTAssertEqual(appSyncSubscriptions, numberOfSubscription) + + subscriptions.forEach { $0.cancel() } + try await Task.sleep(seconds: 2) + appSyncSubscriptions = await appSyncRealTimeClient.numberOfSubscriptions + XCTAssertEqual(appSyncSubscriptions, 0) + + } else { + XCTFail("There should be at least one AppSyncRealTimeClient instance") + } + } + // MARK: Helpers func createPost(id: String, title: String) async throws -> Post? { @@ -499,4 +558,8 @@ class GraphQLModelBasedTests: XCTestCase { throw error } } + + func getUnderlyingAPIPlugin() -> AWSAPIPlugin? { + return Amplify.API.plugins["awsAPIPlugin"] as? AWSAPIPlugin + } } From 887c2d96e6c3c7330496e4a0378b95cafc49bf1d Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:46:50 -0500 Subject: [PATCH 23/86] chore(predictions): add support for invalid signature exception (#3625) --- .../AWSPredictionsPlugin/Liveness/Events/LivenessEvent.swift | 1 + .../Liveness/SPI/AWSPredictionsPlugin+Liveness.swift | 3 +++ 2 files changed, 4 insertions(+) diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessEvent.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessEvent.swift index f8ba73d2b9..822373e296 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessEvent.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Events/LivenessEvent.swift @@ -52,6 +52,7 @@ public enum LivenessEventKind { public static let serviceQuotaExceeded = Self(rawValue: "ServiceQuotaExceededException") public static let serviceUnavailable = Self(rawValue: "ServiceUnavailableException") public static let sessionNotFound = Self(rawValue: "SessionNotFoundException") + public static let invalidSignature = Self(rawValue: "InvalidSignatureException") } } diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/SPI/AWSPredictionsPlugin+Liveness.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/SPI/AWSPredictionsPlugin+Liveness.swift index f476122ab0..d2eec8d96e 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/SPI/AWSPredictionsPlugin+Liveness.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/SPI/AWSPredictionsPlugin+Liveness.swift @@ -66,6 +66,7 @@ public struct FaceLivenessSessionError: Swift.Error, Equatable { public static let accessDenied = Self(code: 7) public static let invalidRegion = Self(code: 8) public static let invalidURL = Self(code: 9) + public static let invalidSignature = Self(code: 10) } extension FaceLivenessSessionError { @@ -85,6 +86,8 @@ extension FaceLivenessSessionError { self = .serviceUnavailable case .sessionNotFound: self = .sessionNotFound + case .invalidSignature: + self = .invalidSignature default: self = .unknown } From 608a3c7547ff91fbe5c56957b67ee1bacfe4d950 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Mon, 22 Apr 2024 17:30:59 +0000 Subject: [PATCH 24/86] chore: release 2.29.3 [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 3d00f35cb4..4982582d64 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.2" + public static let amplifyVersion = "2.29.3" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From adf5a2e0cc8e240ef12d2f63054806d1ef3dcc47 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Mon, 22 Apr 2024 17:34:01 +0000 Subject: [PATCH 25/86] chore: finalize release 2.29.3 [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 469c7b0f80..e35310cc92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.29.3 (2024-04-22) + +### Bug Fixes + +- **API**: make sure unsubscribe is invoked when subscription cancelled (#3619) + ## 2.29.2 (2024-04-17) ### Bug Fixes 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 26/86] 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 27/86] 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 28/86] 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 29/86] 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 30/86] 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 31/86] 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 32/86] 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 33/86] 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 34/86] 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 35/86] 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 36/86] 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 37/86] 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 38/86] 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 39/86] 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 2b2856a07a3b2b8e8b8b6129d91b922bd94d78eb Mon Sep 17 00:00:00 2001 From: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:31:23 -0400 Subject: [PATCH 40/86] fix(Auth): Fixing the Gen2 json configuration used by the Authenticator (#3647) --------- Co-authored-by: Michael Law <1365977+lawmicha@users.noreply.github.com> --- .../Support/Helpers/ConfigurationHelper.swift | 13 ++++++------- .../Support/ConfigurationHelperTests.swift | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift index 42f647f95e..877571243c 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift @@ -220,7 +220,7 @@ struct ConfigurationHelper { AuthPluginErrorConstants.configurationMissingError ) } - let userPoolConfig = try parseUserPoolData(config) + let userPoolConfig = parseUserPoolData(config) let identityPoolConfig = parseIdentityPoolData(config) return try createAuthConfiguration(userPoolConfig: userPoolConfig, @@ -281,11 +281,10 @@ struct ConfigurationHelper { "verificationMechanism": .array(verificationMechanisms)]) } - return JSONValue.object( - ["auth": .object( - ["plugins": .object( - ["awsCognitoAuthPlugin": .object( - ["Auth": .object( - ["Default": authConfigObject])])])])]) + return JSONValue.object([ + "Auth": .object([ + "Default": authConfigObject + ]) + ]) } } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift index 812cb05b7c..d2f5f732ff 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift @@ -246,7 +246,7 @@ final class ConfigurationHelperTests: XCTestCase { ])) let json = ConfigurationHelper.createUserPoolJsonConfiguration(config) - guard let authConfig = json.auth?.plugins?.awsCognitoAuthPlugin?.Auth?.Default else { + guard let authConfig = json.Auth?.Default else { XCTFail("Could not retrieve auth configuration from json") return } From a3e267efa7a93e9cbfdfd3dede60812c1556f30c Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Mon, 29 Apr 2024 13:27:58 -0400 Subject: [PATCH 41/86] chore: skip report coverage (#3652) [skip ci] --- .github/workflows/unit_test.yml | 48 ++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 7873f7b5d8..1dbd2e4565 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -78,30 +78,30 @@ jobs: scheme: ${{ matrix.scheme }} generate_coverage_report: true - report-coverage: - name: ${{ matrix.file.scheme }} Unit Tests - needs: [unit-tests-with-coverage] - strategy: - fail-fast: false - matrix: - file: [ - { scheme: Amplify, flags: 'Amplify,unit_tests' }, - { scheme: AWSPluginsCore, flags: 'AWSPluginsCore,unit_tests' }, - { scheme: AWSAPIPlugin, flags: 'API_plugin_unit_test,unit_tests' }, - { scheme: AWSCloudWatchLoggingPlugin, flags: 'Logging_plugin_unit_test,unit_tests' }, - { scheme: AWSCognitoAuthPlugin, flags: 'Auth_plugin_unit_test,unit_tests' }, - { scheme: AWSDataStorePlugin, flags: 'DataStore_plugin_unit_test,unit_tests' }, - { scheme: AWSLocationGeoPlugin, flags: 'Geo_plugin_unit_test,unit_tests' }, - { scheme: AWSPredictionsPlugin, flags: 'Predictions_plugin_unit_test,unit_tests' }, - { scheme: AWSPinpointAnalyticsPlugin, flags: 'Analytics_plugin_unit_test,unit_tests' }, - { scheme: AWSPinpointPushNotificationsPlugin, flags: 'PushNotifications_plugin_unit_test,unit_tests' }, - { scheme: AWSS3StoragePlugin, flags: 'Storage_plugin_unit_test,unit_tests' }, - { scheme: CoreMLPredictionsPlugin, flags: 'CoreMLPredictions_plugin_unit_test,unit_tests' } - ] - uses: ./.github/workflows/upload_coverage_report.yml - with: - scheme: ${{ matrix.file.scheme }} - flags: ${{ matrix.file.flags }} + # report-coverage: + # name: ${{ matrix.file.scheme }} Unit Tests + # needs: [unit-tests-with-coverage] + # strategy: + # fail-fast: false + # matrix: + # file: [ + # { scheme: Amplify, flags: 'Amplify,unit_tests' }, + # { scheme: AWSPluginsCore, flags: 'AWSPluginsCore,unit_tests' }, + # { scheme: AWSAPIPlugin, flags: 'API_plugin_unit_test,unit_tests' }, + # { scheme: AWSCloudWatchLoggingPlugin, flags: 'Logging_plugin_unit_test,unit_tests' }, + # { scheme: AWSCognitoAuthPlugin, flags: 'Auth_plugin_unit_test,unit_tests' }, + # { scheme: AWSDataStorePlugin, flags: 'DataStore_plugin_unit_test,unit_tests' }, + # { scheme: AWSLocationGeoPlugin, flags: 'Geo_plugin_unit_test,unit_tests' }, + # { scheme: AWSPredictionsPlugin, flags: 'Predictions_plugin_unit_test,unit_tests' }, + # { scheme: AWSPinpointAnalyticsPlugin, flags: 'Analytics_plugin_unit_test,unit_tests' }, + # { scheme: AWSPinpointPushNotificationsPlugin, flags: 'PushNotifications_plugin_unit_test,unit_tests' }, + # { scheme: AWSS3StoragePlugin, flags: 'Storage_plugin_unit_test,unit_tests' }, + # { scheme: CoreMLPredictionsPlugin, flags: 'CoreMLPredictions_plugin_unit_test,unit_tests' } + # ] + # uses: ./.github/workflows/upload_coverage_report.yml + # with: + # scheme: ${{ matrix.file.scheme }} + # flags: ${{ matrix.file.flags }} unit-test-pass-confirmation: runs-on: ubuntu-latest From 87a857cde71e465f0da76068f98483e18b9a0417 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Mon, 29 Apr 2024 17:58:26 +0000 Subject: [PATCH 42/86] chore: release 2.32.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 de46ed476c..53d5a1416d 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.1" + public static let amplifyVersion = "2.32.0" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From af4a5f751511fd4e6ebafbe7ae2ff902c145dfae Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Mon, 29 Apr 2024 18:00:02 +0000 Subject: [PATCH 43/86] chore: finalize release 2.32.0 [skip ci] --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce92ffe40b..6849cb7ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 2.32.0 (2024-04-29) + +### Features + +- **storage**: update Storage APIs with StoragePath parameter +- **Storage**: Adding integration tests for getURL, remove and list (#3584) +- **Storage**: Refactor list objects API to include `path` (#3580) +- **storage**: update storage upload APIs to use storage path (#3574) +- **Storage**: Refactor GetURL API to include `path` (#3573) +- **storage**: update storage download api (#3561) +- **storage**: refactor storage remove api by including path (#3571) +- **storage**: add new storage gen2 APIs (#3559) + +### Bug Fixes + +- **Auth**: Fixing the Gen2 json configuration used by the Authenticator (#3647) + ## 2.31.1 (2024-04-26) ### Bug Fixes From d03f5a6616bb44fb7916c98627e5302998a02d64 Mon Sep 17 00:00:00 2001 From: Harsh <6162866+harsh62@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:37:48 -0400 Subject: [PATCH 44/86] fix(Auth): Refactoring state machine logic to fix memory leak (#3613) * fix(Auth): Fixing memory leaks happening because of the state machine * Update AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateMachine.swift Co-authored-by: Di Wu * worked on review comment --------- Co-authored-by: Di Wu --- .../CancellableAsyncStream.swift | 43 +++++++++++++ .../StateAsyncSequence.swift | 36 ----------- .../StateMachine.swift | 63 +++++-------------- 3 files changed, 58 insertions(+), 84 deletions(-) create mode 100644 AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/CancellableAsyncStream.swift delete mode 100644 AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateAsyncSequence.swift diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/CancellableAsyncStream.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/CancellableAsyncStream.swift new file mode 100644 index 0000000000..35ce4b65c1 --- /dev/null +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/CancellableAsyncStream.swift @@ -0,0 +1,43 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Combine + +class CancellableAsyncStream: AsyncSequence { + + typealias AsyncIterator = AsyncStream.AsyncIterator + private let asyncStream: AsyncStream + private let cancellable: AnyCancellable? + + deinit { + cancel() + } + + init(asyncStream: AsyncStream, cancellable: AnyCancellable?) { + self.asyncStream = asyncStream + self.cancellable = cancellable + } + + convenience init(with publisher: AnyPublisher) { + var cancellable: AnyCancellable? + self.init(asyncStream: AsyncStream { continuation in + cancellable = publisher.sink { _ in + continuation.finish() + } receiveValue: { + continuation.yield($0) + } + }, cancellable: cancellable) + } + + func makeAsyncIterator() -> AsyncStream.AsyncIterator { + asyncStream.makeAsyncIterator() + } + + func cancel() { + cancellable?.cancel() + } +} diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateAsyncSequence.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateAsyncSequence.swift deleted file mode 100644 index 04cf788b1f..0000000000 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateAsyncSequence.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import Foundation - -class StateAsyncSequence: AsyncSequence { - - typealias Iterator = AsyncStream.Iterator - private var continuation: AsyncStream.Continuation! = nil - - private var asyncStream: AsyncStream! = nil - - init(bufferingPolicy: AsyncStream.Continuation.BufferingPolicy = .unbounded) { - asyncStream = AsyncStream( - Element.self, - bufferingPolicy: bufferingPolicy) { continuation in - self.continuation = continuation - } - } - - func makeAsyncIterator() -> Iterator { - asyncStream.makeAsyncIterator() - } - - func send(_ element: Element) { - continuation.yield(element) - } - - func cancel() { - continuation.finish() - } -} diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateMachine.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateMachine.swift index 7b69423411..6fd47e52ad 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateMachine.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateMachine.swift @@ -5,15 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import Foundation - -/// Captures a weak reference to the value -class WeakWrapper where T: AnyObject { - private(set) weak var value: T? - init(_ value: T) { - self.value = value - } -} +import Combine /// Models, evolves, and processes effects for a system. /// @@ -31,15 +23,18 @@ actor StateMachine< EnvironmentType: Environment > where StateType: State { - /// AsyncSequences are invoked a minimum of one time: Each sequence receives the current - /// state as soon as `listen()` is invoked, and will receive each subsequent state change. - typealias StateChangeSequence = StateAsyncSequence - private let environment: EnvironmentType private let resolver: AnyResolver - private(set) var currentState: StateType - private var subscribers: [WeakWrapper>] + public var currentState: StateType { + currentStateSubject.value + } + + private let currentStateSubject: CurrentValueSubject + + deinit { + currentStateSubject.send(completion: .finished) + } init( resolver: ResolverType, @@ -48,22 +43,16 @@ actor StateMachine< ) where ResolverType: StateMachineResolver, ResolverType.StateType == StateType { self.resolver = resolver.eraseToAnyResolver() self.environment = environment - self.currentState = initialState ?? resolver.defaultState - - self.subscribers = [] + self.currentStateSubject = CurrentValueSubject(initialState ?? resolver.defaultState) } /// Start listening to state change updates. The current state and all subsequent state changes will be sent to the sequence. /// /// - Returns: An async sequence that get states asynchronously - func listen() -> StateChangeSequence { - let sequence = StateAsyncSequence() - let currentState = self.currentState - let wrappedSequence = WeakWrapper(sequence) - subscribers.append(wrappedSequence) - sequence.send(currentState) - return sequence + func listen() -> CancellableAsyncStream { + CancellableAsyncStream(with: currentStateSubject.eraseToAnyPublisher()) } + } extension StateMachine: EventDispatcher { @@ -88,33 +77,11 @@ extension StateMachine: EventDispatcher { ) if currentState != resolution.newState { - currentState = resolution.newState - subscribers.removeAll { item in - !notify(subscriberElement: item, about: resolution.newState) - } + currentStateSubject.send(resolution.newState) } execute(resolution.actions) } - /// - Parameters: - /// - subscriberElement: A weak wrapped async sequence - /// - newState: The new state to be sent - /// - Returns: true if the subscriber was notified, false if the wrapper reference was nil or a cancellation was pending - private func notify( - subscriberElement: WeakWrapper, - about newState: StateType - ) -> Bool { - - // If weak reference has become nil, do not process, and return false so caller can remove - // the subscription from the subscribers list - guard let sequence = subscriberElement.value else { - return false - } - - sequence.send(newState) - return true - } - private func execute(_ actions: [Action]) { guard !actions.isEmpty else { return From 6e2a881a01e573f2f413859bcb9b697270604bf2 Mon Sep 17 00:00:00 2001 From: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:39:32 -0400 Subject: [PATCH 45/86] chore: Control coverage through repository variable [skip ci] (#3656) --- .github/workflows/unit_test.yml | 59 +++++++++++++++++---------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 1dbd2e4565..1e733c2060 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -41,7 +41,7 @@ concurrency: cancel-in-progress: ${{ github.ref_name != 'main'}} jobs: - unit-tests-without-coverage: + targets-without-coverage: name: ${{ matrix.scheme }} Unit Tests strategy: fail-fast: false @@ -54,7 +54,7 @@ jobs: scheme: ${{ matrix.scheme }} generate_coverage_report: false - unit-tests-with-coverage: + targets-with-coverage: name: ${{ matrix.scheme }} Unit Tests strategy: fail-fast: false @@ -76,40 +76,41 @@ jobs: uses: ./.github/workflows/run_unit_tests_platforms.yml with: scheme: ${{ matrix.scheme }} - generate_coverage_report: true + generate_coverage_report: ${{ vars.DISABLE_COVERAGE_REPORT != 'true' }} - # report-coverage: - # name: ${{ matrix.file.scheme }} Unit Tests - # needs: [unit-tests-with-coverage] - # strategy: - # fail-fast: false - # matrix: - # file: [ - # { scheme: Amplify, flags: 'Amplify,unit_tests' }, - # { scheme: AWSPluginsCore, flags: 'AWSPluginsCore,unit_tests' }, - # { scheme: AWSAPIPlugin, flags: 'API_plugin_unit_test,unit_tests' }, - # { scheme: AWSCloudWatchLoggingPlugin, flags: 'Logging_plugin_unit_test,unit_tests' }, - # { scheme: AWSCognitoAuthPlugin, flags: 'Auth_plugin_unit_test,unit_tests' }, - # { scheme: AWSDataStorePlugin, flags: 'DataStore_plugin_unit_test,unit_tests' }, - # { scheme: AWSLocationGeoPlugin, flags: 'Geo_plugin_unit_test,unit_tests' }, - # { scheme: AWSPredictionsPlugin, flags: 'Predictions_plugin_unit_test,unit_tests' }, - # { scheme: AWSPinpointAnalyticsPlugin, flags: 'Analytics_plugin_unit_test,unit_tests' }, - # { scheme: AWSPinpointPushNotificationsPlugin, flags: 'PushNotifications_plugin_unit_test,unit_tests' }, - # { scheme: AWSS3StoragePlugin, flags: 'Storage_plugin_unit_test,unit_tests' }, - # { scheme: CoreMLPredictionsPlugin, flags: 'CoreMLPredictions_plugin_unit_test,unit_tests' } - # ] - # uses: ./.github/workflows/upload_coverage_report.yml - # with: - # scheme: ${{ matrix.file.scheme }} - # flags: ${{ matrix.file.flags }} + report-coverage: + if: ${{ vars.DISABLE_COVERAGE_REPORT != 'true' }} + name: ${{ matrix.file.scheme }} Unit Tests + needs: [targets-with-coverage] + strategy: + fail-fast: false + matrix: + file: [ + { scheme: Amplify, flags: 'Amplify,unit_tests' }, + { scheme: AWSPluginsCore, flags: 'AWSPluginsCore,unit_tests' }, + { scheme: AWSAPIPlugin, flags: 'API_plugin_unit_test,unit_tests' }, + { scheme: AWSCloudWatchLoggingPlugin, flags: 'Logging_plugin_unit_test,unit_tests' }, + { scheme: AWSCognitoAuthPlugin, flags: 'Auth_plugin_unit_test,unit_tests' }, + { scheme: AWSDataStorePlugin, flags: 'DataStore_plugin_unit_test,unit_tests' }, + { scheme: AWSLocationGeoPlugin, flags: 'Geo_plugin_unit_test,unit_tests' }, + { scheme: AWSPredictionsPlugin, flags: 'Predictions_plugin_unit_test,unit_tests' }, + { scheme: AWSPinpointAnalyticsPlugin, flags: 'Analytics_plugin_unit_test,unit_tests' }, + { scheme: AWSPinpointPushNotificationsPlugin, flags: 'PushNotifications_plugin_unit_test,unit_tests' }, + { scheme: AWSS3StoragePlugin, flags: 'Storage_plugin_unit_test,unit_tests' }, + { scheme: CoreMLPredictionsPlugin, flags: 'CoreMLPredictions_plugin_unit_test,unit_tests' } + ] + uses: ./.github/workflows/upload_coverage_report.yml + with: + scheme: ${{ matrix.file.scheme }} + flags: ${{ matrix.file.flags }} unit-test-pass-confirmation: runs-on: ubuntu-latest name: Confirm Passing Unit Tests if: ${{ !cancelled() }} needs: [ - unit-tests-with-coverage, - unit-tests-without-coverage + targets-with-coverage, + targets-without-coverage ] env: EXIT_CODE: ${{ contains(needs.*.result, 'failure') && 1 || 0 }} From 5497087dbe770233eec15c8b187f58dea5e75cbe Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Tue, 30 Apr 2024 19:48:38 +0000 Subject: [PATCH 46/86] chore: release 2.32.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 53d5a1416d..b4ab4ab955 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.32.0" + public static let amplifyVersion = "2.32.1" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From 7fbba024c922873b9c0f8ce97d88ea632c6e9fae Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Tue, 30 Apr 2024 19:50:23 +0000 Subject: [PATCH 47/86] chore: finalize release 2.32.1 [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6849cb7ea0..cc9cb67baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.32.1 (2024-04-30) + +### Bug Fixes + +- **Auth**: Refactoring state machine logic to fix memory leak (#3613) + ## 2.32.0 (2024-04-29) ### Features From 4c27645a8f819aba90f463592e19ec82031bfbf1 Mon Sep 17 00:00:00 2001 From: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> Date: Tue, 30 Apr 2024 18:25:31 -0400 Subject: [PATCH 48/86] feat: Updating to SQLite.swift 0.15.0 (#3659) --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 5be60bbe90..2be2cf993d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stephencelis/SQLite.swift.git", "state" : { - "revision" : "5f5ad81ac0d0a0f3e56e39e646e8423c617df523", - "version" : "0.13.2" + "revision" : "e78ae0220e17525a15ac68c697a155eb7a672a8e", + "version" : "0.15.0" } }, { diff --git a/Package.swift b/Package.swift index 77f5211f2a..c26132df6f 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let platforms: [SupportedPlatform] = [ ] let dependencies: [Package.Dependency] = [ .package(url: "https://github.com/awslabs/aws-sdk-swift.git", exact: "0.36.1"), - .package(url: "https://github.com/stephencelis/SQLite.swift.git", exact: "0.13.2"), + .package(url: "https://github.com/stephencelis/SQLite.swift.git", exact: "0.15.0"), .package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", from: "2.1.0"), .package(url: "https://github.com/aws-amplify/amplify-swift-utils-notifications.git", from: "1.1.0") ] From 5e964fa0df439a1854ca6866bb674e39229b6f55 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 1 May 2024 00:49:49 +0000 Subject: [PATCH 49/86] chore: release 2.33.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 b4ab4ab955..ac47f29a81 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.32.1" + public static let amplifyVersion = "2.33.0" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From 0009ab31928bceb113b0c43e3e02f0d71cb26355 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 1 May 2024 00:51:49 +0000 Subject: [PATCH 50/86] chore: finalize release 2.33.0 [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc9cb67baa..ef43e07ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.33.0 (2024-05-01) + +### Features + +- Updating to SQLite.swift 0.15.0 (#3659) + ## 2.32.1 (2024-04-30) ### Bug Fixes From 8ff89a8a4d49efdacb8716c162775456af90c534 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Wed, 1 May 2024 11:25:55 -0400 Subject: [PATCH 51/86] fix(auth): gen2 config oauth domain (#3663) --- .../Configuration/AmplifyOutputsData.swift | 3 +- .../Support/Helpers/ConfigurationHelper.swift | 2 +- .../Support/ConfigurationHelperTests.swift | 33 ++----------------- 3 files changed, 4 insertions(+), 34 deletions(-) diff --git a/Amplify/Core/Configuration/AmplifyOutputsData.swift b/Amplify/Core/Configuration/AmplifyOutputsData.swift index 5fb9435c2f..bdd7270017 100644 --- a/Amplify/Core/Configuration/AmplifyOutputsData.swift +++ b/Amplify/Core/Configuration/AmplifyOutputsData.swift @@ -66,8 +66,7 @@ public struct AmplifyOutputsData: Codable { @_spi(InternalAmplifyConfiguration) public struct OAuth: Codable { public let identityProviders: [String] - public let cognitoDomain: String - public let customDomain: String? + public let domain: String public let scopes: [String] public let redirectSignInUri: [String] public let redirectSignOutUri: [String] diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift index 877571243c..ae6d367070 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/ConfigurationHelper.swift @@ -159,7 +159,7 @@ struct ConfigurationHelper { return createHostedConfiguration(appClientId: configuration.userPoolClientId, clientSecret: nil, - domain: oauth.customDomain ?? oauth.cognitoDomain, + domain: oauth.domain, scopes: oauth.scopes, signInRedirectURI: signInRedirectURI, signOutRedirectURI: signOutRedirectURI) diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift index d2f5f732ff..ee6ea93a1e 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/ConfigurationHelperTests.swift @@ -46,8 +46,7 @@ final class ConfigurationHelperTests: XCTestCase { userPoolId: "poolId", userPoolClientId: "clientId", oauth: AmplifyOutputsData.Auth.OAuth(identityProviders: ["provider1", "provider2"], - cognitoDomain: "cognitoDomain", - customDomain: nil, + domain: "domain", scopes: ["scope1", "scope2"], redirectSignInUri: ["redirect1", "redirect2"], redirectSignOutUri: ["signOut1", "signOut2"], @@ -62,35 +61,7 @@ final class ConfigurationHelperTests: XCTestCase { 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.domain, "domain") XCTAssertEqual(hostedUIConfig.oauth.signInRedirectURI, "redirect1") XCTAssertEqual(hostedUIConfig.oauth.signOutRedirectURI, "signOut1") } From 455a7c451ff7be0d253cb39d09524650fa6c73a9 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 1 May 2024 16:08:42 +0000 Subject: [PATCH 52/86] chore: release 2.33.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 ac47f29a81..439bde4e54 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.33.0" + public static let amplifyVersion = "2.33.1" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From de37224f7a33d8347b75a7c2fdbe6b37b45a4942 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 1 May 2024 16:10:26 +0000 Subject: [PATCH 53/86] chore: finalize release 2.33.1 [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef43e07ac1..b85609e668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.33.1 (2024-05-01) + +### Bug Fixes + +- **auth**: gen2 config oauth domain (#3663) + ## 2.33.0 (2024-05-01) ### Features From 1e6f42bd388366892f2a5056bf2406b76b2cdb5a Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Wed, 1 May 2024 14:31:11 -0400 Subject: [PATCH 54/86] test(storage): gen2 integ tests (#3665) --- .../AWSS3StoragePluginAccessLevelTests.swift | 67 +++---------------- .../AWSS3StoragePluginTestBase.swift | 60 +++++++---------- ...3StoragePluginUploadMetadataTestCase.swift | 2 +- .../README.md | 18 ++--- 4 files changed, 46 insertions(+), 101 deletions(-) diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginAccessLevelTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginAccessLevelTests.swift index 00b6fd6147..5a0da31147 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginAccessLevelTests.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginAccessLevelTests.swift @@ -16,8 +16,6 @@ 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 @@ -51,17 +49,8 @@ class AWSS3StoragePluginAccessLevelTests: AWSS3StoragePluginTestBase { func testUploadAndRemoveForGuestOnly() async throws { let logger = Amplify.Logging.logger(forCategory: "Storage", logLevel: .verbose) - 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 username = AWSS3StoragePluginTestBase.user1.lowercased() + let password = AWSS3StoragePluginTestBase.password let accessLevel: StorageAccessLevel = .guest do { @@ -112,21 +101,13 @@ class AWSS3StoragePluginAccessLevelTests: AWSS3StoragePluginTestBase { .guest ] - 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 username = AWSS3StoragePluginTestBase.user1.lowercased() + let 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 @@ -176,39 +157,13 @@ 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, - user1: (username1, password), - user2: (username2, password)), + .init(label: "Guest", key: UUID().uuidString, accessLevel: .guest), // user 2 can read upload by user 1 with protected access - .init(label: "Protected", - key: UUID().uuidString, - accessLevel: .protected, - user1: (username1, password), - user2: (username2, password)), + .init(label: "Protected", key: UUID().uuidString, accessLevel: .protected), // user 2 can get access denied error from upload by user 1 with private access - .init(label: "Private", - key: UUID().uuidString, - accessLevel: .private, - user1: (username1, password), - user2: (username2, password)), + .init(label: "Private", key: UUID().uuidString, accessLevel: .private), ] for testRun in testRuns { @@ -218,8 +173,7 @@ class AWSS3StoragePluginAccessLevelTests: AWSS3StoragePluginTestBase { await signOut() logger.debug("Signing in user1") - let user1SignedIn = try await Amplify.Auth.signIn(username: testRun.user1.0, - password: testRun.user1.1).isSignedIn + let user1SignedIn = try await Amplify.Auth.signIn(username: AWSS3StoragePluginTestBase.user1, password: AWSS3StoragePluginTestBase.password).isSignedIn XCTAssertTrue(user1SignedIn) logger.debug("Getting identity for user1") @@ -242,8 +196,7 @@ class AWSS3StoragePluginAccessLevelTests: AWSS3StoragePluginTestBase { await signOut() logger.debug("Signing in as user2") - let user2SignedIn = try await Amplify.Auth.signIn(username: testRun.user2.0, - password: testRun.user2.1).isSignedIn + let user2SignedIn = try await Amplify.Auth.signIn(username: AWSS3StoragePluginTestBase.user2, password: AWSS3StoragePluginTestBase.password).isSignedIn XCTAssertTrue(user2SignedIn) logger.debug("Getting identity for user2") diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift index 2aedb7a1b6..35e4721be2 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginTestBase.swift @@ -118,27 +118,31 @@ class AWSS3StoragePluginTestBase: XCTestCase { func getBucketFromConfig(forResource: String) throws -> String { let data = try TestConfigHelper.retrieve(forResource: forResource) let json = try JSONDecoder().decode(JSONValue.self, from: data) - 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 let bucket = json["storage"]?["plugins"]?["awsS3StoragePlugin"]?["bucket"] else { + throw "Could not retrieve bucket from config" + } - return bucketValue - } else { - guard let bucket = json["storage"]?["plugins"]?["awsS3StoragePlugin"]?["bucket"] 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 + } - return bucketValue + func getBucketFromAmplifyOutputs(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"]?["bucket_name"] else { + throw "Could not retrieve bucket from config" } + + guard case let .string(bucketValue) = bucket else { + throw "bucket is not a string value" + } + + return bucketValue } func signUp() async { @@ -149,15 +153,9 @@ class AWSS3StoragePluginTestBase: XCTestCase { let registerFirstUserComplete = expectation(description: "register firt user completed") Task { do { - 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) - } + try await AuthSignInHelper.signUpUser(username: AWSS3StoragePluginTestBase.user1, + password: AWSS3StoragePluginTestBase.password, + email: AWSS3StoragePluginTestBase.email1) Self.isFirstUserSignedUp = true registerFirstUserComplete.fulfill() } catch { @@ -169,15 +167,9 @@ class AWSS3StoragePluginTestBase: XCTestCase { let registerSecondUserComplete = expectation(description: "register second user completed") Task { do { - 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) - } + 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 0fda026a51..92d6ec8a7a 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginUploadMetadataTestCase.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginUploadMetadataTestCase.swift @@ -240,7 +240,7 @@ class AWSS3StoragePluginUploadMetadataTestCase: AWSS3StoragePluginTestBase { let s3Client = storagePlugin.getEscapeHatch() let bucket: String if useGen2Configuration { - bucket = try getBucketFromConfig( + bucket = try getBucketFromAmplifyOutputs( forResource: "amplify_outputs" ) } else { diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md index 75f4dd97a2..e6b3e03e6c 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md @@ -115,18 +115,18 @@ At the time this was written, it follows the steps from here https://docs.amplif { ... "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", + "@aws-amplify/backend": "^0.15.0", + "@aws-amplify/backend-cli": "^0.15.0", + "aws-cdk": "^2.139.0", + "aws-cdk-lib": "^2.139.0", "constructs": "^10.3.0", "esbuild": "^0.20.2", - "tsx": "^4.7.1", - "typescript": "^5.4.3" + "tsx": "^4.7.3", + "typescript": "^5.4.5" }, "dependencies": { - "aws-amplify": "^6.0.25" - } + "aws-amplify": "^6.2.0" + }, } @@ -241,5 +241,5 @@ If you want to be able utilize Git commits for deployments 10. Generate the `amplify_outputs.json` configuration file ``` -npx amplify generate config --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 +npx amplify generate outputs --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 ``` From 0ea629d371f3704f4eb4cc18aa9e82c92794582b Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Wed, 1 May 2024 17:12:57 -0400 Subject: [PATCH 55/86] test(notifications): gen2 integ tests (#3667) --- .../LocalServer/package-lock.json | 493 +----------------- .../LocalServer/package.json | 2 +- .../PushNotificationGen2HostApp.xctestplan | 1 + .../PushNotificationHostApp copy.xcscheme | 77 --- .../PushNotificationHostApp/ContentView.swift | 2 +- .../PushNotificationHostAppUITests.swift | 8 +- .../PushNotificationHostAppUITests/README.md | 107 +++- .../README.md | 6 +- 8 files changed, 118 insertions(+), 578 deletions(-) delete mode 100644 AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationHostApp copy.xcscheme diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/LocalServer/package-lock.json b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/LocalServer/package-lock.json index 28f834f671..90c039c020 100644 --- a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/LocalServer/package-lock.json +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/LocalServer/package-lock.json @@ -1,7 +1,7 @@ { "name": "localserver", "version": "1.0.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "express": "^4.18.2" + "express": "^4.19.2" } }, "node_modules/accepts": { @@ -690,494 +690,5 @@ "node": ">= 0.8" } } - }, - "dependencies": { - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - } - }, - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "requires": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - } - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "requires": { - "safe-buffer": "5.2.1" - } - }, - "content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, - "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "requires": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } - }, - "es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - } - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "requires": { - "es-define-property": "^1.0.0" - } - }, - "has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "requires": { - "function-bind": "^1.1.2" - } - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "requires": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "requires": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - } - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - } } } diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/LocalServer/package.json b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/LocalServer/package.json index 9bf4864902..56bc2a2054 100644 --- a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/LocalServer/package.json +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/LocalServer/package.json @@ -9,6 +9,6 @@ "author": "Aws Amplify", "license": "Apache-2.0", "dependencies": { - "express": "^4.18.2" + "express": "^4.19.2" } } diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationGen2HostApp.xctestplan b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationGen2HostApp.xctestplan index 4f89e013f5..8a1c6e72f2 100644 --- a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationGen2HostApp.xctestplan +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationGen2HostApp.xctestplan @@ -23,6 +23,7 @@ }, "testTargets" : [ { + "parallelizable" : true, "target" : { "containerPath" : "container:PushNotificationHostApp.xcodeproj", "identifier" : "6084F1AB2967B87200434CBF", 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 deleted file mode 100644 index 2d57b01c49..0000000000 --- a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp.xcodeproj/xcshareddata/xcschemes/PushNotificationHostApp copy.xcscheme +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp/ContentView.swift b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp/ContentView.swift index 0dbe33d01f..41e72a36c7 100644 --- a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp/ContentView.swift +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostApp/ContentView.swift @@ -13,7 +13,7 @@ import AWSPinpointPushNotificationsPlugin import AWSPinpointAnalyticsPlugin let amplifyConfigurationFilePath = "testconfiguration/AWSPushNotificationPluginIntegrationTest-amplifyconfiguration" -let amplifyOutputsFilePath = "testconfiguration/AWSPushNotificationPluginIntegrationTest-amplifyconfiguration" +let amplifyOutputsFilePath = "testconfiguration/AWSPushNotificationPluginIntegrationTest-amplify_outputs" var pushNotificationHubSubscription: UnsubscribeToken? var analyticsHubSubscription: UnsubscribeToken? diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/PushNotificationHostAppUITests.swift b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/PushNotificationHostAppUITests.swift index ab504d95d2..fbf25d42eb 100644 --- a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/PushNotificationHostAppUITests.swift +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/PushNotificationHostAppUITests.swift @@ -9,7 +9,7 @@ import XCTest final class PushNotificationHostAppUITests: XCTestCase { - let timeout = TimeInterval(3) + let timeout = TimeInterval(6) let app = XCUIApplication() lazy var deviceIdentifier: String? = { @@ -82,9 +82,9 @@ final class PushNotificationHostAppUITests: XCTestCase { initAmplify() grantNotificationPermissionIfNeeded() - let identifyUserButton = app.buttons["Register Device"] - if identifyUserButton.waitForExistence(timeout: timeout) { - identifyUserButton.tap() + let registerDeviceButton = app.buttons["Register Device"] + if registerDeviceButton.waitForExistence(timeout: timeout) { + registerDeviceButton.tap() } else { XCTFail("Failed to find 'Register Device' button") } diff --git a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/README.md b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/README.md index 9e2c14f541..af758fe6ee 100644 --- a/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/README.md +++ b/AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/PushNotificationHostAppUITests/README.md @@ -37,12 +37,117 @@ cp amplifyconfiguration.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSPus The following steps demonstrate to set up the same as above with Amplify CLI Gen2. +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.15.0", + "@aws-amplify/backend-cli": "^0.15.0", + "aws-cdk": "^2.139.0", + "aws-cdk-lib": "^2.139.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "tsx": "^4.7.3", + "typescript": "^5.4.5" + }, + "dependencies": { + "aws-amplify": "^6.2.0" + }, +} +``` + +2. Update `amplify/backend.ts` to create the analytics stack (https://docs.amplify.aws/gen2/build-a-backend/add-aws-services/analytics/) and notifications stack + +```ts +const pushNotificationStack = backend.createStack('pushNotification-analytics-stack') +const pinpoint = new CfnApp(pushNotificationStack, "PinPoint", { + name: "pushNotification-pinPoint" +}) + +const pinpointPolicy = new Policy(pushNotificationStack, "PinpointPolicy", { + policyName: "PinpointPolicy", + statements: [ + new PolicyStatement({ + actions: ["mobiletargeting:*"], + resources: [pinpoint.attrArn + "/*"], + }), + ], +}); + +backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(pinpointPolicy) +backend.auth.resources.unauthenticatedUserIamRole.attachInlinePolicy(pinpointPolicy) + +backend.addOutput({ + analytics: { + amazon_pinpoint: { + app_id: pinpoint.ref, + aws_region: Stack.of(pinpoint).region, + } + } +}) + +backend.addOutput({ + notifications: { + amazon_pinpoint_app_id: pinpoint.ref, + aws_region: Stack.of(pushNotificationStack).region, + channels: ['APNS'] + } +}) +``` + +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 + } +}); + +``` + +3. 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 --profile [PROFILE] +``` -1. Copy `amplify_outputs.json` to `AWSPushNotificationPluginIntegrationTest-amplify_outputs.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/` +4. 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 ``` +### 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 outputs --branch main --app-id [APP_ID] --profile [AWS_PROFILE] +``` ## Run Integration Tests diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md index e6b3e03e6c..871be5d98e 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/README.md @@ -128,7 +128,7 @@ At the time this was written, it follows the steps from here https://docs.amplif "aws-amplify": "^6.2.0" }, } - +``` 2. Update `amplify/storage/resource.ts`. The resulting file should look like this @@ -213,7 +213,7 @@ cfnUserPool.addPropertyOverride( 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] +npx amplify sandbox --config-out-dir ./config --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. @@ -241,5 +241,5 @@ If you want to be able utilize Git commits for deployments 10. Generate the `amplify_outputs.json` configuration file ``` -npx amplify generate outputs --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 +npx amplify generate outputs --branch main --app-id [APP_ID] --profile [AWS_PROFILE] ``` From 6f1a0898976ee694d910ef7ecb6a3d108b6ea00d Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 2 May 2024 16:50:35 +0000 Subject: [PATCH 56/86] fix(core): potential memory leak in WebSocketClient (#3624) --- .../Core/AWSPluginsCore/WebSocket/WebSocketClient.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketClient.swift b/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketClient.swift index bc677744f8..cc1149ac27 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketClient.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketClient.swift @@ -265,7 +265,7 @@ extension WebSocketClient: URLSessionWebSocketDelegate { extension WebSocketClient { /// Monitor network status. Disconnect or reconnect when the network drops or comes back online. private func startNetworkMonitor() { - networkMonitor.publisher.sink(receiveValue: { stateChange in + networkMonitor.publisher.sink(receiveValue: { [weak self] stateChange in Task { [weak self] in await self?.onNetworkStateChange(stateChange) } @@ -304,7 +304,7 @@ extension WebSocketClient { return closeCode } .compactMap { $0 } - .sink(receiveCompletion: { _ in }) { closeCode in + .sink(receiveCompletion: { _ in }) { [weak self] closeCode in Task { [weak self] in await self?.retryOnCloseCode(closeCode) } } .store(in: &cancelables) @@ -319,7 +319,7 @@ extension WebSocketClient { } return false } - .sink(receiveCompletion: { _ in }) { _ in + .sink(receiveCompletion: { _ in }) { [weak self] _ in Task { [weak self] in await self?.retryWithJitter.reset() } From 363b903a586cddcb0aeba30708a2510109bbf3f2 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Thu, 2 May 2024 13:45:13 -0400 Subject: [PATCH 57/86] chore(analytics): integ readme clean up (#3668) --- .../README.md | 42 +++++-------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/README.md b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/README.md index eb10747a96..bb943c2ced 100644 --- a/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/README.md +++ b/AmplifyPlugins/Analytics/Tests/AnalyticsHostApp/AWSPinpointAnalyticsPluginIntegrationTests/README.md @@ -42,20 +42,19 @@ At the time this was written, it follows the steps from here https://docs.amplif { ... "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", + "@aws-amplify/backend": "^0.15.0", + "@aws-amplify/backend-cli": "^0.15.0", + "aws-cdk": "^2.139.0", + "aws-cdk-lib": "^2.139.0", "constructs": "^10.3.0", "esbuild": "^0.20.2", - "tsx": "^4.7.1", - "typescript": "^5.4.3" + "tsx": "^4.7.3", + "typescript": "^5.4.5" }, "dependencies": { - "aws-amplify": "^6.0.25" - } + "aws-amplify": "^6.2.0" + }, } - ``` 2. Update `amplify/auth/resource.ts`. The resulting file should look like this @@ -69,27 +68,11 @@ import { defineAuth, defineFunction } from '@aws-amplify/backend'; 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 @@ -104,10 +87,7 @@ Create `backend` const ```ts const backend = defineBackend({ - auth, - // data, - // storage - // additional resource + auth }); ``` @@ -152,7 +132,7 @@ backend.addOutput({ 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] +npx amplify sandbox --config-out-dir ./config --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. @@ -180,6 +160,6 @@ If you want to be able utilize Git commits for deployments 7. Generate the `amplify_outputs.json` configuration file ``` -npx amplify generate config --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 +npx amplify generate outputs --branch main --app-id [APP_ID] --profile [AWS_PROFILE] ``` From 95a53ab4158ee3c0f9a3914c50b881a15ec3c3bd Mon Sep 17 00:00:00 2001 From: Harsh <6162866+harsh62@users.noreply.github.com> Date: Thu, 2 May 2024 15:06:04 -0400 Subject: [PATCH 58/86] fix(Auth): Add keychain logging for better debugging (#3669) * fix(Auth): Add keychain logging for better debugging * Update AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift Co-authored-by: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> * Update AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift Co-authored-by: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> * Update AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift Co-authored-by: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> --------- Co-authored by: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> --- .../Keychain/KeychainStore.swift | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift b/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift index cbacba2e44..6327676c83 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Keychain/KeychainStore.swift @@ -7,6 +7,7 @@ import Foundation import Security +import Amplify // swiftlint:disable identifier_name public protocol KeychainStoreBehavior { @@ -76,6 +77,7 @@ public struct KeychainStore: KeychainStoreBehavior { var attributes = KeychainStoreAttributes(service: service) attributes.accessGroup = accessGroup self.init(attributes: attributes) + log.verbose("[KeychainStore] Initialized keychain with service=\(service), attributes=\(attributes), accessGroup=\(accessGroup ?? "")") } @_spi(KeychainStore) @@ -84,12 +86,13 @@ public struct KeychainStore: KeychainStoreBehavior { /// - Parameter key: A String key use to look up the value in the Keychain /// - Returns: A string value public func _getString(_ key: String) throws -> String { - + log.verbose("[KeychainStore] Started retrieving `String` from the store with key=\(key)") let data = try _getData(key) - guard let string = String(data: data, encoding: .utf8) else { + log.error("[KeychainStore] Unable to create String from Data retrieved") throw KeychainStoreError.conversionError("Unable to create String from Data retrieved") } + log.verbose("[KeychainStore] Successfully retrieved string from the store") return string } @@ -100,6 +103,7 @@ public struct KeychainStore: KeychainStoreBehavior { /// - Parameter key: A String key use to look up the value in the Keychain /// - Returns: A data value public func _getData(_ key: String) throws -> Data { + log.verbose("[KeychainStore] Started retrieving `Data` from the store with key=\(key)") var query = attributes.defaultGetQuery() query[Constants.MatchLimit] = Constants.MatchLimitOne @@ -113,12 +117,16 @@ public struct KeychainStore: KeychainStoreBehavior { switch status { case errSecSuccess: guard let data = result as? Data else { + log.error("[KeychainStore] The keychain item retrieved is not the correct type") throw KeychainStoreError.unknown("The keychain item retrieved is not the correct type") } + log.verbose("[KeychainStore] Successfully retrieved `Data` from the store with key=\(key)") return data case errSecItemNotFound: + log.verbose("[KeychainStore] No Keychain item found for key=\(key)") throw KeychainStoreError.itemNotFound default: + log.error("[KeychainStore] Error of status=\(status) occurred when attempting to retrieve a Keychain item for key=\(key)") throw KeychainStoreError.securityError(status) } } @@ -130,10 +138,13 @@ public struct KeychainStore: KeychainStoreBehavior { /// - value: A string value to store in Keychain /// - key: A String key for the value to store in the Keychain public func _set(_ value: String, key: String) throws { + log.verbose("[KeychainStore] Started setting `String` for key=\(key)") guard let data = value.data(using: .utf8, allowLossyConversion: false) else { + log.error("[KeychainStore] Unable to create Data from String retrieved for key=\(key)") throw KeychainStoreError.conversionError("Unable to create Data from String retrieved") } try _set(data, key: key) + log.verbose("[KeychainStore] Successfully added `String` for key=\(key)") } @_spi(KeychainStore) @@ -143,34 +154,43 @@ public struct KeychainStore: KeychainStoreBehavior { /// - value: A data value to store in Keychain /// - key: A String key for the value to store in the Keychain public func _set(_ value: Data, key: String) throws { + log.verbose("[KeychainStore] Started setting `Data` for key=\(key)") var getQuery = attributes.defaultGetQuery() getQuery[Constants.AttributeAccount] = key - + log.verbose("[KeychainStore] Initialized fetching to decide whether update or add") let fetchStatus = SecItemCopyMatching(getQuery as CFDictionary, nil) switch fetchStatus { case errSecSuccess: #if os(macOS) + log.verbose("[KeychainStore] Deleting item on MacOS to add an item.") SecItemDelete(getQuery as CFDictionary) fallthrough #else + log.verbose("[KeychainStore] Found existing item, updating") var attributesToUpdate = [String: Any]() attributesToUpdate[Constants.ValueData] = value let updateStatus = SecItemUpdate(getQuery as CFDictionary, attributesToUpdate as CFDictionary) if updateStatus != errSecSuccess { + log.error("[KeychainStore] Error updating item to keychain with status=\(updateStatus)") throw KeychainStoreError.securityError(updateStatus) } + log.verbose("[KeychainStore] Successfully updated `String` in keychain for key=\(key)") #endif case errSecItemNotFound: + log.verbose("[KeychainStore] Unable to find an existing item, creating new item") var attributesToSet = attributes.defaultSetQuery() attributesToSet[Constants.AttributeAccount] = key attributesToSet[Constants.ValueData] = value let addStatus = SecItemAdd(attributesToSet as CFDictionary, nil) if addStatus != errSecSuccess { + log.error("[KeychainStore] Error adding item to keychain with status=\(addStatus)") throw KeychainStoreError.securityError(addStatus) } + log.verbose("[KeychainStore] Successfully added `String` in keychain for key=\(key)") default: + log.error("[KeychainStore] Error occurred while retrieving data from keychain when deciding to update or add with status=\(fetchStatus)") throw KeychainStoreError.securityError(fetchStatus) } } @@ -180,19 +200,23 @@ public struct KeychainStore: KeychainStoreBehavior { /// This System Programming Interface (SPI) may have breaking changes in future updates. /// - Parameter key: A String key to delete the key-value pair public func _remove(_ key: String) throws { + log.verbose("[KeychainStore] Starting to remove item from keychain with key=\(key)") var query = attributes.defaultGetQuery() query[Constants.AttributeAccount] = key let status = SecItemDelete(query as CFDictionary) if status != errSecSuccess && status != errSecItemNotFound { + log.error("[KeychainStore] Error removing itms from keychain with status=\(status)") throw KeychainStoreError.securityError(status) } + log.verbose("[KeychainStore] Successfully removed item from keychain") } @_spi(KeychainStore) /// Removes all key-value pair in the Keychain. /// This System Programming Interface (SPI) may have breaking changes in future updates. public func _removeAll() throws { + log.verbose("[KeychainStore] Starting to remove all items from keychain") var query = attributes.defaultGetQuery() #if !os(iOS) && !os(watchOS) && !os(tvOS) query[Constants.MatchLimit] = Constants.MatchLimitAll @@ -200,8 +224,10 @@ public struct KeychainStore: KeychainStoreBehavior { let status = SecItemDelete(query as CFDictionary) if status != errSecSuccess && status != errSecItemNotFound { + log.error("[KeychainStore] Error removing all items from keychain with status=\(status)") throw KeychainStoreError.securityError(status) } + log.verbose("[KeychainStore] Successfully removed all items from keychain") } } @@ -241,3 +267,11 @@ extension KeychainStore { } } // swiftlint:enable identifier_name + +extension KeychainStore: DefaultLogger { + public static var log: Logger { + Amplify.Logging.logger(forNamespace: String(describing: self)) + } + + public nonisolated var log: Logger { Self.log } +} From 9c1cf85e9e25118ffd937746f350721d6a6548fa Mon Sep 17 00:00:00 2001 From: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> Date: Thu, 2 May 2024 16:24:30 -0400 Subject: [PATCH 59/86] chore: Upgrading to SDK 0.36.2 (#3670) --- Package.resolved | 8 ++++---- Package.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.resolved b/Package.resolved index 2be2cf993d..3e7e7f61f5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/awslabs/aws-sdk-swift.git", "state" : { - "revision" : "485501db8b0c57d6636c2b33a03dcd77ee0911f9", - "version" : "0.36.1" + "revision" : "47922c05dd66be717c7bce424651a534456717b7", + "version" : "0.36.2" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/smithy-lang/smithy-swift", "state" : { - "revision" : "64e66f3e3ac07d4180c2460a7af6e4ba4b92e9cb", - "version" : "0.41.0" + "revision" : "8a5b0105c1b8a1d26a9435fb0af3959a7f5de578", + "version" : "0.41.1" } }, { diff --git a/Package.swift b/Package.swift index c26132df6f..25639dce9c 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ let platforms: [SupportedPlatform] = [ .watchOS(.v9) ] let dependencies: [Package.Dependency] = [ - .package(url: "https://github.com/awslabs/aws-sdk-swift.git", exact: "0.36.1"), + .package(url: "https://github.com/awslabs/aws-sdk-swift.git", exact: "0.36.2"), .package(url: "https://github.com/stephencelis/SQLite.swift.git", exact: "0.15.0"), .package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", from: "2.1.0"), .package(url: "https://github.com/aws-amplify/amplify-swift-utils-notifications.git", from: "1.1.0") From cb90dd1b16c60c0e140723088b97d926831b5e83 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Thu, 2 May 2024 22:01:03 +0000 Subject: [PATCH 60/86] chore: release 2.33.2 [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 439bde4e54..697b60fc40 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.33.1" + public static let amplifyVersion = "2.33.2" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From 8ebd804b4169bd0d9f7a18a87f04d443a726ff69 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Thu, 2 May 2024 22:02:36 +0000 Subject: [PATCH 61/86] chore: finalize release 2.33.2 [skip ci] --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b85609e668..e7dcbb728b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2.33.2 (2024-05-02) + +### Bug Fixes + +- **Auth**: Add keychain logging for better debugging (#3669) +- **core**: potential memory leak in WebSocketClient (#3624) + ## 2.33.1 (2024-05-01) ### Bug Fixes From 89abfcd0f03fa68b748d0db0a1e0ccd2a1bcac91 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Fri, 3 May 2024 13:29:15 -0700 Subject: [PATCH 62/86] fix(auth): Using a custom Foundation-based HTTPClient for HTTP Requests (#3582) --------- Co-authored-by: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> --- .../ClientRuntimeFoundationBridge.swift | 60 ++++++++++++++ .../FoundationClientEngine.swift | 37 +++++++++ .../FoundationClientEngineError.swift | 81 +++++++++++++++++++ .../PluginClientEngine.swift | 21 +---- ...3StoragePluginGetURLIntegrationTests.swift | 3 +- 5 files changed, 181 insertions(+), 21 deletions(-) create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift new file mode 100644 index 0000000000..18fa1470b2 --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift @@ -0,0 +1,60 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import ClientRuntime + +extension Foundation.URLRequest { + init(sdkRequest: ClientRuntime.SdkHttpRequest) async throws { + guard let url = sdkRequest.endpoint.url else { + throw FoundationClientEngineError.invalidRequestURL(sdkRequest: sdkRequest) + } + self.init(url: url) + httpMethod = sdkRequest.method.rawValue + + for header in sdkRequest.headers.headers { + for value in header.value { + addValue(value, forHTTPHeaderField: header.name) + } + } + + httpBody = try await sdkRequest.body.readData() + } +} + +extension ClientRuntime.HttpResponse { + private static func headers( + from allHeaderFields: [AnyHashable: Any] + ) -> ClientRuntime.Headers { + var headers = Headers() + for header in allHeaderFields { + switch (header.key, header.value) { + case let (key, value) as (String, String): + headers.add(name: key, value: value) + case let (key, values) as (String, [String]): + headers.add(name: key, values: values) + default: continue + } + } + return headers + } + + convenience init(httpURLResponse: HTTPURLResponse, data: Data) throws { + let headers = Self.headers(from: httpURLResponse.allHeaderFields) + let body = ByteStream.data(data) + + guard let statusCode = HttpStatusCode(rawValue: httpURLResponse.statusCode) else { + // This shouldn't happen, but `HttpStatusCode` only exposes a failable + // `init`. The alternative here is force unwrapping, but we can't + // make the decision to crash here on behalf on consuming applications. + throw FoundationClientEngineError.unexpectedStatusCode( + statusCode: httpURLResponse.statusCode + ) + } + self.init(headers: headers, body: body, statusCode: statusCode) + } +} diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift new file mode 100644 index 0000000000..d8f18fe889 --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift @@ -0,0 +1,37 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import ClientRuntime +import Amplify + +@_spi(FoundationClientEngine) +public struct FoundationClientEngine: HTTPClient { + public func send(request: ClientRuntime.SdkHttpRequest) async throws -> ClientRuntime.HttpResponse { + let urlRequest = try await URLRequest(sdkRequest: request) + + let (data, response) = try await URLSession.shared.data(for: urlRequest) + guard let httpURLResponse = response as? HTTPURLResponse else { + // This shouldn't be necessary because we're only making HTTP requests. + // `URLResponse` should always be a `HTTPURLResponse`. + // But to refrain from crashing consuming applications, we're throwing here. + throw FoundationClientEngineError.invalidURLResponse(urlRequest: response) + } + + let httpResponse = try HttpResponse( + httpURLResponse: httpURLResponse, + data: data + ) + + return httpResponse + } + + public init() {} + + /// no-op + func close() async {} +} diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift new file mode 100644 index 0000000000..09e6df49ef --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift @@ -0,0 +1,81 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Amplify +import ClientRuntime + +struct FoundationClientEngineError: AmplifyError { + let errorDescription: ErrorDescription + let recoverySuggestion: RecoverySuggestion + let underlyingError: Error? + + // protocol requirement + init( + errorDescription: ErrorDescription, + recoverySuggestion: RecoverySuggestion, + error: Error + ) { + self.errorDescription = errorDescription + self.recoverySuggestion = recoverySuggestion + self.underlyingError = error + } +} + +extension FoundationClientEngineError { + init( + errorDescription: ErrorDescription, + recoverySuggestion: RecoverySuggestion, + error: Error? + ) { + self.errorDescription = errorDescription + self.recoverySuggestion = recoverySuggestion + self.underlyingError = error + } + + static func invalidRequestURL(sdkRequest: ClientRuntime.SdkHttpRequest) -> Self { + .init( + errorDescription: """ + The SdkHttpRequest generated by ClientRuntime doesn't include a valid URL + - \(sdkRequest) + """, + recoverySuggestion: """ + Please open an issue at https://github.com/aws-amplify/amplify-swift + with the contents of this error message. + """, + error: nil + ) + } + + static func invalidURLResponse(urlRequest: URLResponse) -> Self { + .init( + errorDescription: """ + The URLResponse received is not an HTTPURLResponse + - \(urlRequest) + """, + recoverySuggestion: """ + Please open an issue at https://github.com/aws-amplify/amplify-swift + with the contents of this error message. + """, + error: nil + ) + } + + static func unexpectedStatusCode(statusCode: Int) -> Self { + .init( + errorDescription: """ + The status code received isn't a valid `HttpStatusCode` value. + - status code: \(statusCode) + """, + recoverySuggestion: """ + Please open an issue at https://github.com/aws-amplify/amplify-swift + with the contents of this error message. + """, + error: nil + ) + } +} diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift index cb25e6dc8e..6a2e8d7544 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift @@ -13,24 +13,5 @@ import AWSClientRuntime public func baseClientEngine( for configuration: AWSClientConfiguration ) -> HTTPClient { - - /// An example of how a client engine provided by aws-swift-sdk can be overridden - /// ``` - /// let baseClientEngine: HTTPClient - /// #if os(iOS) || os(macOS) - /// // networking goes through default aws sdk engine - /// baseClientEngine = configuration.httpClientEngine - /// #else - /// // The custom client engine from where we want to route requests - /// // FoundationClientEngine() was an example used in 2.26.x and before - /// baseClientEngine = - /// #endif - /// return baseClientEngine - /// ``` - /// - /// Starting aws-sdk-release 0.34.0, base HTTP client has been defaulted to foundation. - /// Hence, amplify doesn't need an override. So return the httpClientEngine present in the configuration. - return configuration.httpClientEngine - - + return FoundationClientEngine() } diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift index d8a4496e82..f1170140f7 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift @@ -25,7 +25,8 @@ class AWSS3StoragePluginGetURLIntegrationTests: AWSS3StoragePluginTestBase { _ = try await Amplify.Storage.uploadData( path: .fromString(key), data: Data(key.utf8), - options: .init()) + options: .init() + ).value let remoteURL = try await Amplify.Storage.getURL(path: .fromString(key)) From 4bdcfe5038852e677b7ff2096c9262523a575f32 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Fri, 3 May 2024 21:14:51 +0000 Subject: [PATCH 63/86] chore: release 2.33.3 [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 697b60fc40..5a5837bf37 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.33.2" + public static let amplifyVersion = "2.33.3" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From 7846328106dba471b3fb35170155e92aad50d427 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Fri, 3 May 2024 21:16:59 +0000 Subject: [PATCH 64/86] chore: finalize release 2.33.3 [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7dcbb728b..288952be9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.33.3 (2024-05-03) + +### Bug Fixes + +- **auth**: Using a custom Foundation-based HTTPClient for HTTP Requests (#3582) + ## 2.33.2 (2024-05-02) ### Bug Fixes From f078184af7d112900837c41bc1ab2b03d6695360 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Mon, 6 May 2024 13:45:03 -0400 Subject: [PATCH 65/86] test(auth): gen2 integ tests (#3673) --- .../AWSAuthBaseTest.swift | 2 +- .../AuthDeleteUserTests.swift | 3 + .../TOTPSetupWhenAuthenticatedTests.swift | 1 - .../TOTPSetupWhenUnauthenticatedTests.swift | 1 + .../AuthIntegrationTests/README.md | 75 ++++++++++++++++--- .../AuthConfirmResetPasswordTests.swift | 9 ++- .../AuthResetPasswordTests.swift | 17 ++++- .../SignInTests/AuthSRPSignInTests.swift | 1 + .../SignUpTests/AuthConfirmSignUpTests.swift | 16 +++- .../AuthResendSignUpCodeTests.swift | 15 +++- 10 files changed, 114 insertions(+), 26 deletions(-) diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift index ee974fa662..69668eee0d 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AWSAuthBaseTest.swift @@ -27,7 +27,7 @@ class AWSAuthBaseTest: XCTestCase { } var amplifyConfigurationFile = "testconfiguration/AWSCognitoAuthPluginIntegrationTests-amplifyconfiguration" - let amplifyOutputsFile = + var amplifyOutputsFile = "testconfiguration/AWSCognitoAuthPluginIntegrationTests-amplify_outputs" let credentialsFile = "testconfiguration/AWSCognitoAuthPluginIntegrationTests-credentials" diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AuthDeleteUserTests/AuthDeleteUserTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AuthDeleteUserTests/AuthDeleteUserTests.swift index a7d09a8796..6a34b479ed 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AuthDeleteUserTests/AuthDeleteUserTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/AuthDeleteUserTests/AuthDeleteUserTests.swift @@ -48,6 +48,9 @@ class AuthDeleteUserTests: AWSAuthBaseTest { do { _ = try await AuthSignInHelper.signInUser(username: username, password: password) XCTFail("signIn after account deletion should fail") + } catch AuthError.notAuthorized { + // App clients with "Prevent user existence errors" enabled will return this. + // https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-managing-errors.html } catch let error as AuthError { switch error { case .service(_, _, let underlying): diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/TOTPSetupWhenAuthenticatedTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/TOTPSetupWhenAuthenticatedTests.swift index e0a04174c2..f76d5d5c1a 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/TOTPSetupWhenAuthenticatedTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/TOTPSetupWhenAuthenticatedTests.swift @@ -23,7 +23,6 @@ class TOTPSetupWhenAuthenticatedTests: AWSAuthBaseTest { password: password, email: randomEmail ) - XCTAssertTrue(didSucceed, "Signup and sign in should succeed") } diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/TOTPSetupWhenUnauthenticatedTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/TOTPSetupWhenUnauthenticatedTests.swift index 216c48f7ef..4e514f81e4 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/TOTPSetupWhenUnauthenticatedTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/MFATests/TOTPSetupWhenUnauthenticatedTests.swift @@ -14,6 +14,7 @@ class TOTPSetupWhenUnauthenticatedTests: AWSAuthBaseTest { override func setUp() async throws { // Use a custom configuration these tests amplifyConfigurationFile = "testconfiguration/AWSCognitoAuthPluginMFARequiredIntegrationTests-amplifyconfiguration" + amplifyOutputsFile = "testconfiguration/AWSCognitoAuthPluginMFARequiredIntegrationTests-amplify_outputs" try await super.setUp() AuthSessionHelper.clearSession() } diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/README.md b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/README.md index 271a8d22d9..aac4451920 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/README.md +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/README.md @@ -6,6 +6,12 @@ The following steps demonstrate how to setup the integration tests for auth plug The integration test require auth configured with AWS Cognito User Pool and AWS Cognito Identity Pool. +Create the auth resource with the following use cases: +- Sign in with username +- A pre-sign up lambda to auto confirm a user signing up. +- MFA enabled and optional for TOTP. + +Example `amplify add auth` steps (some of these steps may not be out of date) ``` amplify add auth @@ -87,8 +93,17 @@ 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?" +For User Attributes tests: +Follow steps here (https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-email-phone-verification.html?icmpid=docs_cognito_console_help_panel#user-pool-settings-verifications-verify-attribute-updates) and uncheck "Keep original attribute value active when an update is pending". + +For MFA required tests (tests in `TOTPSetupWhenUnauthenticatedTests.swift`): + +1. Create a new amplify project (`amplify init`) and follow the same steps as above, except MFA is required. +2. Then copy over the configuration as +``` ~/.aws-amplify/amplify-ios/testconfiguration/AWSCognitoAuthPluginMFARequiredIntegrationTests-amplifyconfiguration.json +``` -# Schema: AuthGen2IntegrationTests +# Schema: AuthGen2IntegrationTests ## Schema: AuthGen2IntegrationTests @@ -104,21 +119,21 @@ At the time this was written, it follows the steps from here https://docs.amplif { ... "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", + "@aws-amplify/backend": "^0.15.0", + "@aws-amplify/backend-cli": "^0.15.0", + "aws-cdk": "^2.139.0", + "aws-cdk-lib": "^2.139.0", "constructs": "^10.3.0", "esbuild": "^0.20.2", - "tsx": "^4.7.1", - "typescript": "^5.4.3" + "tsx": "^4.7.3", + "typescript": "^5.4.5" }, "dependencies": { - "aws-amplify": "^6.0.25" - } + "aws-amplify": "^6.2.0" + }, } - ``` + 2. Update `amplify/auth/resource.ts`. The resulting file should look like this ```ts @@ -132,6 +147,11 @@ export const auth = defineAuth({ loginWith: { email: true }, + multifactor: { + mode: 'OPTIONAL', + totp: true, + sms: true, + }, triggers: { // configure a trigger to point to a function definition preSignUp: defineFunction({ @@ -155,6 +175,8 @@ export const handler: PreSignUpTriggerHandler = async (event) => { Update `backend.ts` ```ts +// Override sign in with username as the username + const { cfnUserPool } = backend.auth.resources.cfnResources cfnUserPool.usernameAttributes = [] @@ -171,6 +193,22 @@ cfnUserPool.addPropertyOverride( }, } ); + +// Enable Device Tracking +// https://docs.amplify.aws/react/build-a-backend/auth/concepts/multi-factor-authentication/#remember-a-device + +cfnUserPool.addPropertyOverride('DeviceConfiguration', { + ChallengeRequiredOnNewDevice: true, + DeviceOnlyRememberedOnUserPrompt: false +}); + +// Disable verifying updates to email addresses +// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-userattributeupdatesettings.html + +cfnUserPool.addPropertyOverride('UserAttributeUpdateSettings', { + AttributesRequireVerificationBeforeUpdate: [] +}); + ``` 4. Deploy the backend with npx amplify sandbox @@ -178,7 +216,7 @@ cfnUserPool.addPropertyOverride( 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] +npx amplify sandbox --config-out-dir ./config --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. @@ -187,6 +225,19 @@ npx amplify sandbox --config-out-dir ./config --config-version 1 --profile [PROF cp amplify_outputs.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSCognitoAuthPluginIntegrationTests-amplify_outputs.json ``` +6. For MFA required (tests in `TOTPSetupWhenUnauthenticatedTests.swift`), update `amplify/auth/resource.ts` with multifactor mode required. + +``` +// ... + multifactor: { + mode: 'REQUIRED', +// ... +``` + +7. Deploy, it may be easier to deploy a separate backend for MFA required using branches (see below) copy over the configuration as +``` ~/.aws-amplify/amplify-ios/testconfiguration/AWSCognitoAuthPluginMFARequiredIntegrationTests-amplify_outputs.json +``` + ### Deploying from a branch (Optional) If you want to be able utilize Git commits for deployments @@ -206,5 +257,5 @@ If you want to be able utilize Git commits for deployments 7. Generate the `amplify_outputs.json` configuration file ``` -npx amplify generate config --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 +npx amplify generate outputs --branch main --app-id [APP_ID] --profile [AWS_PROFILE] ``` diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/ResetPasswordTests/AuthConfirmResetPasswordTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/ResetPasswordTests/AuthConfirmResetPasswordTests.swift index c951fdc3a7..299d67b6a1 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/ResetPasswordTests/AuthConfirmResetPasswordTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/ResetPasswordTests/AuthConfirmResetPasswordTests.swift @@ -17,16 +17,17 @@ class AuthConfirmResetPasswordTests: AWSAuthBaseTest { /// - When: /// - I invoke confirmResetPassword with the user /// - Then: - /// - I should get a userNotFound error. - /// + /// - I should get a userNotFound error. (Gen1 - PreventUserExistenceErrors disabled) + /// - I should get a codeExpired error. (Gen2 - PreventUserExistenceErrors enabled) + /// (https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-managing-errors.html#cognito-user-pool-managing-errors-password-reset) func testUserNotFoundResetPassword() async throws { do { try await Amplify.Auth.confirmResetPassword(for: "user-non-exists", with: "password", confirmationCode: "123", options: nil) XCTFail("resetPassword with non existing user should not return result") - } catch AuthError.service(_, _, let error as AWSCognitoAuthError) where [.userNotFound, .limitExceeded].contains(error) { + } catch AuthError.service(_, _, let error as AWSCognitoAuthError) where [.userNotFound, .codeExpired, .limitExceeded].contains(error) { return } catch { - XCTFail("Expected .userNotFound or .limitExceeded error. received: \(error)") + XCTFail("Expected .userNotFound, .codeExpired, or .limitExceeded error. received: \(error)") } } } diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/ResetPasswordTests/AuthResetPasswordTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/ResetPasswordTests/AuthResetPasswordTests.swift index fed368aeac..cabe4a672b 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/ResetPasswordTests/AuthResetPasswordTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/ResetPasswordTests/AuthResetPasswordTests.swift @@ -21,8 +21,21 @@ class AuthResetPasswordTests: AWSAuthBaseTest { /// func testUserNotFoundResetPassword() async throws { do { - _ = try await Amplify.Auth.resetPassword(for: "user-non-exists", options: nil) - XCTFail("resetPassword with non existing user should not return result") + let randomUserNotExists = UUID().uuidString + let result = try await Amplify.Auth.resetPassword(for: randomUserNotExists, options: nil) + + // App clients with "Prevent user existence errors" enabled will return a simulated result + // https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-managing-errors.html#cognito-user-pool-managing-errors-password-reset + // Gen2 configuration is enabled with Prevent User existence errors, while Gen1 backend is not. + if useGen2Configuration { + XCTAssertFalse(result.isPasswordReset) + guard case .confirmResetPasswordWithCode = result.nextStep else { + XCTFail("Expected confirmResultPasswordCode in result, result: \(result)") + return + } + } else { + XCTFail("resetPassword with non existing user should not return result, result returned: \(result)") + } } catch AuthError.service(_, _, let error as AWSCognitoAuthError) where [.userNotFound, .limitExceeded].contains(error) { return } catch { diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignInTests/AuthSRPSignInTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignInTests/AuthSRPSignInTests.swift index 9f65e88532..ffbecc10d4 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignInTests/AuthSRPSignInTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignInTests/AuthSRPSignInTests.swift @@ -157,6 +157,7 @@ class AuthSRPSignInTests: AWSAuthBaseTest { XCTFail("SignIn with unknown user should not succeed") } catch AuthError.notAuthorized { // App clients with "Prevent user existence errors" enabled will return this. + // https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-managing-errors.html } catch let error as AuthError { let underlyingError = error.underlyingError as? AWSCognitoAuthError switch underlyingError { diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthConfirmSignUpTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthConfirmSignUpTests.swift index 6707fbc286..b706fcf957 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthConfirmSignUpTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthConfirmSignUpTests.swift @@ -17,18 +17,26 @@ class AuthConfirmSignUpTests: AWSAuthBaseTest { /// - When: /// - I invoke confirmSignUp with the user /// - Then: - /// - I should get a userNotFound error. + /// - I should get a userNotFound error. (Gen1 - PreventUserExistenceErrors disabled) + /// - I should get a codeMismatch error. (Gen2 - PreventUserExistenceErrors enabled) + /// (https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-managing-errors.html#cognito-user-pool-managing-errors-password-reset) /// func testUserNotFoundConfirmSignUp() async throws { do { _ = try await Amplify.Auth.confirmSignUp(for: "user-non-exists", confirmationCode: "232") XCTFail("Confirm signUp with non existing user should not return result") } catch { - guard let authError = error as? AuthError, let cognitoError = authError.underlyingError as? AWSCognitoAuthError, - case .userNotFound = cognitoError else { - XCTFail("Should return userNotFound") + guard let authError = error as? AuthError, let cognitoError = authError.underlyingError as? AWSCognitoAuthError else { + XCTFail("Should return cognitoAuthError") return } + + switch cognitoError { + case .userNotFound, .codeMismatch: + return + default: + XCTFail("Should be either `userNotFound` or `codeMismatch`") + } } } diff --git a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthResendSignUpCodeTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthResendSignUpCodeTests.swift index 05f8161581..f6965e61b5 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthResendSignUpCodeTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostApp/AuthIntegrationTests/SignUpTests/AuthResendSignUpCodeTests.swift @@ -21,8 +21,19 @@ class AuthResendSignUpCodeTests: AWSAuthBaseTest { /// func testUserNotFoundResendSignUpCode() async throws { do { - _ = try await Amplify.Auth.resendSignUpCode(for: "user-non-exists") - XCTFail("resendSignUpCode with non existing user should not return result") + let result = try await Amplify.Auth.resendSignUpCode(for: "user-non-exists") + + // App clients with "Prevent user existence errors" enabled will return a simulated result + // https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-managing-errors.html#cognito-user-pool-managing-errors-password-reset + // Gen2 configuration is enabled with Prevent User existence errors, while Gen1 backend is not. + if useGen2Configuration { + guard case .email = result.destination else { + XCTFail("Expected email detination in result, result: \(result)") + return + } + } else { + XCTFail("resendSignUpCode with non existing user should not return result, result returned: \(result)") + } } catch let error as AuthError { let underlyingError = error.underlyingError as? AWSCognitoAuthError switch underlyingError { From c55363cac63017ad9149d05f13d17fbcde42b962 Mon Sep 17 00:00:00 2001 From: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> Date: Tue, 7 May 2024 09:49:04 -0400 Subject: [PATCH 66/86] chore: Using codecov-action to upload coverage report (#3679) --- .codecov.yml | 4 +- .github/workflows/unit_test.yml | 1 + .github/workflows/upload_coverage_report.yml | 7 +- build-support/codecov.sh | 1746 ------------------ 4 files changed, 8 insertions(+), 1750 deletions(-) delete mode 100755 build-support/codecov.sh diff --git a/.codecov.yml b/.codecov.yml index 54e02fe70d..b36f658506 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,5 +1,7 @@ -coverage: +codecov: branch: main + +coverage: status: project: default: diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 1e733c2060..c8f3a74225 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -103,6 +103,7 @@ jobs: with: scheme: ${{ matrix.file.scheme }} flags: ${{ matrix.file.flags }} + secrets: inherit unit-test-pass-confirmation: runs-on: ubuntu-latest diff --git a/.github/workflows/upload_coverage_report.yml b/.github/workflows/upload_coverage_report.yml index 3ce54a81ce..c514a50f0e 100644 --- a/.github/workflows/upload_coverage_report.yml +++ b/.github/workflows/upload_coverage_report.yml @@ -32,6 +32,7 @@ jobs: path: ${{ github.workspace }} - name: Upload report to Codecov - shell: bash - run: | - build-support/codecov.sh -F '${{ inputs.flags }}' \ No newline at end of file + uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # v4.3.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: ${{ inputs.flags }} diff --git a/build-support/codecov.sh b/build-support/codecov.sh deleted file mode 100755 index 2b756ad83b..0000000000 --- a/build-support/codecov.sh +++ /dev/null @@ -1,1746 +0,0 @@ -#!/usr/bin/env bash - -# Apache License Version 2.0, January 2004 -# https://github.com/codecov/codecov-bash/blob/master/LICENSE - - -set -e +o pipefail - -VERSION="20200910-5a68dc2" - -codecov_flags=( ) -url="https://codecov.io" -env="$CODECOV_ENV" -service="" -token="" -search_in="" -# shellcheck disable=SC2153 -flags="$CODECOV_FLAGS" -exit_with=0 -curlargs="" -curlawsargs="" -dump="0" -clean="0" -curl_s="-s" -name="$CODECOV_NAME" -include_cov="" -exclude_cov="" -ddp="$HOME/Library/Developer/Xcode/DerivedData" -xp="" -files="" -save_to="" -cacert="$CODECOV_CA_BUNDLE" -gcov_ignore="-not -path './bower_components/**' -not -path './node_modules/**' -not -path './vendor/**'" -gcov_include="" - -ft_gcov="1" -ft_coveragepy="1" -ft_fix="1" -ft_search="1" -ft_s3="1" -ft_network="1" -ft_xcodellvm="1" -ft_xcodeplist="0" -ft_gcovout="1" -ft_html="0" - -_git_root=$(git rev-parse --show-toplevel 2>/dev/null || hg root 2>/dev/null || echo "$PWD") -git_root="$_git_root" -remote_addr="" -if [ "$git_root" = "$PWD" ]; -then - git_root="." -fi - -url_o="" -pr_o="" -build_o="" -commit_o="" -search_in_o="" -tag_o="" -branch_o="" -slug_o="" -prefix_o="" - -commit="$VCS_COMMIT_ID" -branch="$VCS_BRANCH_NAME" -pr="$VCS_PULL_REQUEST" -slug="$VCS_SLUG" -tag="$VCS_TAG" -build_url="$CI_BUILD_URL" -build="$CI_BUILD_ID" -job="$CI_JOB_ID" - -beta_xcode_partials="" - -proj_root="$git_root" -gcov_exe="gcov" -gcov_arg="" - -b="\033[0;36m" -g="\033[0;32m" -r="\033[0;31m" -e="\033[0;90m" -x="\033[0m" - -show_help() { -cat << EOF - - Codecov Bash $VERSION - - Global report uploading tool for Codecov - Documentation at https://docs.codecov.io/docs - Contribute at https://github.com/codecov/codecov-bash - - - -h Display this help and exit - -f FILE Target file(s) to upload - - -f "path/to/file" only upload this file - skips searching unless provided patterns below - - -f '!*.bar' ignore all files at pattern *.bar - -f '*.foo' include all files at pattern *.foo - Must use single quotes. - This is non-exclusive, use -s "*.foo" to match specific paths. - - -s DIR Directory to search for coverage reports. - Already searches project root and artifact folders. - -t TOKEN Set the private repository token - (option) set environment variable CODECOV_TOKEN=:uuid - - -t @/path/to/token_file - -t uuid - - -n NAME Custom defined name of the upload. Visible in Codecov UI - - -e ENV Specify environment variables to be included with this build - Also accepting environment variables: CODECOV_ENV=VAR,VAR2 - - -e VAR,VAR2 - - -X feature Toggle functionalities - - -X gcov Disable gcov - -X coveragepy Disable python coverage - -X fix Disable report fixing - -X search Disable searching for reports - -X xcode Disable xcode processing - -X network Disable uploading the file network - -X gcovout Disable gcov output - -X html Enable coverage for HTML files - - -N The commit SHA of the parent for which you are uploading coverage. If not present, - the parent will be determined using the API of your repository provider. - When using the repository provider's API, the parent is determined via finding - the closest ancestor to the commit. - - -R root dir Used when not in git/hg project to identify project root directory - -F flag Flag the upload to group coverage metrics - - -F unittests This upload is only unittests - -F integration This upload is only integration tests - -F ui,chrome This upload is Chrome - UI tests - - -c Move discovered coverage reports to the trash - -Z Exit with 1 if not successful. Default will Exit with 0 - - -- xcode -- - -D Custom Derived Data Path for Coverage.profdata and gcov processing - Default '~/Library/Developer/Xcode/DerivedData' - -J Specify packages to build coverage. Uploader will only build these packages. - This can significantly reduces time to build coverage reports. - - -J 'MyAppName' Will match "MyAppName" and "MyAppNameTests" - -J '^ExampleApp$' Will match only "ExampleApp" not "ExampleAppTests" - - -- gcov -- - -g GLOB Paths to ignore during gcov gathering - -G GLOB Paths to include during gcov gathering - -p dir Project root directory - Also used when preparing gcov - -k prefix Prefix filepaths to help resolve path fixing: https://github.com/codecov/support/issues/472 - -x gcovexe gcov executable to run. Defaults to 'gcov' - -a gcovargs extra arguments to pass to gcov - - -- Override CI Environment Variables -- - These variables are automatically detected by popular CI providers - - -B branch Specify the branch name - -C sha Specify the commit sha - -P pr Specify the pull request number - -b build Specify the build number - -T tag Specify the git tag - - -- Enterprise -- - -u URL Set the target url for Enterprise customers - Not required when retrieving the bash uploader from your CCE - (option) Set environment variable CODECOV_URL=https://my-hosted-codecov.com - -r SLUG owner/repo slug used instead of the private repo token in Enterprise - (option) set environment variable CODECOV_SLUG=:owner/:repo - (option) set in your codecov.yml "codecov.slug" - -S PATH File path to your cacert.pem file used to verify ssl with Codecov Enterprise (optional) - (option) Set environment variable: CODECOV_CA_BUNDLE="/path/to/ca.pem" - -U curlargs Extra curl arguments to communicate with Codecov. e.g., -U "--proxy http://http-proxy" - -A curlargs Extra curl arguments to communicate with AWS. - - -- Debugging -- - -d Don't upload, but dump upload file to stdout - -q PATH Write upload file to path - -K Remove color from the output - -v Verbose mode - -EOF -} - - -say() { - echo -e "$1" -} - - -urlencode() { - local string="${1}" - local strlen=${#string} - local encoded="" - local pos c o - - for (( pos=0 ; pos "$_proj_name.$_type.coverage.txt" \ - || say " ${r}x>${x} llvm-cov failed to produce results for $dest" - fi - done - done -} - - -# Credits to: https://gist.github.com/pkuczynski/8665367 -parse_yaml() { - local prefix=$2 - local s='[[:space:]]*' w='[a-zA-Z0-9_]*' - local fs - fs=$(echo @|tr @ '\034') - sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \ - -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$1" | - awk -F"$fs" '{ - indent = length($1)/2; - vname[indent] = $2; - for (i in vname) {if (i > indent) {delete vname[i]}} - if (length($3) > 0) { - vn=""; if (indent > 0) {vn=(vn)(vname[0])("_")} - printf("%s%s%s=\"%s\"\n", "'"$prefix"'",vn, $2, $3); - } - }' -} - - -if [ $# != 0 ]; -then - while getopts "a:A:b:B:cC:dD:e:f:F:g:G:hJ:k:Kn:p:P:q:r:R:s:S:t:T:u:U:vx:X:ZN:" o - do - codecov_flags+=( "$o" ) - case "$o" in - "N") - parent=$OPTARG - ;; - "a") - gcov_arg=$OPTARG - ;; - "A") - curlawsargs="$OPTARG" - ;; - "b") - build_o="$OPTARG" - ;; - "B") - branch_o="$OPTARG" - ;; - "c") - clean="1" - ;; - "C") - commit_o="$OPTARG" - ;; - "d") - dump="1" - ;; - "D") - ddp="$OPTARG" - ;; - "e") - env="$env,$OPTARG" - ;; - "f") - if [ "${OPTARG::1}" = "!" ]; - then - exclude_cov="$exclude_cov -not -path '${OPTARG:1}'" - - elif [[ "$OPTARG" = *"*"* ]]; - then - include_cov="$include_cov -or -name '$OPTARG'" - - else - ft_search=0 - if [ "$files" = "" ]; - then - files="$OPTARG" - else - files="$files -$OPTARG" - fi - fi - ;; - "F") - if [ "$flags" = "" ]; - then - flags="$OPTARG" - else - flags="$flags,$OPTARG" - fi - ;; - "g") - gcov_ignore="$gcov_ignore -not -path '$OPTARG'" - ;; - "G") - gcov_include="$gcov_include -path '$OPTARG'" - ;; - "h") - show_help - exit 0; - ;; - "J") - ft_xcodellvm="1" - ft_xcodeplist="0" - if [ "$xp" = "" ]; - then - xp="$OPTARG" - else - xp="$xp\|$OPTARG" - fi - ;; - "k") - prefix_o=$(echo "$OPTARG" | sed -e 's:^/*::' -e 's:/*$::') - ;; - "K") - b="" - g="" - r="" - e="" - x="" - ;; - "n") - name="$OPTARG" - ;; - "p") - proj_root="$OPTARG" - ;; - "P") - pr_o="$OPTARG" - ;; - "q") - save_to="$OPTARG" - ;; - "r") - slug_o="$OPTARG" - ;; - "R") - git_root="$OPTARG" - ;; - "s") - if [ "$search_in_o" = "" ]; - then - search_in_o="$OPTARG" - else - search_in_o="$search_in_o $OPTARG" - fi - ;; - "S") - # shellcheck disable=SC2089 - cacert="--cacert \"$OPTARG\"" - ;; - "t") - if [ "${OPTARG::1}" = "@" ]; - then - token=$(> "${OPTARG:1}" tr -d ' \n') - else - token="$OPTARG" - fi - ;; - "T") - tag_o="$OPTARG" - ;; - "u") - url_o=$(echo "$OPTARG" | sed -e 's/\/$//') - ;; - "U") - curlargs="$OPTARG" - ;; - "v") - set -x - curl_s="" - ;; - "x") - gcov_exe=$OPTARG - ;; - "X") - if [ "$OPTARG" = "gcov" ]; - then - ft_gcov="0" - elif [ "$OPTARG" = "coveragepy" ] || [ "$OPTARG" = "py" ]; - then - ft_coveragepy="0" - elif [ "$OPTARG" = "gcovout" ]; - then - ft_gcovout="0" - elif [ "$OPTARG" = "xcodellvm" ]; - then - ft_xcodellvm="1" - ft_xcodeplist="0" - elif [ "$OPTARG" = "fix" ] || [ "$OPTARG" = "fixes" ]; - then - ft_fix="0" - elif [ "$OPTARG" = "xcode" ]; - then - ft_xcodellvm="0" - ft_xcodeplist="0" - elif [ "$OPTARG" = "search" ]; - then - ft_search="0" - elif [ "$OPTARG" = "xcodepartials" ]; - then - beta_xcode_partials="-use-color" - elif [ "$OPTARG" = "network" ]; - then - ft_network="0" - elif [ "$OPTARG" = "s3" ]; - then - ft_s3="0" - elif [ "$OPTARG" = "html" ]; - then - ft_html="1" - fi - ;; - "Z") - exit_with=1 - ;; - *) - echo -e "${r}Unexpected flag not supported${x}" - ;; - esac - done -fi - -say " - _____ _ - / ____| | | -| | ___ __| | ___ ___ _____ __ -| | / _ \\ / _\` |/ _ \\/ __/ _ \\ \\ / / -| |___| (_) | (_| | __/ (_| (_) \\ V / - \\_____\\___/ \\__,_|\\___|\\___\\___/ \\_/ - Bash-$VERSION - -" - -search_in="$proj_root" - -#shellcheck disable=SC2154 -if [ "$JENKINS_URL" != "" ]; -then - say "$e==>$x Jenkins CI detected." - # https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project - # https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin#GitHubpullrequestbuilderplugin-EnvironmentVariables - service="jenkins" - - # shellcheck disable=SC2154 - if [ "$ghprbSourceBranch" != "" ]; - then - branch="$ghprbSourceBranch" - elif [ "$GIT_BRANCH" != "" ]; - then - branch="$GIT_BRANCH" - elif [ "$BRANCH_NAME" != "" ]; - then - branch="$BRANCH_NAME" - fi - - # shellcheck disable=SC2154 - if [ "$ghprbActualCommit" != "" ]; - then - commit="$ghprbActualCommit" - elif [ "$GIT_COMMIT" != "" ]; - then - commit="$GIT_COMMIT" - fi - - # shellcheck disable=SC2154 - if [ "$ghprbPullId" != "" ]; - then - pr="$ghprbPullId" - elif [ "$CHANGE_ID" != "" ]; - then - pr="$CHANGE_ID" - fi - - build="$BUILD_NUMBER" - # shellcheck disable=SC2153 - build_url=$(urlencode "$BUILD_URL") - -elif [ "$CI" = "true" ] && [ "$TRAVIS" = "true" ] && [ "$SHIPPABLE" != "true" ]; -then - say "$e==>$x Travis CI detected." - # https://docs.travis-ci.com/user/environment-variables/ - service="travis" - commit="${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT}" - build="$TRAVIS_JOB_NUMBER" - pr="$TRAVIS_PULL_REQUEST" - job="$TRAVIS_JOB_ID" - slug="$TRAVIS_REPO_SLUG" - env="$env,TRAVIS_OS_NAME" - tag="$TRAVIS_TAG" - if [ "$TRAVIS_BRANCH" != "$TRAVIS_TAG" ]; - then - branch="$TRAVIS_BRANCH" - fi - - language=$(compgen -A variable | grep "^TRAVIS_.*_VERSION$" | head -1) - if [ "$language" != "" ]; - then - env="$env,${!language}" - fi - -elif [ "$CODEBUILD_CI" = "true" ]; -then - say "$e==>$x AWS Codebuild detected." - # https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html - service="codebuild" - commit="$CODEBUILD_RESOLVED_SOURCE_VERSION" - build="$CODEBUILD_BUILD_ID" - branch="$(echo "$CODEBUILD_WEBHOOK_HEAD_REF" | sed 's/^refs\/heads\///')" - if [ "${CODEBUILD_SOURCE_VERSION/pr}" = "$CODEBUILD_SOURCE_VERSION" ] ; then - pr="false" - else - pr="$(echo "$CODEBUILD_SOURCE_VERSION" | sed 's/^pr\///')" - fi - job="$CODEBUILD_BUILD_ID" - slug="$(echo "$CODEBUILD_SOURCE_REPO_URL" | sed 's/^.*:\/\/[^\/]*\///' | sed 's/\.git$//')" - -elif [ "$DOCKER_REPO" != "" ]; -then - say "$e==>$x Docker detected." - # https://docs.docker.com/docker-cloud/builds/advanced/ - service="docker" - branch="$SOURCE_BRANCH" - commit="$SOURCE_COMMIT" - slug="$DOCKER_REPO" - tag="$CACHE_TAG" - env="$env,IMAGE_NAME" - -elif [ "$CI" = "true" ] && [ "$CI_NAME" = "codeship" ]; -then - say "$e==>$x Codeship CI detected." - # https://www.codeship.io/documentation/continuous-integration/set-environment-variables/ - service="codeship" - branch="$CI_BRANCH" - build="$CI_BUILD_NUMBER" - build_url=$(urlencode "$CI_BUILD_URL") - commit="$CI_COMMIT_ID" - -elif [ -n "$CF_BUILD_URL" ] && [ -n "$CF_BUILD_ID" ]; -then - say "$e==>$x Codefresh CI detected." - # https://docs.codefresh.io/v1.0/docs/variables - service="codefresh" - branch="$CF_BRANCH" - build="$CF_BUILD_ID" - build_url=$(urlencode "$CF_BUILD_URL") - commit="$CF_REVISION" - -elif [ "$TEAMCITY_VERSION" != "" ]; -then - say "$e==>$x TeamCity CI detected." - # https://confluence.jetbrains.com/display/TCD8/Predefined+Build+Parameters - # https://confluence.jetbrains.com/plugins/servlet/mobile#content/view/74847298 - if [ "$TEAMCITY_BUILD_BRANCH" = '' ]; - then - echo " Teamcity does not automatically make build parameters available as environment variables." - echo " Add the following environment parameters to the build configuration" - echo " env.TEAMCITY_BUILD_BRANCH = %teamcity.build.branch%" - echo " env.TEAMCITY_BUILD_ID = %teamcity.build.id%" - echo " env.TEAMCITY_BUILD_URL = %teamcity.serverUrl%/viewLog.html?buildId=%teamcity.build.id%" - echo " env.TEAMCITY_BUILD_COMMIT = %system.build.vcs.number%" - echo " env.TEAMCITY_BUILD_REPOSITORY = %vcsroot..url%" - fi - service="teamcity" - branch="$TEAMCITY_BUILD_BRANCH" - build="$TEAMCITY_BUILD_ID" - build_url=$(urlencode "$TEAMCITY_BUILD_URL") - if [ "$TEAMCITY_BUILD_COMMIT" != "" ]; - then - commit="$TEAMCITY_BUILD_COMMIT" - else - commit="$BUILD_VCS_NUMBER" - fi - remote_addr="$TEAMCITY_BUILD_REPOSITORY" - -elif [ "$CI" = "true" ] && [ "$CIRCLECI" = "true" ]; -then - say "$e==>$x Circle CI detected." - # https://circleci.com/docs/environment-variables - service="circleci" - branch="$CIRCLE_BRANCH" - build="$CIRCLE_BUILD_NUM" - job="$CIRCLE_NODE_INDEX" - if [ "$CIRCLE_PROJECT_REPONAME" != "" ]; - then - slug="$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME" - else - # git@github.com:owner/repo.git - slug="${CIRCLE_REPOSITORY_URL##*:}" - # owner/repo.git - slug="${slug%%.git}" - fi - pr="$CIRCLE_PR_NUMBER" - commit="$CIRCLE_SHA1" - search_in="$search_in $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS" - -elif [ "$BUDDYBUILD_BRANCH" != "" ]; -then - say "$e==>$x buddybuild detected" - # http://docs.buddybuild.com/v6/docs/custom-prebuild-and-postbuild-steps - service="buddybuild" - branch="$BUDDYBUILD_BRANCH" - build="$BUDDYBUILD_BUILD_NUMBER" - build_url="https://dashboard.buddybuild.com/public/apps/$BUDDYBUILD_APP_ID/build/$BUDDYBUILD_BUILD_ID" - # BUDDYBUILD_TRIGGERED_BY - if [ "$ddp" = "$HOME/Library/Developer/Xcode/DerivedData" ]; - then - ddp="/private/tmp/sandbox/${BUDDYBUILD_APP_ID}/bbtest" - fi - -elif [ "${bamboo_planRepository_revision}" != "" ]; -then - say "$e==>$x Bamboo detected" - # https://confluence.atlassian.com/bamboo/bamboo-variables-289277087.html#Bamboovariables-Build-specificvariables - service="bamboo" - commit="${bamboo_planRepository_revision}" - # shellcheck disable=SC2154 - branch="${bamboo_planRepository_branch}" - # shellcheck disable=SC2154 - build="${bamboo_buildNumber}" - # shellcheck disable=SC2154 - build_url="${bamboo_buildResultsUrl}" - # shellcheck disable=SC2154 - remote_addr="${bamboo_planRepository_repositoryUrl}" - -elif [ "$CI" = "true" ] && [ "$BITRISE_IO" = "true" ]; -then - # http://devcenter.bitrise.io/faq/available-environment-variables/ - say "$e==>$x Bitrise CI detected." - service="bitrise" - branch="$BITRISE_GIT_BRANCH" - build="$BITRISE_BUILD_NUMBER" - build_url=$(urlencode "$BITRISE_BUILD_URL") - pr="$BITRISE_PULL_REQUEST" - if [ "$GIT_CLONE_COMMIT_HASH" != "" ]; - then - commit="$GIT_CLONE_COMMIT_HASH" - fi - -elif [ "$CI" = "true" ] && [ "$SEMAPHORE" = "true" ]; -then - say "$e==>$x Semaphore CI detected." -# https://docs.semaphoreci.com/ci-cd-environment/environment-variables/#semaphore-related - service="semaphore" - branch="$SEMAPHORE_GIT_BRANCH" - build="$SEMAPHORE_WORKFLOW_NUMBER" - job="$SEMAPHORE_JOB_ID" - pr="$PULL_REQUEST_NUMBER" - slug="$SEMAPHORE_REPO_SLUG" - commit="$REVISION" - env="$env,SEMAPHORE_TRIGGER_SOURCE" - -elif [ "$CI" = "true" ] && [ "$BUILDKITE" = "true" ]; -then - say "$e==>$x Buildkite CI detected." - # https://buildkite.com/docs/guides/environment-variables - service="buildkite" - branch="$BUILDKITE_BRANCH" - build="$BUILDKITE_BUILD_NUMBER" - job="$BUILDKITE_JOB_ID" - build_url=$(urlencode "$BUILDKITE_BUILD_URL") - slug="$BUILDKITE_PROJECT_SLUG" - commit="$BUILDKITE_COMMIT" - if [[ "$BUILDKITE_PULL_REQUEST" != "false" ]]; then - pr="$BUILDKITE_PULL_REQUEST" - fi - tag="$BUILDKITE_TAG" - -elif [ "$CI" = "drone" ] || [ "$DRONE" = "true" ]; -then - say "$e==>$x Drone CI detected." - # http://docs.drone.io/env.html - # drone commits are not full shas - service="drone.io" - branch="$DRONE_BRANCH" - build="$DRONE_BUILD_NUMBER" - build_url=$(urlencode "${DRONE_BUILD_LINK}") - pr="$DRONE_PULL_REQUEST" - job="$DRONE_JOB_NUMBER" - tag="$DRONE_TAG" - -elif [ "$HEROKU_TEST_RUN_BRANCH" != "" ]; -then - say "$e==>$x Heroku CI detected." - # https://devcenter.heroku.com/articles/heroku-ci#environment-variables - service="heroku" - branch="$HEROKU_TEST_RUN_BRANCH" - build="$HEROKU_TEST_RUN_ID" - -elif [[ "$CI" = "true" || "$CI" = "True" ]] && [[ "$APPVEYOR" = "true" || "$APPVEYOR" = "True" ]]; -then - say "$e==>$x Appveyor CI detected." - # http://www.appveyor.com/docs/environment-variables - service="appveyor" - branch="$APPVEYOR_REPO_BRANCH" - build=$(urlencode "$APPVEYOR_JOB_ID") - pr="$APPVEYOR_PULL_REQUEST_NUMBER" - job="$APPVEYOR_ACCOUNT_NAME%2F$APPVEYOR_PROJECT_SLUG%2F$APPVEYOR_BUILD_VERSION" - slug="$APPVEYOR_REPO_NAME" - commit="$APPVEYOR_REPO_COMMIT" - build_url=$(urlencode "${APPVEYOR_URL}/project/${APPVEYOR_REPO_NAME}/builds/$APPVEYOR_BUILD_ID/job/${APPVEYOR_JOB_ID}") - -elif [ "$CI" = "true" ] && [ "$WERCKER_GIT_BRANCH" != "" ]; -then - say "$e==>$x Wercker CI detected." - # http://devcenter.wercker.com/articles/steps/variables.html - service="wercker" - branch="$WERCKER_GIT_BRANCH" - build="$WERCKER_MAIN_PIPELINE_STARTED" - slug="$WERCKER_GIT_OWNER/$WERCKER_GIT_REPOSITORY" - commit="$WERCKER_GIT_COMMIT" - -elif [ "$CI" = "true" ] && [ "$MAGNUM" = "true" ]; -then - say "$e==>$x Magnum CI detected." - # https://magnum-ci.com/docs/environment - service="magnum" - branch="$CI_BRANCH" - build="$CI_BUILD_NUMBER" - commit="$CI_COMMIT" - -elif [ "$SHIPPABLE" = "true" ]; -then - say "$e==>$x Shippable CI detected." - # http://docs.shippable.com/ci_configure/ - service="shippable" - # shellcheck disable=SC2153 - branch=$([ "$HEAD_BRANCH" != "" ] && echo "$HEAD_BRANCH" || echo "$BRANCH") - build="$BUILD_NUMBER" - build_url=$(urlencode "$BUILD_URL") - pr="$PULL_REQUEST" - slug="$REPO_FULL_NAME" - # shellcheck disable=SC2153 - commit="$COMMIT" - -elif [ "$TDDIUM" = "true" ]; -then - say "Solano CI detected." - # http://docs.solanolabs.com/Setup/tddium-set-environment-variables/ - service="solano" - commit="$TDDIUM_CURRENT_COMMIT" - branch="$TDDIUM_CURRENT_BRANCH" - build="$TDDIUM_TID" - pr="$TDDIUM_PR_ID" - -elif [ "$GREENHOUSE" = "true" ]; -then - say "$e==>$x Greenhouse CI detected." - # http://docs.greenhouseci.com/docs/environment-variables-files - service="greenhouse" - branch="$GREENHOUSE_BRANCH" - build="$GREENHOUSE_BUILD_NUMBER" - build_url=$(urlencode "$GREENHOUSE_BUILD_URL") - pr="$GREENHOUSE_PULL_REQUEST" - commit="$GREENHOUSE_COMMIT" - search_in="$search_in $GREENHOUSE_EXPORT_DIR" - -elif [ "$GITLAB_CI" != "" ]; -then - say "$e==>$x GitLab CI detected." - # http://doc.gitlab.com/ce/ci/variables/README.html - service="gitlab" - branch="${CI_BUILD_REF_NAME:-$CI_COMMIT_REF_NAME}" - build="${CI_BUILD_ID:-$CI_JOB_ID}" - remote_addr="${CI_BUILD_REPO:-$CI_REPOSITORY_URL}" - commit="${CI_BUILD_REF:-$CI_COMMIT_SHA}" - slug="${CI_PROJECT_PATH}" - -elif [ "$GITHUB_ACTIONS" != "" ]; -then - say "$e==>$x GitHub Actions detected." - - # https://github.com/features/actions - service="github-actions" - - # https://help.github.com/en/articles/virtual-environments-for-github-actions#environment-variables - branch="${GITHUB_REF#refs/heads/}" - if [ "$GITHUB_HEAD_REF" != "" ]; - then - # PR refs are in the format: refs/pull/7/merge - pr="${GITHUB_REF#refs/pull/}" - pr="${pr%/merge}" - branch="${GITHUB_HEAD_REF}" - fi - commit="${GITHUB_SHA}" - slug="${GITHUB_REPOSITORY}" - build="${GITHUB_RUN_ID}" - build_url=$(urlencode "http://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}") - -elif [ "$SYSTEM_TEAMFOUNDATIONSERVERURI" != "" ]; -then - say "$e==>$x Azure Pipelines detected." - # https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=vsts - # https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&viewFallbackFrom=vsts&tabs=yaml - service="azure_pipelines" - commit="$BUILD_SOURCEVERSION" - build="$BUILD_BUILDNUMBER" - if [ -z "$SYSTEM_PULLREQUEST_PULLREQUESTNUMBER" ]; - then - pr="$SYSTEM_PULLREQUEST_PULLREQUESTID" - else - pr="$SYSTEM_PULLREQUEST_PULLREQUESTNUMBER" - fi - project="${SYSTEM_TEAMPROJECT}" - server_uri="${SYSTEM_TEAMFOUNDATIONSERVERURI}" - job="${BUILD_BUILDID}" - branch="$BUILD_SOURCEBRANCHNAME" - build_url=$(urlencode "${SYSTEM_TEAMFOUNDATIONSERVERURI}${SYSTEM_TEAMPROJECT}/_build/results?buildId=${BUILD_BUILDID}") -elif [ "$CI" = "true" ] && [ "$BITBUCKET_BUILD_NUMBER" != "" ]; -then - say "$e==>$x Bitbucket detected." - # https://confluence.atlassian.com/bitbucket/variables-in-pipelines-794502608.html - service="bitbucket" - branch="$BITBUCKET_BRANCH" - build="$BITBUCKET_BUILD_NUMBER" - slug="$BITBUCKET_REPO_OWNER/$BITBUCKET_REPO_SLUG" - job="$BITBUCKET_BUILD_NUMBER" - pr="$BITBUCKET_PR_ID" - commit="$BITBUCKET_COMMIT" - # See https://jira.atlassian.com/browse/BCLOUD-19393 - if [ "${#commit}" = 12 ]; - then - commit=$(git rev-parse "$BITBUCKET_COMMIT") - fi -elif [ "$CI" = "true" ] && [ "$BUDDY" = "true" ]; -then - say "$e==>$x Buddy CI detected." - # https://buddy.works/docs/pipelines/environment-variables - service="buddy" - branch="$BUDDY_EXECUTION_BRANCH" - build="$BUDDY_EXECUTION_ID" - build_url=$(urlencode "$BUDDY_EXECUTION_URL") - commit="$BUDDY_EXECUTION_REVISION" - pr="$BUDDY_EXECUTION_PULL_REQUEST_NO" - tag="$BUDDY_EXECUTION_TAG" - slug="$BUDDY_REPO_SLUG" - -elif [ "$CIRRUS_CI" != "" ]; -then - say "$e==>$x Cirrus CI detected." - # https://cirrus-ci.org/guide/writing-tasks/#environment-variables - service="cirrus-ci" - slug="$CIRRUS_REPO_FULL_NAME" - branch="$CIRRUS_BRANCH" - pr="$CIRRUS_PR" - commit="$CIRRUS_CHANGE_IN_REPO" - build="$CIRRUS_TASK_ID" - job="$CIRRUS_TASK_NAME" - -else - say "${r}x>${x} No CI provider detected." - say " Testing inside Docker? ${b}http://docs.codecov.io/docs/testing-with-docker${x}" - say " Testing with Tox? ${b}https://docs.codecov.io/docs/python#section-testing-with-tox${x}" - -fi - -say " ${e}project root:${x} $git_root" - -# find branch, commit, repo from git command -if [ "$GIT_BRANCH" != "" ]; -then - branch="$GIT_BRANCH" - -elif [ "$branch" = "" ]; -then - branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || hg branch 2>/dev/null || echo "") - if [ "$branch" = "HEAD" ]; - then - branch="" - fi -fi - -if [ "$commit_o" = "" ]; -then - # merge commit -> actual commit - mc= - if [ -n "$pr" ] && [ "$pr" != false ]; - then - mc=$(git show --no-patch --format="%P" 2>/dev/null || echo "") - fi - if [[ "$mc" =~ ^[a-z0-9]{40}[[:space:]][a-z0-9]{40}$ ]]; - then - say " Fixing merge commit SHA" - commit=$(echo "$mc" | cut -d' ' -f2) - elif [ "$GIT_COMMIT" != "" ]; - then - commit="$GIT_COMMIT" - elif [ "$commit" = "" ]; - then - commit=$(git log -1 --format="%H" 2>/dev/null || hg id -i --debug 2>/dev/null | tr -d '+' || echo "") - fi -else - commit="$commit_o" -fi - -if [ "$CODECOV_TOKEN" != "" ] && [ "$token" = "" ]; -then - say "${e}-->${x} token set from env" - token="$CODECOV_TOKEN" -fi - -if [ "$CODECOV_URL" != "" ] && [ "$url_o" = "" ]; -then - say "${e}-->${x} url set from env" - url_o=$(echo "$CODECOV_URL" | sed -e 's/\/$//') -fi - -if [ "$CODECOV_SLUG" != "" ]; -then - say "${e}-->${x} slug set from env" - slug_o="$CODECOV_SLUG" - -elif [ "$slug" = "" ]; -then - if [ "$remote_addr" = "" ]; - then - remote_addr=$(git config --get remote.origin.url || hg paths default || echo '') - fi - if [ "$remote_addr" != "" ]; - then - if echo "$remote_addr" | grep -q "//"; then - # https - slug=$(echo "$remote_addr" | cut -d / -f 4,5 | sed -e 's/\.git$//') - else - # ssh - slug=$(echo "$remote_addr" | cut -d : -f 2 | sed -e 's/\.git$//') - fi - fi - if [ "$slug" = "/" ]; - then - slug="" - fi -fi - -yaml=$(cd "$git_root" && \ - git ls-files "*codecov.yml" "*codecov.yaml" 2>/dev/null \ - || hg locate "*codecov.yml" "*codecov.yaml" 2>/dev/null \ - || cd "$proj_root" && find . -maxdepth 1 -type f -name '*codecov.y*ml' 2>/dev/null \ - || echo '') -yaml=$(echo "$yaml" | head -1) - -if [ "$yaml" != "" ]; -then - say " ${e}Yaml found at:${x} $yaml" - if [[ "$yaml" != /* ]]; then - # relative path for yaml file given, assume relative to the repo root - yaml="$git_root/$yaml" - fi - config=$(parse_yaml "$yaml" || echo '') - - # TODO validate the yaml here - - if [ "$(echo "$config" | grep 'codecov_token="')" != "" ] && [ "$token" = "" ]; - then - say "${e}-->${x} token set from yaml" - token="$(echo "$config" | grep 'codecov_token="' | sed -e 's/codecov_token="//' | sed -e 's/"\.*//')" - fi - - if [ "$(echo "$config" | grep 'codecov_url="')" != "" ] && [ "$url_o" = "" ]; - then - say "${e}-->${x} url set from yaml" - url_o="$(echo "$config" | grep 'codecov_url="' | sed -e 's/codecov_url="//' | sed -e 's/"\.*//')" - fi - - if [ "$(echo "$config" | grep 'codecov_slug="')" != "" ] && [ "$slug_o" = "" ]; - then - say "${e}-->${x} slug set from yaml" - slug_o="$(echo "$config" | grep 'codecov_slug="' | sed -e 's/codecov_slug="//' | sed -e 's/"\.*//')" - fi -else - say " ${g}Yaml not found, that's ok! Learn more at${x} ${b}http://docs.codecov.io/docs/codecov-yaml${x}" -fi - -if [ "$branch_o" != "" ]; -then - branch=$(urlencode "$branch_o") -else - branch=$(urlencode "$branch") -fi - -if [ "$slug_o" = "" ]; -then - urlencoded_slug=$(urlencode "$slug") -else - urlencoded_slug=$(urlencode "$slug_o") -fi - -query="branch=$branch\ - &commit=$commit\ - &build=$([ "$build_o" = "" ] && echo "$build" || echo "$build_o")\ - &build_url=$build_url\ - &name=$(urlencode "$name")\ - &tag=$([ "$tag_o" = "" ] && echo "$tag" || echo "$tag_o")\ - &slug=$urlencoded_slug\ - &service=$service\ - &flags=$flags\ - &pr=$([ "$pr_o" = "" ] && echo "${pr##\#}" || echo "${pr_o##\#}")\ - &job=$job\ - &cmd_args=$(IFS=,; echo "${codecov_flags[*]}")" - -if [ -n "$project" ] && [ -n "$server_uri" ]; -then - query=$(echo "$query&project=$project&server_uri=$server_uri" | tr -d ' ') -fi - -if [ "$parent" != "" ]; -then - query=$(echo "parent=$parent&$query" | tr -d ' ') -fi - -if [ "$ft_search" = "1" ]; -then - # detect bower comoponents location - bower_components="bower_components" - bower_rc=$(cd "$git_root" && cat .bowerrc 2>/dev/null || echo "") - if [ "$bower_rc" != "" ]; - then - bower_components=$(echo "$bower_rc" | tr -d '\n' | grep '"directory"' | cut -d'"' -f4 | sed -e 's/\/$//') - if [ "$bower_components" = "" ]; - then - bower_components="bower_components" - fi - fi - - # Swift Coverage - if [ "$ft_xcodellvm" = "1" ] && [ -d "$ddp" ]; - then - say "${e}==>${x} Processing Xcode reports via llvm-cov" - say " DerivedData folder: $ddp" - profdata_files=$(find "$ddp" -name '*.profdata' 2>/dev/null || echo '') - if [ "$profdata_files" != "" ]; - then - # xcode via profdata - if [ "$xp" = "" ]; - then - # xp=$(xcodebuild -showBuildSettings 2>/dev/null | grep -i "^\s*PRODUCT_NAME" | sed -e 's/.*= \(.*\)/\1/') - # say " ${e}->${x} Speed up Xcode processing by adding ${e}-J '$xp'${x}" - say " ${g}hint${x} Speed up Swift processing by using use ${g}-J 'AppName'${x} (regexp accepted)" - say " ${g}hint${x} This will remove Pods/ from your report. Also ${b}https://docs.codecov.io/docs/ignoring-paths${x}" - fi - while read -r profdata; - do - if [ "$profdata" != "" ]; - then - swiftcov "$profdata" "$xp" - fi - done <<< "$profdata_files" - else - say " ${e}->${x} No Swift coverage found" - fi - - # Obj-C Gcov Coverage - if [ "$ft_gcov" = "1" ]; - then - say " ${e}->${x} Running $gcov_exe for Obj-C" - if [ "$ft_gcovout" = "0" ]; - then - # suppress gcov output - bash -c "find $ddp -type f -name '*.gcda' $gcov_include $gcov_ignore -exec $gcov_exe -p $gcov_arg {} +" >/dev/null 2>&1 || true - else - bash -c "find $ddp -type f -name '*.gcda' $gcov_include $gcov_ignore -exec $gcov_exe -p $gcov_arg {} +" || true - fi - fi - fi - - if [ "$ft_xcodeplist" = "1" ] && [ -d "$ddp" ]; - then - say "${e}==>${x} Processing Xcode plists" - plists_files=$(find "$ddp" -name '*.xccoverage' 2>/dev/null || echo '') - if [ "$plists_files" != "" ]; - then - while read -r plist; - do - if [ "$plist" != "" ]; - then - say " ${g}Found${x} plist file at $plist" - plutil -convert xml1 -o "$(basename "$plist").plist" -- "$plist" - fi - done <<< "$plists_files" - fi - fi - - # Gcov Coverage - if [ "$ft_gcov" = "1" ]; - then - say "${e}==>${x} Running $gcov_exe in $proj_root ${e}(disable via -X gcov)${x}" - if [ "$ft_gcovout" = "0" ]; - then - # suppress gcov output - bash -c "find $proj_root -type f -name '*.gcno' $gcov_include $gcov_ignore -exec $gcov_exe -pb $gcov_arg {} +" >/dev/null 2>&1 || true - else - bash -c "find $proj_root -type f -name '*.gcno' $gcov_include $gcov_ignore -exec $gcov_exe -pb $gcov_arg {} +" || true - fi - else - say "${e}==>${x} gcov disabled" - fi - - # Python Coverage - if [ "$ft_coveragepy" = "1" ]; - then - if [ ! -f coverage.xml ]; - then - if command -v coverage >/dev/null 2>&1; - then - say "${e}==>${x} Python coveragepy exists ${e}disable via -X coveragepy${x}" - - dotcoverage=$(find "$git_root" -name '.coverage' -or -name '.coverage.*' | head -1 || echo '') - if [ "$dotcoverage" != "" ]; - then - cd "$(dirname "$dotcoverage")" - if [ ! -f .coverage ]; - then - say " ${e}->${x} Running coverage combine" - coverage combine -a - fi - say " ${e}->${x} Running coverage xml" - if [ "$(coverage xml -i)" != "No data to report." ]; - then - files="$files -$PWD/coverage.xml" - else - say " ${r}No data to report.${x}" - fi - cd "$proj_root" - else - say " ${r}No .coverage file found.${x}" - fi - else - say "${e}==>${x} Python coveragepy not found" - fi - fi - else - say "${e}==>${x} Python coveragepy disabled" - fi - - if [ "$search_in_o" != "" ]; - then - # location override - search_in="$search_in_o" - fi - - say "$e==>$x Searching for coverage reports in:" - for _path in $search_in - do - say " ${g}+${x} $_path" - done - - patterns="find $search_in \( \ - -name vendor \ - -or -name htmlcov \ - -or -name virtualenv \ - -or -name js/generated/coverage \ - -or -name .virtualenv \ - -or -name virtualenvs \ - -or -name .virtualenvs \ - -or -name .env \ - -or -name .envs \ - -or -name env \ - -or -name .yarn-cache \ - -or -name envs \ - -or -name .venv \ - -or -name .venvs \ - -or -name venv \ - -or -name venvs \ - -or -name .git \ - -or -name .hg \ - -or -name .tox \ - -or -name __pycache__ \ - -or -name '.egg-info*' \ - -or -name '$bower_components' \ - -or -name node_modules \ - -or -name 'conftest_*.c.gcov' \ - \) -prune -or \ - -type f \( -name '*coverage*.*' \ - -or -name 'nosetests.xml' \ - -or -name 'jacoco*.xml' \ - -or -name 'clover.xml' \ - -or -name 'report.xml' \ - -or -name '*.codecov.*' \ - -or -name 'codecov.*' \ - -or -name 'cobertura.xml' \ - -or -name 'excoveralls.json' \ - -or -name 'luacov.report.out' \ - -or -name 'coverage-final.json' \ - -or -name 'naxsi.info' \ - -or -name 'lcov.info' \ - -or -name 'lcov.dat' \ - -or -name '*.lcov' \ - -or -name '*.clover' \ - -or -name 'cover.out' \ - -or -name 'gcov.info' \ - -or -name '*.gcov' \ - -or -name '*.lst' \ - $include_cov \) \ - $exclude_cov \ - -not -name '*.profdata' \ - -not -name 'coverage-summary.json' \ - -not -name 'phpunit-code-coverage.xml' \ - -not -name '*/classycle/report.xml' \ - -not -name 'remapInstanbul.coverage*.json' \ - -not -name 'phpunit-coverage.xml' \ - -not -name '*codecov.yml' \ - -not -name '*.serialized' \ - -not -name '.coverage*' \ - -not -name '.*coveragerc' \ - -not -name '*.sh' \ - -not -name '*.bat' \ - -not -name '*.ps1' \ - -not -name '*.env' \ - -not -name '*.cmake' \ - -not -name '*.dox' \ - -not -name '*.ec' \ - -not -name '*.rst' \ - -not -name '*.h' \ - -not -name '*.scss' \ - -not -name '*.o' \ - -not -name '*.proto' \ - -not -name '*.sbt' \ - -not -name '*.xcoverage.*' \ - -not -name '*.gz' \ - -not -name '*.conf' \ - -not -name '*.p12' \ - -not -name '*.csv' \ - -not -name '*.rsp' \ - -not -name '*.m4' \ - -not -name '*.pem' \ - -not -name '*~' \ - -not -name '*.exe' \ - -not -name '*.am' \ - -not -name '*.template' \ - -not -name '*.cp' \ - -not -name '*.bw' \ - -not -name '*.crt' \ - -not -name '*.log' \ - -not -name '*.cmake' \ - -not -name '*.pth' \ - -not -name '*.in' \ - -not -name '*.jar*' \ - -not -name '*.pom*' \ - -not -name '*.png' \ - -not -name '*.jpg' \ - -not -name '*.sql' \ - -not -name '*.jpeg' \ - -not -name '*.svg' \ - -not -name '*.gif' \ - -not -name '*.csv' \ - -not -name '*.snapshot' \ - -not -name '*.mak*' \ - -not -name '*.bash' \ - -not -name '*.data' \ - -not -name '*.py' \ - -not -name '*.class' \ - -not -name '*.xcconfig' \ - -not -name '*.ec' \ - -not -name '*.coverage' \ - -not -name '*.pyc' \ - -not -name '*.cfg' \ - -not -name '*.egg' \ - -not -name '*.ru' \ - -not -name '*.css' \ - -not -name '*.less' \ - -not -name '*.pyo' \ - -not -name '*.whl' \ - -not -name '*.html' \ - -not -name '*.ftl' \ - -not -name '*.erb' \ - -not -name '*.rb' \ - -not -name '*.js' \ - -not -name '*.jade' \ - -not -name '*.db' \ - -not -name '*.md' \ - -not -name '*.cpp' \ - -not -name '*.gradle' \ - -not -name '*.tar.tz' \ - -not -name '*.scss' \ - -not -name 'include.lst' \ - -not -name 'fullLocaleNames.lst' \ - -not -name 'inputFiles.lst' \ - -not -name 'createdFiles.lst' \ - -not -name 'scoverage.measurements.*' \ - -not -name 'test_*_coverage.txt' \ - -not -name 'testrunner-coverage*' \ - -print 2>/dev/null" - files=$(eval "$patterns" || echo '') - -elif [ "$include_cov" != "" ]; -then - files=$(eval "find $search_in -type f \( ${include_cov:5} \)$exclude_cov 2>/dev/null" || echo '') -fi - -num_of_files=$(echo "$files" | wc -l | tr -d ' ') -if [ "$num_of_files" != '' ] && [ "$files" != '' ]; -then - say " ${e}->${x} Found $num_of_files reports" -fi - -# no files found -if [ "$files" = "" ]; -then - say "${r}-->${x} No coverage report found." - say " Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}" - exit ${exit_with}; -fi - -if [ "$ft_network" == "1" ]; -then - say "${e}==>${x} Detecting git/mercurial file structure" - network=$(cd "$git_root" && git ls-files 2>/dev/null || hg locate 2>/dev/null || echo "") - if [ "$network" = "" ]; - then - network=$(find "$git_root" \( \ - -name virtualenv \ - -name .virtualenv \ - -name virtualenvs \ - -name .virtualenvs \ - -name '*.png' \ - -name '*.gif' \ - -name '*.jpg' \ - -name '*.jpeg' \ - -name '*.md' \ - -name .env \ - -name .envs \ - -name env \ - -name envs \ - -name .venv \ - -name .venvs \ - -name venv \ - -name venvs \ - -name .git \ - -name .egg-info \ - -name shunit2-2.1.6 \ - -name vendor \ - -name __pycache__ \ - -name node_modules \ - -path "*/$bower_components/*" \ - -path '*/target/delombok/*' \ - -path '*/build/lib/*' \ - -path '*/js/generated/coverage/*' \ - \) -prune -or \ - -type f -print 2>/dev/null || echo '') - fi - - if [ "$prefix_o" != "" ]; - then - network=$(echo "$network" | awk "{print \"$prefix_o/\"\$0}") - fi -fi - -upload_file=$(mktemp /tmp/codecov.XXXXXX) -adjustments_file=$(mktemp /tmp/codecov.adjustments.XXXXXX) - -cleanup() { - rm -f "$upload_file" "$adjustments_file" "$upload_file.gz" -} - -trap cleanup INT ABRT TERM - -if [ "$env" != "" ]; -then - inc_env="" - say "${e}==>${x} Appending build variables" - for varname in $(echo "$env" | tr ',' ' ') - do - if [ "$varname" != "" ]; - then - say " ${g}+${x} $varname" - inc_env="${inc_env}${varname}=$(eval echo "\$${varname}") -" - fi - done - -echo "$inc_env<<<<<< ENV" >> "$upload_file" -fi - -# Append git file list -# write discovered yaml location -echo "$yaml" >> "$upload_file" -if [ "$ft_network" == "1" ]; -then - i="woff|eot|otf" # fonts - i="$i|gif|png|jpg|jpeg|psd" # images - i="$i|ptt|pptx|numbers|pages|md|txt|xlsx|docx|doc|pdf|csv" # docs - i="$i|yml|yaml|.gitignore" # supporting docs - - if [ "$ft_html" != "1" ]; - then - i="$i|html" - fi - - echo "$network" | grep -vwE "($i)$" >> "$upload_file" -fi -echo "<<<<<< network" >> "$upload_file" - -fr=0 -say "${e}==>${x} Reading reports" -while IFS='' read -r file; -do - # read the coverage file - if [ "$(echo "$file" | tr -d ' ')" != '' ]; - then - if [ -f "$file" ]; - then - report_len=$(wc -c < "$file") - if [ "$report_len" -ne 0 ]; - then - say " ${g}+${x} $file ${e}bytes=$(echo "$report_len" | tr -d ' ')${x}" - # append to to upload - _filename=$(basename "$file") - if [ "${_filename##*.}" = 'gcov' ]; - then - { - echo "# path=$(echo "$file.reduced" | sed "s|^$git_root/||")"; - # get file name - head -1 "$file"; - } >> "$upload_file" - # 1. remove source code - # 2. remove ending bracket lines - # 3. remove whitespace - # 4. remove contextual lines - # 5. remove function names - awk -F': *' '{print $1":"$2":"}' "$file" \ - | sed '\/: *} *$/d' \ - | sed 's/^ *//' \ - | sed '/^-/d' \ - | sed 's/^function.*/func/' >> "$upload_file" - else - { - echo "# path=${file//^$git_root/||}"; - cat "$file"; - } >> "$upload_file" - fi - echo "<<<<<< EOF" >> "$upload_file" - fr=1 - if [ "$clean" = "1" ]; - then - rm "$file" - fi - else - say " ${r}-${x} Skipping empty file $file" - fi - else - say " ${r}-${x} file not found at $file" - fi - fi -done <<< "$(echo -e "$files")" - -if [ "$fr" = "0" ]; -then - say "${r}-->${x} No coverage data found." - say " Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}" - say " search for your projects language to learn how to collect reports." - exit ${exit_with}; -fi - -if [ "$ft_fix" = "1" ]; -then - say "${e}==>${x} Appending adjustments" - say " ${b}https://docs.codecov.io/docs/fixing-reports${x}" - - empty_line='^[[:space:]]*$' - # // - syntax_comment='^[[:space:]]*//.*' - # /* or */ - syntax_comment_block='^[[:space:]]*(\/\*|\*\/)[[:space:]]*$' - # { or } - syntax_bracket='^[[:space:]]*[\{\}][[:space:]]*(//.*)?$' - # [ or ] - syntax_list='^[[:space:]]*[][][[:space:]]*(//.*)?$' - - # shellcheck disable=SC2089 - skip_dirs="-not -path '*/$bower_components/*' \ - -not -path '*/node_modules/*'" - - cut_and_join() { - awk 'BEGIN { FS=":" } - $3 ~ /\/\*/ || $3 ~ /\*\// { print $0 ; next } - $1!=key { if (key!="") print out ; key=$1 ; out=$1":"$2 ; next } - { out=out","$2 } - END { print out }' 2>/dev/null - } - - if echo "$network" | grep -m1 '.kt$' 1>/dev/null; - then - # skip brackets and comments - find "$git_root" -type f \ - -name '*.kt' \ - -exec \ - grep -nIHE -e "$syntax_bracket" \ - -e "$syntax_comment_block" {} \; \ - | cut_and_join \ - >> "$adjustments_file" \ - || echo '' - - # last line in file - find "$git_root" -type f \ - -name '*.kt' -exec \ - wc -l {} \; \ - | while read -r l; do echo "EOF: $l"; done \ - 2>/dev/null \ - >> "$adjustments_file" \ - || echo '' - - fi - - if echo "$network" | grep -m1 '.go$' 1>/dev/null; - then - # skip empty lines, comments, and brackets - find "$git_root" -not -path '*/vendor/*' \ - -type f \ - -name '*.go' \ - -exec \ - grep -nIHE \ - -e "$empty_line" \ - -e "$syntax_comment" \ - -e "$syntax_comment_block" \ - -e "$syntax_bracket" \ - {} \; \ - | cut_and_join \ - >> "$adjustments_file" \ - || echo '' - fi - - if echo "$network" | grep -m1 '.dart$' 1>/dev/null; - then - # skip brackets - find "$git_root" -type f \ - -name '*.dart' \ - -exec \ - grep -nIHE \ - -e "$syntax_bracket" \ - {} \; \ - | cut_and_join \ - >> "$adjustments_file" \ - || echo '' - fi - - if echo "$network" | grep -m1 '.php$' 1>/dev/null; - then - # skip empty lines, comments, and brackets - find "$git_root" -not -path "*/vendor/*" \ - -type f \ - -name '*.php' \ - -exec \ - grep -nIHE \ - -e "$syntax_list" \ - -e "$syntax_bracket" \ - -e '^[[:space:]]*\);[[:space:]]*(//.*)?$' \ - {} \; \ - | cut_and_join \ - >> "$adjustments_file" \ - || echo '' - fi - - if echo "$network" | grep -m1 '\(.cpp\|.h\|.cxx\|.c\|.hpp\|.m\|.swift\)$' 1>/dev/null; - then - # skip brackets - # shellcheck disable=SC2086,SC2090 - find "$git_root" -type f \ - $skip_dirs \ - \( \ - -name '*.h' \ - -or -name '*.cpp' \ - -or -name '*.cxx' \ - -or -name '*.m' \ - -or -name '*.c' \ - -or -name '*.hpp' \ - -or -name '*.swift' \ - \) -exec \ - grep -nIHE \ - -e "$empty_line" \ - -e "$syntax_bracket" \ - -e '// LCOV_EXCL' \ - {} \; \ - | cut_and_join \ - >> "$adjustments_file" \ - || echo '' - - # skip brackets - # shellcheck disable=SC2086,SC2090 - find "$git_root" -type f \ - $skip_dirs \ - \( \ - -name '*.h' \ - -or -name '*.cpp' \ - -or -name '*.cxx' \ - -or -name '*.m' \ - -or -name '*.c' \ - -or -name '*.hpp' \ - -or -name '*.swift' \ - \) -exec \ - grep -nIH '// LCOV_EXCL' \ - {} \; \ - >> "$adjustments_file" \ - || echo '' - - fi - - found=$(< "$adjustments_file" tr -d ' ') - - if [ "$found" != "" ]; - then - say " ${g}+${x} Found adjustments" - { - echo "# path=fixes"; - cat "$adjustments_file"; - echo "<<<<<< EOF"; - } >> "$upload_file" - rm -rf "$adjustments_file" - else - say " ${e}->${x} No adjustments found" - fi -fi - -if [ "$url_o" != "" ]; -then - url="$url_o" -fi - -if [ "$dump" != "0" ]; -then - # trim whitespace from query - say " ${e}->${x} Dumping upload file (no upload)" - echo "$url/upload/v4?$(echo "package=bash-$VERSION&token=$token&$query" | tr -d ' ')" - cat "$upload_file" -else - if [ "$save_to" != "" ]; - then - say "${e}==>${x} Copying upload file to ${save_to}" - cp "$upload_file" "$save_to" - fi - - say "${e}==>${x} Gzipping contents" - gzip -nf9 "$upload_file" - - query=$(echo "${query}" | tr -d ' ') - say "${e}==>${x} Uploading reports" - say " ${e}url:${x} $url" - say " ${e}query:${x} $query" - - # Full query without token (to display on terminal output) - queryNoToken=$(echo "package=bash-$VERSION&token=secret&$query" | tr -d ' ') - # now add token to query - query=$(echo "package=bash-$VERSION&token=$token&$query" | tr -d ' ') - - if [ "$ft_s3" = "1" ]; - then - say "${e}->${x} Pinging Codecov" - say "$url/upload/v4?$queryNoToken" - # shellcheck disable=SC2086,2090 - res=$(curl $curl_s -X POST $curlargs $cacert \ - --retry 5 --retry-delay 2 --connect-timeout 2 \ - -H 'X-Reduced-Redundancy: false' \ - -H 'X-Content-Type: application/x-gzip' \ - "$url/upload/v4?$query" || true) - # a good reply is "https://codecov.io" + "\n" + "https://storage.googleapis.com/codecov/..." - status=$(echo "$res" | head -1 | grep 'HTTP ' | cut -d' ' -f2) - if [ "$status" = "" ] && [ "$res" != "" ]; - then - s3target=$(echo "$res" | sed -n 2p) - say "${e}->${x} Uploading to" - say "${s3target}" - - # shellcheck disable=SC2086 - s3=$(curl -fiX PUT $curlawsargs \ - --data-binary @"$upload_file.gz" \ - -H 'Content-Type: application/x-gzip' \ - -H 'Content-Encoding: gzip' \ - "$s3target" || true) - - if [ "$s3" != "" ]; - then - say " ${g}->${x} View reports at ${b}$(echo "$res" | sed -n 1p)${x}" - exit 0 - else - say " ${r}X>${x} Failed to upload" - fi - elif [ "$status" = "400" ]; - then - # 400 Error - say "${g}${res}${x}" - exit ${exit_with} - fi - fi - - say "${e}==>${x} Uploading to Codecov" - - # shellcheck disable=SC2086,2090 - res=$(curl -X POST $curlargs $cacert \ - --data-binary @"$upload_file.gz" \ - --retry 5 --retry-delay 2 --connect-timeout 2 \ - -H 'Content-Type: text/plain' \ - -H 'Content-Encoding: gzip' \ - -H 'X-Content-Encoding: gzip' \ - -H 'Accept: text/plain' \ - "$url/upload/v2?$query&attempt=$i" || echo 'HTTP 500') - # HTTP 200 - # http://.... - status=$(echo "$res" | head -1 | cut -d' ' -f2) - if [ "$status" = "" ] || [ "$status" = "200" ]; - then - say " View reports at ${b}$(echo "$res" | head -2 | tail -1)${x}" - exit 0 - else - say " ${g}${res}${x}" - exit ${exit_with} - fi - - say " ${r}X> Failed to upload coverage reports${x}" -fi - -exit ${exit_with} \ No newline at end of file From dd64b9043e21602b93fade93779cce13793af4d1 Mon Sep 17 00:00:00 2001 From: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> Date: Tue, 7 May 2024 10:39:23 -0400 Subject: [PATCH 67/86] chore: fixing coverage token not accessible on releases (#3682) --- .github/workflows/deploy_package.yml | 1 + .github/workflows/deploy_release.yml | 1 + .github/workflows/deploy_unstable.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/deploy_package.yml b/.github/workflows/deploy_package.yml index a9083cc9ab..cedeb1a066 100644 --- a/.github/workflows/deploy_package.yml +++ b/.github/workflows/deploy_package.yml @@ -25,6 +25,7 @@ jobs: uses: ./.github/workflows/unit_test.yml with: identifier: 'workflow-call-unit-test' + secrets: inherit fortify: name: Run Fortify Scan diff --git a/.github/workflows/deploy_release.yml b/.github/workflows/deploy_release.yml index c15e30ab78..93a66c9618 100644 --- a/.github/workflows/deploy_release.yml +++ b/.github/workflows/deploy_release.yml @@ -21,6 +21,7 @@ jobs: uses: ./.github/workflows/unit_test.yml with: identifier: 'workflow-call-unit-test' + secrets: inherit fortify: name: Run Fortify Scan diff --git a/.github/workflows/deploy_unstable.yml b/.github/workflows/deploy_unstable.yml index 3280627c49..67adaf7d97 100644 --- a/.github/workflows/deploy_unstable.yml +++ b/.github/workflows/deploy_unstable.yml @@ -21,6 +21,7 @@ jobs: uses: ./.github/workflows/unit_test.yml with: identifier: 'workflow-call-unit-test' + secrets: inherit fortify: name: Run Fortify Scan From cd5545bdc848acc21e1f1cd3eada1d53d4f58aa1 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 7 May 2024 17:37:44 +0000 Subject: [PATCH 68/86] fix(datastore): memory leak in ModelSyncedEventEmitter (#3675) * fix(datastore): memory leak in ModelSyncedEventEmitter * resolve lint comments --- .../Sync/InitialSync/ModelSyncedEventEmitter.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/ModelSyncedEventEmitter.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/ModelSyncedEventEmitter.swift index 9a44dbbc45..8e59168d0d 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/ModelSyncedEventEmitter.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/ModelSyncedEventEmitter.swift @@ -65,7 +65,7 @@ final class ModelSyncedEventEmitter { self.syncOrchestratorSink = initialSyncOrchestrator? .publisher .receive(on: queue) - .filter(filterSyncOperationEvent(_:)) + .filter { [weak self] in self?.filterSyncOperationEvent($0) == true } .sink(receiveCompletion: { _ in }, receiveValue: { [weak self] value in self?.onReceiveSyncOperationEvent(value: value) @@ -74,7 +74,7 @@ final class ModelSyncedEventEmitter { self.reconciliationQueueSink = reconciliationQueue? .publisher .receive(on: queue) - .filter(filterReconciliationQueueEvent(_:)) + .filter { [weak self] in self?.filterReconciliationQueueEvent($0) == true } .sink(receiveCompletion: { _ in }, receiveValue: { [weak self] value in self?.onReceiveReconciliationEvent(value: value) From 5fd846411519ac379e6f4dcfa8c2e5d38463903c Mon Sep 17 00:00:00 2001 From: Ian Saultz <52051793+atierian@users.noreply.github.com> Date: Tue, 7 May 2024 14:42:03 -0400 Subject: [PATCH 69/86] chore: require admin approval for codeowners and CI/CD versions (#3684) --- .github/CODEOWNERS | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7cf00ba028..22e13f4dc7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,8 @@ -* @aws-amplify/amplify-ios +# Amplify iOS / Swift team has general approval permissions. +* @aws-amplify/amplify-ios + +# Changes to this file requires admin approval. +/.github/CODEOWNERS @aws-amplify/amplify-ios-admins + +# Changes to Xcode / OS runtime versions run in CI/CD requires admin approval. +/.github/composite_actions/get_platform_parameters/action.yml @aws-amplify/amplify-ios-admins From 2e11056af1bdafae32449d0e70e4bafbd5bd8f8c Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Wed, 8 May 2024 10:37:40 -0400 Subject: [PATCH 70/86] test(logging): gen2 integ tests (#3683) --- ...ggingPluginGen2IntegrationTests.xctestplan | 5 - ...udWatchLoggingPluginIntegrationTests.swift | 11 +- .../README.md | 164 ++++++++++++++---- 3 files changed, 140 insertions(+), 40 deletions(-) diff --git a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginGen2IntegrationTests.xctestplan b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginGen2IntegrationTests.xctestplan index 4eee184d86..821300d9aa 100644 --- a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginGen2IntegrationTests.xctestplan +++ b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginGen2IntegrationTests.xctestplan @@ -17,11 +17,6 @@ }, "testTargets" : [ { - "skippedTests" : [ - "AWSCloudWatchLoggingPluginIntergrationTests\/testFlushLogWithMessages()", - "AWSCloudWatchLoggingPluginIntergrationTests\/testFlushLogWithVerboseMessageAfterDisablingPlugin()", - "AWSCloudWatchLoggingPluginIntergrationTests\/testFlushLogWithVerboseMessageAfterEnablingPlugin()" - ], "target" : { "containerPath" : "container:CloudWatchLoggingHostApp.xcodeproj", "identifier" : "21F762DF2BD6B55F0048845A", diff --git a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginIntegrationTests.swift b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginIntegrationTests.swift index 77448d5fa3..0685c07358 100644 --- a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginIntegrationTests.swift +++ b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/AWSCloudWatchLoggingPluginIntegrationTests.swift @@ -15,11 +15,11 @@ class AWSCloudWatchLoggingPluginIntergrationTests: XCTestCase { let amplifyConfigurationFile = "testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration" let amplifyOutputsFile = "testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplify_outputs" #if os(tvOS) - let amplifyConfigurationLoggingFile = "testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration_logging_tvOS" + var amplifyConfigurationLoggingFile = "testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration_logging_tvOS" #elseif os(watchOS) - let amplifyConfigurationLoggingFile = "testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration_logging_watchOS" + var amplifyConfigurationLoggingFile = "testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration_logging_watchOS" #else - let amplifyConfigurationLoggingFile = "testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration_logging" + var amplifyConfigurationLoggingFile = "testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration_logging" #endif var loggingConfiguration: AWSCloudWatchLoggingPluginConfiguration? @@ -31,6 +31,11 @@ class AWSCloudWatchLoggingPluginIntergrationTests: XCTestCase { continueAfterFailure = false do { try Amplify.add(plugin: AWSCognitoAuthPlugin()) + + if useGen2Configuration { + amplifyConfigurationLoggingFile += "_gen2" + } + let loggingConfigurationFile = try TestConfigHelper.retrieveLoggingConfiguration(forResource: amplifyConfigurationLoggingFile) loggingConfiguration = try AWSCloudWatchLoggingPluginConfiguration.loadConfiguration(from: loggingConfigurationFile) let loggingPlugin = AWSCloudWatchLoggingPlugin(loggingPluginConfiguration: loggingConfiguration) diff --git a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/README.md b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/README.md index 31956367b8..0276137774 100644 --- a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/README.md +++ b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginHostApp/AWSCloudWatchLoggingPluginIntegrationTests/README.md @@ -10,9 +10,31 @@ The following steps demonstrate how to set up Logging. Auth category is also req 2. Copy `amplifyconfiguration.json` to a new file named `AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/`. +``` +cp amplifyconfiguration.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration.json +``` + 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/`. +```json +{ + "awsCloudWatchLoggingPlugin": { + "enable": true, + "logGroupName": "", + "region": "", + "localStoreMaxSizeInMB": 1, + "flushIntervalInSeconds": 60, + "loggingConstraints": { + "defaultLogLevel": "VERBOSE" + } + } +} +``` +4. Copy `amplifyconfiguration_logging.json` to a new file named `AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration_logging.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/`. + +``` +cp amplifyconfiguration_logging.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration_logging.json +``` 5. You can now run all of the integration tests. @@ -30,17 +52,17 @@ At the time this was written, it follows the steps from here https://docs.amplif { ... "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", + "@aws-amplify/backend": "^0.15.0", + "@aws-amplify/backend-cli": "^0.15.0", + "aws-cdk": "^2.139.0", + "aws-cdk-lib": "^2.139.0", "constructs": "^10.3.0", "esbuild": "^0.20.2", - "tsx": "^4.7.1", - "typescript": "^5.4.3" + "tsx": "^4.7.3", + "typescript": "^5.4.5" }, "dependencies": { - "aws-amplify": "^6.0.25" + "aws-amplify": "^6.2.0" } } @@ -58,53 +80,131 @@ import { defineAuth, defineFunction } from '@aws-amplify/backend'; 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' - }) } }); ``` +3. Create `amplify/custom/RemoteLoggingConstraintsConstruct.resource.ts` and add the following + ```ts -import type { PreSignUpTriggerHandler } from 'aws-lambda'; +import * as cdk from "aws-cdk-lib" +import { Construct } from "constructs" +import * as logs from "aws-cdk-lib/aws-logs" +import * as iam from "aws-cdk-lib/aws-iam" + +export class RemoteLoggingConstraintsConstruct extends Construct { + constructor(scope: Construct, id: string, authRoleName: string, unAuthRoleName: string) { + super(scope, id) + + const region = cdk.Stack.of(this).region + const account = cdk.Stack.of(this).account + const logGroupName = "" + + new logs.LogGroup(this, 'Log Group', { + logGroupName: logGroupName, + retention: logs.RetentionDays.INFINITE + }) + + const authRole = iam.Role.fromRoleName(this, "Auth-Role", authRoleName) + const unAuthRole = iam.Role.fromRoleName(this, "UnAuth-Role", unAuthRoleName) + const logResource = `arn:aws:logs:${region}:${account}:log-group:${logGroupName}:log-stream:*` + const logIAMPolicy = new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + resources: [logResource], + actions: ["logs:PutLogEvents", "logs:DescribeLogStreams", "logs:CreateLogStream", "logs:FilterLogEvents"] + }) + + authRole.addToPrincipalPolicy(logIAMPolicy) + unAuthRole.addToPrincipalPolicy(logIAMPolicy) -export const handler: PreSignUpTriggerHandler = async (event) => { - // your code here - event.response.autoConfirmUser = true - return event; -}; + new cdk.CfnOutput(this, 'CloudWatchLogGroupName', { value: logGroupName }); + new cdk.CfnOutput(this, 'CloudWatchRegion', { value: region }); + } +} ``` -3. Commit and push the files to a git repository. +Update `backend.ts`. -4. Navigate to the AWS Amplify console (https://us-east-1.console.aws.amazon.com/amplify/home?region=us-east-1#/) +```ts +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; +import { RemoteLoggingConstraintsConstruct } from './custom/RemoteLoggingConstraintsConstruct/resource'; -5. Click on "Try Amplify Gen 2" button. +const backend = defineBackend({ + auth +}); -6. Choose "Option 2: Start with an existing app", and choose Github, and press Next. +// Auth - sign in with username +const { cfnUserPool } = backend.auth.resources.cfnResources +cfnUserPool.usernameAttributes = [] -7. Find the repository and branch, and click Next +// ============ Logging Stack =========== -8. Click "Save and deploy" and wait for deployment to finish. +const loggingConstruct = new RemoteLoggingConstraintsConstruct( + backend.createStack('logging-stack'), + 'logging-stack', + backend.auth.resources.authenticatedUserIamRole.roleName, + backend.auth.resources.unauthenticatedUserIamRole.roleName +); -9. Generate the `amplify_outputs.json` configuration file +``` + +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 generate config --branch main --app-id [APP_ID] --profile [AWS_PROFILE] --config-version 1 +npx amplify sandbox --config-out-dir ./config --profile [PROFILE] ``` -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. +5. Copy `amplify_outputs.json` to a new file named `AWSCloudWatchLoggingPluginIntegrationTests-amplify_outputs.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/`. ``` 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) +6. Configure the `amplifyconfiguration_logging_gen2.json` file (https://docs.amplify.aws/swift/build-a-backend/add-aws-services/logging/set-up-logging/#initialize-amplify-logging) + +```json +{ + "awsCloudWatchLoggingPlugin": { + "enable": true, + "logGroupName": "", + "region": "", + "localStoreMaxSizeInMB": 1, + "flushIntervalInSeconds": 60, + "loggingConstraints": { + "defaultLogLevel": "VERBOSE" + } + } +} +``` + +7. Copy `amplifyconfiguration_logging_gen2.json` to a new file named `AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration_logging_gen2.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/`. + +``` +cp amplifyconfiguration_logging_gen2.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration_logging_gen2.json +``` -12. Copy `amplifyconfiguration-logging.json` to a new file named `AWSCloudWatchLoggingPluginIntegrationTests-amplifyconfiguration-logging.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/`. +### Deploying from a branch (Optional) -13. You can now run all of the integration tests. +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 outputs --branch main --app-id [APP_ID] --profile [AWS_PROFILE] +``` From 668095ae23265580a880dc01f124750060b8cc7c Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Wed, 8 May 2024 12:56:55 -0400 Subject: [PATCH 71/86] test(auth): hostedUI integration tests (#3685) * test(auth): hostedUI integration tests * remove duplicate schema * update README with gen1 instructions --- .../AuthHostedUIApp.xcodeproj/project.pbxproj | 109 +++++++++++ .../AuthHostedUIAppGen2UITests.xcscheme | 58 ++++++ .../xcschemes/AuthHostedUIAppUITests.xcscheme | 54 ++++++ .../AuthHostedUIApp/AuthHostedUIApp/README.md | 170 ++++++++++++++++++ .../AuthHostedUIAppGen2UITests.xctestplan | 28 +++ .../HostedUISignInTests.swift | 4 +- .../Screen/SignInScreen.swift | 6 +- 7 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/xcshareddata/xcschemes/AuthHostedUIAppGen2UITests.xcscheme create mode 100644 AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/xcshareddata/xcschemes/AuthHostedUIAppUITests.xcscheme create mode 100644 AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/README.md create mode 100644 AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/AuthHostedUIAppGen2UITests.xctestplan diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/project.pbxproj b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/project.pbxproj index baf78f85ac..aef62a414a 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/project.pbxproj @@ -7,6 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 2163D6352BEAB577009689B1 /* SignInScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B978CD291C9C35005B465D /* SignInScreen.swift */; }; + 2163D6362BEAB577009689B1 /* UITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B978CB291C9C07005B465D /* UITestCase.swift */; }; + 2163D6372BEAB577009689B1 /* HostedUISignInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B41080F7291AD4D600297354 /* HostedUISignInTests.swift */; }; + 2163D6382BEAB577009689B1 /* SignUpScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B978CF291CA085005B465D /* SignUpScreen.swift */; }; + 2163D6392BEAB577009689B1 /* AuthenticatedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B978D3291DB1D1005B465D /* AuthenticatedScreen.swift */; }; B41080ED291AD02500297354 /* Amplify in Frameworks */ = {isa = PBXBuildFile; productRef = B41080EC291AD02500297354 /* Amplify */; }; B41080EF291AD02500297354 /* AWSCognitoAuthPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = B41080EE291AD02500297354 /* AWSCognitoAuthPlugin */; }; B41080F5291AD10700297354 /* ConfigurationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B41080F4291AD10700297354 /* ConfigurationHelper.swift */; }; @@ -26,6 +31,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 2163D6332BEAB577009689B1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B4EB96A0291ACF4400B73755 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B4EB96A7291ACF4400B73755; + remoteInfo = AuthHostedUIApp; + }; B41080E4291ACF7E00297354 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = B4EB96A0291ACF4400B73755 /* Project object */; @@ -36,6 +48,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 2163D62F2BEAADD0009689B1 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 2163D63F2BEAB577009689B1 /* AuthHostedUIAppGen2UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthHostedUIAppGen2UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2163D6402BEAB5B4009689B1 /* AuthHostedUIAppGen2UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AuthHostedUIAppGen2UITests.xctestplan; sourceTree = ""; }; 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 = ""; }; @@ -58,6 +73,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 2163D63A2BEAB577009689B1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; B41080DB291ACF7E00297354 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -80,6 +102,7 @@ B41080DF291ACF7E00297354 /* AuthHostedUIAppUITests */ = { isa = PBXGroup; children = ( + 2163D6402BEAB5B4009689B1 /* AuthHostedUIAppGen2UITests.xctestplan */, B4B978CB291C9C07005B465D /* UITestCase.swift */, B4B978CA291C9BE5005B465D /* Screen */, B41080F6291AD4C200297354 /* AuthenticationTest */, @@ -155,6 +178,7 @@ children = ( B4EB96A8291ACF4400B73755 /* AuthHostedUIApp.app */, B41080DE291ACF7E00297354 /* AuthHostedUIAppUITests.xctest */, + 2163D63F2BEAB577009689B1 /* AuthHostedUIAppGen2UITests.xctest */, ); name = Products; sourceTree = ""; @@ -170,6 +194,7 @@ B4EB96AD291ACF4400B73755 /* ContentView.swift */, B4EB96AF291ACF4600B73755 /* Assets.xcassets */, B4EB96B1291ACF4600B73755 /* Preview Content */, + 2163D62F2BEAADD0009689B1 /* README.md */, ); path = AuthHostedUIApp; sourceTree = ""; @@ -185,6 +210,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 2163D6312BEAB577009689B1 /* AuthHostedUIAppGen2UITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2163D63C2BEAB577009689B1 /* Build configuration list for PBXNativeTarget "AuthHostedUIAppGen2UITests" */; + buildPhases = ( + 2163D6342BEAB577009689B1 /* Sources */, + 2163D63A2BEAB577009689B1 /* Frameworks */, + 2163D63B2BEAB577009689B1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2163D6322BEAB577009689B1 /* PBXTargetDependency */, + ); + name = AuthHostedUIAppGen2UITests; + productName = AuthHostedUIAppUITests; + productReference = 2163D63F2BEAB577009689B1 /* AuthHostedUIAppGen2UITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; B41080DD291ACF7E00297354 /* AuthHostedUIAppUITests */ = { isa = PBXNativeTarget; buildConfigurationList = B41080E8291ACF7E00297354 /* Build configuration list for PBXNativeTarget "AuthHostedUIAppUITests" */; @@ -259,11 +302,19 @@ targets = ( B4EB96A7291ACF4400B73755 /* AuthHostedUIApp */, B41080DD291ACF7E00297354 /* AuthHostedUIAppUITests */, + 2163D6312BEAB577009689B1 /* AuthHostedUIAppGen2UITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 2163D63B2BEAB577009689B1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; B41080DC291ACF7E00297354 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -304,6 +355,18 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 2163D6342BEAB577009689B1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2163D6352BEAB577009689B1 /* SignInScreen.swift in Sources */, + 2163D6362BEAB577009689B1 /* UITestCase.swift in Sources */, + 2163D6372BEAB577009689B1 /* HostedUISignInTests.swift in Sources */, + 2163D6382BEAB577009689B1 /* SignUpScreen.swift in Sources */, + 2163D6392BEAB577009689B1 /* AuthenticatedScreen.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B41080DA291ACF7E00297354 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -333,6 +396,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 2163D6322BEAB577009689B1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B4EB96A7291ACF4400B73755 /* AuthHostedUIApp */; + targetProxy = 2163D6332BEAB577009689B1 /* PBXContainerItemProxy */; + }; B41080E5291ACF7E00297354 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = B4EB96A7291ACF4400B73755 /* AuthHostedUIApp */; @@ -341,6 +409,38 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 2163D63D2BEAB577009689B1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazon.AuthHostedUIAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = AuthHostedUIApp; + }; + name = Debug; + }; + 2163D63E2BEAB577009689B1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amazon.AuthHostedUIAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = AuthHostedUIApp; + }; + name = Release; + }; B41080E6291ACF7E00297354 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -548,6 +648,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 2163D63C2BEAB577009689B1 /* Build configuration list for PBXNativeTarget "AuthHostedUIAppGen2UITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2163D63D2BEAB577009689B1 /* Debug */, + 2163D63E2BEAB577009689B1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; B41080E8291ACF7E00297354 /* Build configuration list for PBXNativeTarget "AuthHostedUIAppUITests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/xcshareddata/xcschemes/AuthHostedUIAppGen2UITests.xcscheme b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/xcshareddata/xcschemes/AuthHostedUIAppGen2UITests.xcscheme new file mode 100644 index 0000000000..43d4a971dd --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/xcshareddata/xcschemes/AuthHostedUIAppGen2UITests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/xcshareddata/xcschemes/AuthHostedUIAppUITests.xcscheme b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/xcshareddata/xcschemes/AuthHostedUIAppUITests.xcscheme new file mode 100644 index 0000000000..b981a0f4db --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp.xcodeproj/xcshareddata/xcschemes/AuthHostedUIAppUITests.xcscheme @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/README.md b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/README.md new file mode 100644 index 0000000000..41be62504e --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIApp/README.md @@ -0,0 +1,170 @@ +# Auth + +## Schema: AuthHostedUIApp + +The following steps demonstrate how to setup the integration tests for auth plugin using Amplify CLI (Gen1). The steps were ran with version 12.10.3. + +1. Run `amplify init` and then `amplify add auth` + +- enable HostedUI +- add sign in redirect URLs for "myapp://" +- add pre-sign up lambda to auto confirm the user + +``` +What do you want to do? `Walkthrough all the auth configurations` + Select the authentication/authorization services that you want to use: `User Sign-Up, Sign-In, connected with AWS IAM controls (Enables per-user Storage features for images or other content, Analytics, and more)` + Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM) `No` + Do you want to enable 3rd party authentication providers in your identity pool? `N`o + Do you want to add User Pool Groups? `No` + Do you want to add an admin queries API? `No` + Multifactor authentication (MFA) user login options: `OFF` + Email based user registration/forgot password: `Enabled (Requires per-user email entry at registration)` + Specify an email verification subject: `Your verification code` + Specify an email verification message: `Your verification code is {####}` + Do you want to override the default password policy for this User Pool? `No` + Specify the app's refresh token expiration period (in days): `30` + Do you want to specify the user attributes this app can read and write? `No` + Do you want to enable any of the following capabilities? + Do you want to use an OAuth flow? `Yes` + What domain name prefix do you want to use? `authintegbf03a97b-xxxx` + Enter your redirect signin URI: `myapp://` +? Do you want to add another redirect signin URI `No` + Enter your redirect signout URI: `myapp://` +? Do you want to add another redirect signout URI `No` + Select the OAuth scopes enabled for this project. `Phone, Email, OpenID, Profile, aws.cognito.signin.user.admin` + Select the identity providers you want to configure for your user pool: +? Do you want to configure Lambda Triggers for Cognito? `Yes` +? Which triggers do you want to enable for Cognito `Pre Sign-up` +? What functionality do you want to use for Pre Sign-up `Create your own module` +``` + +Pre Sign-up code +``` +exports.handler = async (event, context) => { + + event.response.autoConfirmUser = true; + return event; +}; +``` + +3. `amplify push` to provision the backend + +4. Copy the `amplifyconfiguration.json` file over to the test directory as `AWSCognitoAuthPluginHostedUIIntegrationTests-amplifyconfiguration.json`. The tests will automatically pick this file up. Create the directories in this path first if it currently doesn't exist. + +``` +cp amplifyconfiguration.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSCognitoAuthPluginHostedUIIntegrationTests-amplifyconfiguration.json +``` + +## Schema: AuthHostedUIAppGen2 + +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.15.0", + "@aws-amplify/backend-cli": "^0.15.0", + "aws-cdk": "^2.139.0", + "aws-cdk-lib": "^2.139.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "tsx": "^4.7.3", + "typescript": "^5.4.5" + }, + "dependencies": { + "aws-amplify": "^6.2.0" + }, +} +``` + +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, + externalProviders: { + callbackUrls: ["myapp://"], + logoutUrls: ["myapp://"], + } + }, + triggers: { + // configure a trigger to point to a function definition + preSignUp: defineFunction({ + entry: './pre-sign-up-handler.ts' + }) + } +}); +``` + +Add `amplify/auth/pre-sign-up-handler.ts` with the following: + +```ts +import type { PreSignUpTriggerHandler } from 'aws-lambda'; + +export const handler: PreSignUpTriggerHandler = async (event) => { + // your code here + event.response.autoConfirmUser = true + return event; +}; +``` + +3. Update `backend.ts` + +```ts +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; + +const backend = defineBackend({ + auth +}); +``` + +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 --profile [PROFILE] +``` + +5. Copy the `amplify_outputs.json` file over to the test directory as `AWSCognitoAuthPluginHostedUIIntegrationTests-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/AWSCognitoAuthPluginHostedUIIntegrationTests-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 outputs --branch main --app-id [APP_ID] --profile [AWS_PROFILE] +``` diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/AuthHostedUIAppGen2UITests.xctestplan b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/AuthHostedUIAppGen2UITests.xctestplan new file mode 100644 index 0000000000..d8bfefa969 --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/AuthHostedUIAppGen2UITests.xctestplan @@ -0,0 +1,28 @@ +{ + "configurations" : [ + { + "id" : "E36588F7-6C54-4EB4-BDF6-DFE09B98BAF6", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "commandLineArgumentEntries" : [ + { + "argument" : "GEN2" + } + ] + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:AuthHostedUIApp.xcodeproj", + "identifier" : "2163D6312BEAB577009689B1", + "name" : "AuthHostedUIAppGen2UITests" + } + } + ], + "version" : 1 +} diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/AuthenticationTest/HostedUISignInTests.swift b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/AuthenticationTest/HostedUISignInTests.swift index 3c9004a220..8bfc88e1e0 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/AuthenticationTest/HostedUISignInTests.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/AuthenticationTest/HostedUISignInTests.swift @@ -12,7 +12,7 @@ class HostedUISignInTests: UITestCase { func testSignInSuccess() throws { let username = "hostedUI-\(UUID().uuidString)@amazon.com" - let password = "P123@\(UUID().uuidString)" + let password = "Pp123@\(UUID().uuidString)" let signInScreen = SignInScreen(app: app) let signUpScreen = signInScreen.gotoSignUpView() _ = signUpScreen @@ -31,7 +31,7 @@ class HostedUISignInTests: UITestCase { func testSignInWithoutPresentationAnchorSuccess() throws { let username = "hostedUI-\(UUID().uuidString)@amazon.com" - let password = "P123@\(UUID().uuidString)" + let password = "Pp123@\(UUID().uuidString)" let signInScreen = SignInScreen(app: app) let signUpScreen = signInScreen.gotoSignUpView() _ = signUpScreen diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/Screen/SignInScreen.swift b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/Screen/SignInScreen.swift index 0b9224bf40..16d8a61205 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/Screen/SignInScreen.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/Screen/SignInScreen.swift @@ -46,9 +46,9 @@ struct SignInScreen: Screen { } func signIn(username: String, password: String) -> Self { - _ = app.webViews.textFields["Username"].waitForExistence(timeout: 60) - app.webViews.textFields["Username"].tap() - app.webViews.textFields["Username"].typeText(username) + _ = app.webViews.textFields["Email Email"].waitForExistence(timeout: 60) + app.webViews.textFields["Email Email"].tap() + app.webViews.textFields["Email Email"].typeText(username) app.webViews.secureTextFields["Password"].tap() app.webViews.secureTextFields["Password"].typeText(password) From b864d419125908777b3080a934c74f0917ad3722 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Wed, 8 May 2024 15:02:52 -0400 Subject: [PATCH 72/86] fix(auth): configure with standardAttributes snake case values (#3686) --- .../Configuration/AmplifyOutputsData.swift | 12 +++++----- .../AmplifyOutputsInitializationTests.swift | 22 ++++++++++++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Amplify/Core/Configuration/AmplifyOutputsData.swift b/Amplify/Core/Configuration/AmplifyOutputsData.swift index bdd7270017..93a2f60e6a 100644 --- a/Amplify/Core/Configuration/AmplifyOutputsData.swift +++ b/Amplify/Core/Configuration/AmplifyOutputsData.swift @@ -189,19 +189,19 @@ public struct AmplifyOutputsData: Codable { case address case birthdate case email - case familyName + case familyName = "family_name" case gender - case givenName + case givenName = "given_name" case locale - case middleName + case middleName = "middle_name" case name case nickname - case phoneNumber + case phoneNumber = "phone_number" case picture - case preferredUsername + case preferredUsername = "preferred_username" case profile case sub - case updatedAt + case updatedAt = "updated_at" case website case zoneinfo } diff --git a/AmplifyTests/CoreTests/AmplifyOutputsInitializationTests.swift b/AmplifyTests/CoreTests/AmplifyOutputsInitializationTests.swift index 8d34e3b768..12c2c3faae 100644 --- a/AmplifyTests/CoreTests/AmplifyOutputsInitializationTests.swift +++ b/AmplifyTests/CoreTests/AmplifyOutputsInitializationTests.swift @@ -82,7 +82,6 @@ class AmplifyOutputsInitializationTests: XCTestCase { } } - /// 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. @@ -97,6 +96,27 @@ class AmplifyOutputsInitializationTests: XCTestCase { XCTAssertEqual(config.analytics?.amazonPinpoint?.awsRegion, "us-east-1") } + /// Given: A data object with valid AmplifyOutputs JSON containing snake case values. + /// When: Amplify.configure(with: .data(:)) is invoked + /// Then: Decoded data should contain the correct data, decoding snake case values to camel case enum cases. + func testSnakeCaseJSONValues() throws { + let validAmplifyOutputsJSON = #"{"version": "1", "auth": { "aws_region": "us-east-1", "user_pool_id": "poolId123", "user_pool_client_id": "clientId123", "standard_required_attributes": [ "family_name", "given_name", "middle_name", "phone_number", "preferred_username", "updated_at" ]}}"# + let configData = Data(validAmplifyOutputsJSON.utf8) + + try Amplify.configure(with: .data(configData)) + let config = try AmplifyOutputsData.decodeAmplifyOutputsData(from: configData) + XCTAssertEqual(config.version, "1") + + guard let auth = config.auth, let attributes = auth.standardRequiredAttributes else { + XCTFail("Missing auth config after decoding") + return + } + XCTAssertEqual(auth.awsRegion, "us-east-1") + XCTAssertEqual(auth.userPoolId, "poolId123") + XCTAssertEqual(auth.userPoolClientId, "clientId123") + XCTAssertEqual(attributes.count, 6) + } + /// - Given: A valid configuration /// - When: /// - Amplify is finished configuring its plugins From c57c80d68f6cbefce82a0b320922201914a145bf Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Wed, 8 May 2024 16:35:38 -0400 Subject: [PATCH 73/86] test(auth): fix Gen1 hostedUI textfield check (#3687) --- .../Screen/SignInScreen.swift | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/Screen/SignInScreen.swift b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/Screen/SignInScreen.swift index 16d8a61205..26d1584c2d 100644 --- a/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/Screen/SignInScreen.swift +++ b/AmplifyPlugins/Auth/Tests/AuthHostedUIApp/AuthHostedUIAppUITests/Screen/SignInScreen.swift @@ -11,6 +11,10 @@ struct SignInScreen: Screen { let app: XCUIApplication + var useGen2Configuration: Bool { + ProcessInfo.processInfo.arguments.contains("GEN2") + } + private enum Identifiers { static let signUpNav = "hostedUI_signUp_view_nav" static let signInButton = "hostedUI_signIn_button" @@ -45,10 +49,24 @@ struct SignInScreen: Screen { return self } + func signIn(username: String, password: String) -> Self { - _ = app.webViews.textFields["Email Email"].waitForExistence(timeout: 60) - app.webViews.textFields["Email Email"].tap() - app.webViews.textFields["Email Email"].typeText(username) + let signInTextFieldName: String + // Ideally we align the provisioning of Gen1 and Gen2 backends + // to create a HostedUI endpoint that has the same username text field. + // The Gen1 steps are updated in the README already, we re-provision the backend + // in Gen1 according to those steps, this check can be removed and expect + // "Email Email" to be the text field. + if useGen2Configuration { + signInTextFieldName = "Email Email" + } else { + signInTextFieldName = "Username" + } + + _ = app.webViews.textFields[signInTextFieldName].waitForExistence(timeout: 60) + app.webViews.textFields[signInTextFieldName].tap() + app.webViews.textFields[signInTextFieldName].typeText(username) + app.webViews.secureTextFields["Password"].tap() app.webViews.secureTextFields["Password"].typeText(password) From da743229c54c16ab29caf45894a66920db64a79e Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 8 May 2024 23:49:22 +0000 Subject: [PATCH 74/86] chore: release 2.33.4 [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 5a5837bf37..8fa1912171 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.33.3" + public static let amplifyVersion = "2.33.4" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From f4d2466b137858e63d1696e31179a8944ef8a789 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 8 May 2024 23:51:00 +0000 Subject: [PATCH 75/86] chore: finalize release 2.33.4 [skip ci] --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 288952be9a..4c790f6f67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2.33.4 (2024-05-08) + +### Bug Fixes + +- **auth**: configure with standardAttributes snake case values (#3686) +- **datastore**: memory leak in ModelSyncedEventEmitter (#3675) + ## 2.33.3 (2024-05-03) ### Bug Fixes From d8881248b6c411ffbcd2b4306350d000966585e4 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Thu, 9 May 2024 10:24:52 -0400 Subject: [PATCH 76/86] test(api): gen2 integ tests (#3680) --- .../APIHostApp.xcodeproj/project.pbxproj | 460 +++++++---------- .../xcschemes/APIHostApp.xcscheme | 11 + ... => AWSAPIPluginGen2GraphQLTests.xcscheme} | 18 +- ...AWSAPIPluginGen2FunctionalTests.xctestplan | 39 -- .../Base/TestConfigHelper.swift | 11 +- .../GraphQLModelBasedTests.swift | 15 +- .../AWSAPIPluginFunctionalTests/README.md | 109 ---- .../AWSAPIPluginGen2GraphQLBaseTest.swift | 243 +++++++++ .../LL1/Comment4V2+Schema.swift | 66 +++ .../LL1/Comment4V2.swift | 56 +++ .../GraphQLLazyLoadPostComment4V2Tests.swift | 474 ++++++++++++++++++ .../LL1/Post4V2+Schema.swift | 66 +++ .../LL1/Post4V2.swift | 32 ++ .../LL3/CommentWithCompositeKey+Schema.swift | 73 +++ .../LL3/CommentWithCompositeKey.swift | 56 +++ ...LoadPostCommentWithCompositeKeyTests.swift | 447 +++++++++++++++++ .../LL3/PostWithCompositeKey+Schema.swift | 73 +++ .../LL3/PostWithCompositeKey.swift | 32 ++ .../AWSAPIPluginGen2GraphQLTests/README.md | 138 +++++ .../TestConfigHelper.swift | 32 ++ 20 files changed, 2005 insertions(+), 446 deletions(-) rename AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/{AWSAPIPluginGen2FunctionalTests.xcscheme => AWSAPIPluginGen2GraphQLTests.xcscheme} (75%) delete mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AWSAPIPluginGen2FunctionalTests.xctestplan create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/AWSAPIPluginGen2GraphQLBaseTest.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Comment4V2+Schema.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Comment4V2.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/GraphQLLazyLoadPostComment4V2Tests.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Post4V2+Schema.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Post4V2.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/CommentWithCompositeKey+Schema.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/CommentWithCompositeKey.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/GraphQLLazyLoadPostCommentWithCompositeKeyTests.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/PostWithCompositeKey+Schema.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/PostWithCompositeKey.swift create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/README.md create mode 100644 AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/TestConfigHelper.swift diff --git a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj index 15f0bc7a05..2d967426e6 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj @@ -60,6 +60,18 @@ 213DBC8528A6CE9800B30280 /* GraphQLWithLambdaAuthIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698BA328899B3F004BD994 /* GraphQLWithLambdaAuthIntegrationTests.swift */; }; 213DBC8728A6CEDD00B30280 /* Todo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 213DBC8628A6CEDD00B30280 /* Todo.swift */; }; 213DBC8928A700E000B30280 /* SubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 213DBC8828A700E000B30280 /* SubscriptionView.swift */; }; + 2163D60F2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLBaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D60E2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLBaseTest.swift */; }; + 2163D6172BE96E3D009689B1 /* TestConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D6162BE96E3D009689B1 /* TestConfigHelper.swift */; }; + 2163D61B2BE97494009689B1 /* GraphQLLazyLoadPostComment4V2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D61A2BE97494009689B1 /* GraphQLLazyLoadPostComment4V2Tests.swift */; }; + 2163D61D2BE974B0009689B1 /* GraphQLLazyLoadPostCommentWithCompositeKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D61C2BE974B0009689B1 /* GraphQLLazyLoadPostCommentWithCompositeKeyTests.swift */; }; + 2163D6222BE974D8009689B1 /* Comment4V2+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D61E2BE974D7009689B1 /* Comment4V2+Schema.swift */; }; + 2163D6232BE974D8009689B1 /* Post4V2+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D61F2BE974D7009689B1 /* Post4V2+Schema.swift */; }; + 2163D6242BE974D8009689B1 /* Post4V2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D6202BE974D8009689B1 /* Post4V2.swift */; }; + 2163D6252BE974D8009689B1 /* Comment4V2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D6212BE974D8009689B1 /* Comment4V2.swift */; }; + 2163D62A2BE974E6009689B1 /* PostWithCompositeKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D6262BE974E6009689B1 /* PostWithCompositeKey.swift */; }; + 2163D62B2BE974E6009689B1 /* CommentWithCompositeKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D6272BE974E6009689B1 /* CommentWithCompositeKey.swift */; }; + 2163D62C2BE974E6009689B1 /* CommentWithCompositeKey+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D6282BE974E6009689B1 /* CommentWithCompositeKey+Schema.swift */; }; + 2163D62D2BE974E6009689B1 /* PostWithCompositeKey+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2163D6292BE974E6009689B1 /* PostWithCompositeKey+Schema.swift */; }; 21698AA328899921004BD994 /* Todo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698A9F28899921004BD994 /* Todo.swift */; }; 21698AB72889996A004BD994 /* GraphQLConnectionScenario1Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AA82889996A004BD994 /* GraphQLConnectionScenario1Tests.swift */; }; 21698AB92889996A004BD994 /* GraphQLConnectionScenario3Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21698AAA2889996A004BD994 /* GraphQLConnectionScenario3Tests.swift */; }; @@ -214,76 +226,6 @@ 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 */; }; @@ -434,28 +376,28 @@ remoteGlobalIDString = 21E73E6A28898D7800D7DB7E; remoteInfo = APIHostApp; }; - 21698A7F28899805004BD994 /* PBXContainerItemProxy */ = { + 2163D6102BE96C90009689B1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 21E73E6328898D7800D7DB7E /* Project object */; proxyType = 1; remoteGlobalIDString = 21E73E6A28898D7800D7DB7E; remoteInfo = APIHostApp; }; - 21698A9928899818004BD994 /* PBXContainerItemProxy */ = { + 21698A7F28899805004BD994 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 21E73E6328898D7800D7DB7E /* Project object */; proxyType = 1; remoteGlobalIDString = 21E73E6A28898D7800D7DB7E; remoteInfo = APIHostApp; }; - 21EA887728F9BC610000BA75 /* PBXContainerItemProxy */ = { + 21698A9928899818004BD994 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 21E73E6328898D7800D7DB7E /* Project object */; proxyType = 1; remoteGlobalIDString = 21E73E6A28898D7800D7DB7E; remoteInfo = APIHostApp; }; - 21F7624F2BD6B0710048845A /* PBXContainerItemProxy */ = { + 21EA887728F9BC610000BA75 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 21E73E6328898D7800D7DB7E /* Project object */; proxyType = 1; @@ -588,6 +530,20 @@ 213DBC8028A6C4FB00B30280 /* TestConfigHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConfigHelper.swift; sourceTree = ""; }; 213DBC8628A6CEDD00B30280 /* Todo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Todo.swift; sourceTree = ""; }; 213DBC8828A700E000B30280 /* SubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionView.swift; sourceTree = ""; }; + 2163D60C2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSAPIPluginGen2GraphQLTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2163D60E2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLBaseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSAPIPluginGen2GraphQLBaseTest.swift; sourceTree = ""; }; + 2163D6152BE96D12009689B1 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 2163D6162BE96E3D009689B1 /* TestConfigHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConfigHelper.swift; sourceTree = ""; }; + 2163D61A2BE97494009689B1 /* GraphQLLazyLoadPostComment4V2Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLLazyLoadPostComment4V2Tests.swift; sourceTree = ""; }; + 2163D61C2BE974B0009689B1 /* GraphQLLazyLoadPostCommentWithCompositeKeyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLLazyLoadPostCommentWithCompositeKeyTests.swift; sourceTree = ""; }; + 2163D61E2BE974D7009689B1 /* Comment4V2+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Comment4V2+Schema.swift"; sourceTree = ""; }; + 2163D61F2BE974D7009689B1 /* Post4V2+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Post4V2+Schema.swift"; sourceTree = ""; }; + 2163D6202BE974D8009689B1 /* Post4V2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post4V2.swift; sourceTree = ""; }; + 2163D6212BE974D8009689B1 /* Comment4V2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment4V2.swift; sourceTree = ""; }; + 2163D6262BE974E6009689B1 /* PostWithCompositeKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostWithCompositeKey.swift; sourceTree = ""; }; + 2163D6272BE974E6009689B1 /* CommentWithCompositeKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommentWithCompositeKey.swift; sourceTree = ""; }; + 2163D6282BE974E6009689B1 /* CommentWithCompositeKey+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CommentWithCompositeKey+Schema.swift"; sourceTree = ""; }; + 2163D6292BE974E6009689B1 /* PostWithCompositeKey+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PostWithCompositeKey+Schema.swift"; sourceTree = ""; }; 21698A7B28899804004BD994 /* AWSAPIPluginFunctionalTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSAPIPluginFunctionalTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 21698A9528899818004BD994 /* AWSAPIPluginGraphQLAuthDirectiveTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSAPIPluginGraphQLAuthDirectiveTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 21698A9F28899921004BD994 /* Todo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Todo.swift; sourceTree = ""; }; @@ -755,8 +711,6 @@ 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 = ""; }; @@ -796,6 +750,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2163D6092BE96C90009689B1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 21698A7828899804004BD994 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -829,13 +790,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 21F762972BD6B0710048845A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 395906A928AC4A16004B96B1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -995,10 +949,45 @@ path = Base; sourceTree = ""; }; + 2163D60D2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLTests */ = { + isa = PBXGroup; + children = ( + 2163D6192BE97476009689B1 /* LL3 */, + 2163D6182BE9740B009689B1 /* LL1 */, + 2163D60E2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLBaseTest.swift */, + 2163D6152BE96D12009689B1 /* README.md */, + 2163D6162BE96E3D009689B1 /* TestConfigHelper.swift */, + ); + path = AWSAPIPluginGen2GraphQLTests; + sourceTree = ""; + }; + 2163D6182BE9740B009689B1 /* LL1 */ = { + isa = PBXGroup; + children = ( + 2163D6212BE974D8009689B1 /* Comment4V2.swift */, + 2163D61E2BE974D7009689B1 /* Comment4V2+Schema.swift */, + 2163D6202BE974D8009689B1 /* Post4V2.swift */, + 2163D61F2BE974D7009689B1 /* Post4V2+Schema.swift */, + 2163D61A2BE97494009689B1 /* GraphQLLazyLoadPostComment4V2Tests.swift */, + ); + path = LL1; + sourceTree = ""; + }; + 2163D6192BE97476009689B1 /* LL3 */ = { + isa = PBXGroup; + children = ( + 2163D6272BE974E6009689B1 /* CommentWithCompositeKey.swift */, + 2163D6282BE974E6009689B1 /* CommentWithCompositeKey+Schema.swift */, + 2163D6262BE974E6009689B1 /* PostWithCompositeKey.swift */, + 2163D6292BE974E6009689B1 /* PostWithCompositeKey+Schema.swift */, + 2163D61C2BE974B0009689B1 /* GraphQLLazyLoadPostCommentWithCompositeKeyTests.swift */, + ); + path = LL3; + sourceTree = ""; + }; 21698A7C28899805004BD994 /* AWSAPIPluginFunctionalTests */ = { isa = PBXGroup; children = ( - 21F7629E2BD6B0B40048845A /* AWSAPIPluginGen2FunctionalTests.xctestplan */, 21E581E32A6835910027D13A /* API.swift */, 212626CA289ABC79003788E3 /* Base */, 606C8B782B895E5A00716094 /* AppSyncRealTimeClientTests.swift */, @@ -1267,6 +1256,7 @@ 395906C128AC63A9004B96B1 /* AWSAPIPluginRESTUserPoolTests */, 97914BC429558714002000EA /* GraphQLAPIStressTests */, 21EA887428F9BC600000BA75 /* AWSAPIPluginLazyLoadTests */, + 2163D60D2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLTests */, 21E73E6C28898D7900D7DB7E /* Products */, 21698BD728899EBB004BD994 /* Frameworks */, ); @@ -1289,7 +1279,7 @@ 681B35892A43962D0074F369 /* AWSAPIPluginFunctionalTestsWatch.xctest */, 681B35A12A4396CF0074F369 /* AWSAPIPluginGraphQLLambdaAuthTestsWatch.xctest */, 681B35C52A43970A0074F369 /* AWSAPIPluginRESTIAMTestsWatch.xctest */, - 21F7629D2BD6B0710048845A /* AWSAPIPluginGen2FunctionalTests.xctest */, + 2163D60C2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLTests.xctest */, ); name = Products; sourceTree = ""; @@ -1531,6 +1521,25 @@ productReference = 213DBC7528A6C47000B30280 /* AWSAPIPluginGraphQLLambdaAuthTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 2163D60B2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2163D6122BE96C90009689B1 /* Build configuration list for PBXNativeTarget "AWSAPIPluginGen2GraphQLTests" */; + buildPhases = ( + 2163D6082BE96C90009689B1 /* Sources */, + 2163D6092BE96C90009689B1 /* Frameworks */, + 2163D60A2BE96C90009689B1 /* Resources */, + 2163D62E2BE974F3009689B1 /* Copy Configuration Files */, + ); + buildRules = ( + ); + dependencies = ( + 2163D6112BE96C90009689B1 /* PBXTargetDependency */, + ); + name = AWSAPIPluginGen2GraphQLTests; + productName = AWSAPIPluginGen2GraphQLTests; + productReference = 2163D60C2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 21698A7A28899804004BD994 /* AWSAPIPluginFunctionalTests */ = { isa = PBXNativeTarget; buildConfigurationList = 21698A8328899805004BD994 /* Build configuration list for PBXNativeTarget "AWSAPIPluginFunctionalTests" */; @@ -1612,25 +1621,6 @@ 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" */; @@ -1834,6 +1824,10 @@ CreatedOnToolsVersion = 14.0; TestTargetID = 21E73E6A28898D7800D7DB7E; }; + 2163D60B2BE96C90009689B1 = { + CreatedOnToolsVersion = 15.2; + TestTargetID = 21E73E6A28898D7800D7DB7E; + }; 21698A7A28899804004BD994 = { CreatedOnToolsVersion = 13.4.1; LastSwiftMigration = 1340; @@ -1911,7 +1905,7 @@ 681B353E2A43962D0074F369 /* AWSAPIPluginFunctionalTestsWatch */, 681B35912A4396CF0074F369 /* AWSAPIPluginGraphQLLambdaAuthTestsWatch */, 681B35B62A43970A0074F369 /* AWSAPIPluginRESTIAMTestsWatch */, - 21F7624D2BD6B0710048845A /* AWSAPIPluginGen2FunctionalTests */, + 2163D60B2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLTests */, ); }; /* End PBXProject section */ @@ -1924,37 +1918,37 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 21698A7928899804004BD994 /* Resources */ = { + 2163D60A2BE96C90009689B1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 21698A9328899818004BD994 /* Resources */ = { + 21698A7928899804004BD994 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 21E73E6928898D7800D7DB7E /* Resources */ = { + 21698A9328899818004BD994 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 21E73E7628898D7A00D7DB7E /* Preview Assets.xcassets in Resources */, - 21E73E7328898D7A00D7DB7E /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 21EA887128F9BC600000BA75 /* Resources */ = { + 21E73E6928898D7800D7DB7E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 21E73E7628898D7A00D7DB7E /* Preview Assets.xcassets in Resources */, + 21E73E7328898D7A00D7DB7E /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 21F762982BD6B0710048845A /* Resources */ = { + 21EA887128F9BC600000BA75 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -2046,7 +2040,7 @@ 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"; }; - 21698CC12889D5FE004BD994 /* Copy Configuration folder */ = { + 2163D62E2BE974F3009689B1 /* Copy Configuration Files */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -2055,7 +2049,7 @@ ); inputPaths = ( ); - name = "Copy Configuration folder"; + name = "Copy Configuration Files"; outputFileListPaths = ( ); outputPaths = ( @@ -2064,7 +2058,7 @@ 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"; }; - 21EA887C28F9BC720000BA75 /* Copy Configuration Files */ = { + 21698CC12889D5FE004BD994 /* Copy Configuration folder */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -2073,16 +2067,16 @@ ); inputPaths = ( ); - name = "Copy Configuration Files"; + name = "Copy Configuration folder"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; 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"; + 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"; }; - 21F762992BD6B0710048845A /* Copy Configuration folder */ = { + 21EA887C28F9BC720000BA75 /* Copy Configuration Files */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -2091,14 +2085,14 @@ ); inputPaths = ( ); - name = "Copy Configuration folder"; + name = "Copy Configuration Files"; 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"; + 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"; }; 395906B528AC4A22004B96B1 /* Copy Configuration Files */ = { isa = PBXShellScriptBuildPhase; @@ -2278,6 +2272,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2163D6082BE96C90009689B1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2163D61B2BE97494009689B1 /* GraphQLLazyLoadPostComment4V2Tests.swift in Sources */, + 2163D60F2BE96C90009689B1 /* AWSAPIPluginGen2GraphQLBaseTest.swift in Sources */, + 2163D62D2BE974E6009689B1 /* PostWithCompositeKey+Schema.swift in Sources */, + 2163D6252BE974D8009689B1 /* Comment4V2.swift in Sources */, + 2163D6242BE974D8009689B1 /* Post4V2.swift in Sources */, + 2163D6172BE96E3D009689B1 /* TestConfigHelper.swift in Sources */, + 2163D62A2BE974E6009689B1 /* PostWithCompositeKey.swift in Sources */, + 2163D61D2BE974B0009689B1 /* GraphQLLazyLoadPostCommentWithCompositeKeyTests.swift in Sources */, + 2163D62C2BE974E6009689B1 /* CommentWithCompositeKey+Schema.swift in Sources */, + 2163D62B2BE974E6009689B1 /* CommentWithCompositeKey.swift in Sources */, + 2163D6222BE974D8009689B1 /* Comment4V2+Schema.swift in Sources */, + 2163D6232BE974D8009689B1 /* Post4V2+Schema.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 21698A7728899804004BD994 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2511,83 +2524,6 @@ ); 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; @@ -2775,6 +2711,11 @@ target = 21E73E6A28898D7800D7DB7E /* APIHostApp */; targetProxy = 213DBC7928A6C47000B30280 /* PBXContainerItemProxy */; }; + 2163D6112BE96C90009689B1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 21E73E6A28898D7800D7DB7E /* APIHostApp */; + targetProxy = 2163D6102BE96C90009689B1 /* PBXContainerItemProxy */; + }; 21698A8028899805004BD994 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 21E73E6A28898D7800D7DB7E /* APIHostApp */; @@ -2790,11 +2731,6 @@ target = 21E73E6A28898D7800D7DB7E /* APIHostApp */; targetProxy = 21EA887728F9BC610000BA75 /* PBXContainerItemProxy */; }; - 21F7624E2BD6B0710048845A /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 21E73E6A28898D7800D7DB7E /* APIHostApp */; - targetProxy = 21F7624F2BD6B0710048845A /* PBXContainerItemProxy */; - }; 395906B128AC4A16004B96B1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 21E73E6A28898D7800D7DB7E /* APIHostApp */; @@ -2882,6 +2818,53 @@ }; name = Release; }; + 2163D6132BE96C90009689B1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.api.AWSAPIPluginGen2GraphQLTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/APIHostApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/APIHostApp"; + }; + name = Debug; + }; + 2163D6142BE96C90009689B1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.api.AWSAPIPluginGen2GraphQLTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/APIHostApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/APIHostApp"; + }; + name = Release; + }; 21698A8128899805004BD994 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3218,61 +3201,6 @@ }; 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 = { @@ -3709,6 +3637,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 2163D6122BE96C90009689B1 /* Build configuration list for PBXNativeTarget "AWSAPIPluginGen2GraphQLTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2163D6132BE96C90009689B1 /* Debug */, + 2163D6142BE96C90009689B1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 21698A8328899805004BD994 /* Build configuration list for PBXNativeTarget "AWSAPIPluginFunctionalTests" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -3754,15 +3691,6 @@ 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 8d3f8617a9..5d52fc8542 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/APIHostApp.xcscheme +++ b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/APIHostApp.xcscheme @@ -60,6 +60,17 @@ ReferencedContainer = "container:APIHostApp.xcodeproj"> + + + + - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + skipped = "NO" + parallelizable = "YES"> diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AWSAPIPluginGen2FunctionalTests.xctestplan b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AWSAPIPluginGen2FunctionalTests.xctestplan deleted file mode 100644 index 1567400a72..0000000000 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AWSAPIPluginGen2FunctionalTests.xctestplan +++ /dev/null @@ -1,39 +0,0 @@ -{ - "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 1cec4e476b..44837045b1 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/Base/TestConfigHelper.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/Base/TestConfigHelper.swift @@ -6,25 +6,16 @@ // import Foundation -@_spi(InternalAmplifyConfiguration) @testable import Amplify +@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 1790c35b16..8d16762f17 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests.swift @@ -18,7 +18,6 @@ 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) { @@ -37,16 +36,10 @@ class GraphQLModelBasedTests: XCTestCase { do { try Amplify.add(plugin: plugin) - - 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) - } + let amplifyConfig = try TestConfigHelper.retrieveAmplifyConfiguration( + forResource: GraphQLModelBasedTests.amplifyConfiguration) + try Amplify.configure(amplifyConfig) + ModelRegistry.register(modelType: Comment.self) ModelRegistry.register(modelType: Post.self) diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/README.md b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/README.md index ece9d90323..1a2c01c142 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/README.md +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/README.md @@ -250,112 +250,3 @@ cp amplifyconfiguration.json ~/.aws-amplify/amplify-ios/testconfiguration/GraphQ ``` 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/APIHostApp/AWSAPIPluginGen2GraphQLTests/AWSAPIPluginGen2GraphQLBaseTest.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/AWSAPIPluginGen2GraphQLBaseTest.swift new file mode 100644 index 0000000000..b8fb23a94a --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/AWSAPIPluginGen2GraphQLBaseTest.swift @@ -0,0 +1,243 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +@testable import AWSAPIPlugin +@_spi(InternalAmplifyConfiguration) @testable import Amplify +@testable import APIHostApp +@testable import AWSPluginsCore + +class AWSAPIPluginGen2GraphQLBaseTest: XCTestCase { + + var amplifyConfig: AmplifyOutputsData! + + override func setUp() { + continueAfterFailure = false + } + + override func tearDown() async throws { + await Amplify.reset() + try await Task.sleep(seconds: 1) + } + + func setupConfig() { + let basePath = "testconfiguration" + let baseFileName = "Gen2GraphQLTests" + let configFile = "\(basePath)/\(baseFileName)-amplify_outputs" + + do { + amplifyConfig = try TestConfigHelper.retrieveAmplifyOutputsData(forResource: configFile) + } catch { + XCTFail("Error during setup: \(error)") + } + } + + /// Setup API with given models + /// - Parameter models: DataStore models + func setup(withModels models: AmplifyModelRegistration, + logLevel: LogLevel = .verbose) async { + do { + setupConfig() + Amplify.Logging.logLevel = logLevel + try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: models)) + try Amplify.configure(amplifyConfig) + + } catch { + XCTFail("Error during setup: \(error)") + } + } + + @discardableResult + func mutate(_ request: GraphQLRequest) async throws -> M { + do { + let graphQLResponse = try await Amplify.API.mutate(request: request) + switch graphQLResponse { + case .success(let model): + return model + case .failure(let graphQLError): + XCTFail("Failed with error \(graphQLError)") + } + } catch { + XCTFail("Failed with error \(error)") + } + + throw "See XCTFail message" + } + + func query(_ request: GraphQLRequest) async throws -> M? { + do { + let graphQLResponse = try await Amplify.API.query(request: request) + switch graphQLResponse { + case .success(let model): + return model + case .failure(let graphQLError): + XCTFail("Failed with error \(graphQLError)") + } + } catch { + XCTFail("Failed with error \(error)") + } + throw "See XCTFail message" + } + + func listQuery(_ request: GraphQLRequest>) async throws -> List { + do { + let graphQLResponse = try await Amplify.API.query(request: request) + switch graphQLResponse { + case .success(let models): + return models + case .failure(let graphQLError): + XCTFail("Failed with error \(graphQLError)") + } + } catch { + XCTFail("Failed with error \(error)") + } + throw "See XCTFail message" + } + + enum AssertLazyModelState { + case notLoaded(identifiers: [LazyReferenceIdentifier]?) + case loaded(model: M?) + } + + func assertLazyReference(_ lazyModel: LazyReference, + state: AssertLazyModelState) { + switch state { + case .notLoaded(let expectedIdentifiers): + if case .notLoaded(let identifiers) = lazyModel.modelProvider.getState() { + XCTAssertEqual(identifiers, expectedIdentifiers) + } else { + XCTFail("Should be not loaded with identifiers \(expectedIdentifiers)") + } + case .loaded(let expectedModel): + if case .loaded(let model) = lazyModel.modelProvider.getState() { + guard let expectedModel = expectedModel, let model = model else { + XCTAssertNil(model) + return + } + XCTAssertEqual(model.identifier, expectedModel.identifier) + } else { + XCTFail("Should be loaded with model \(String(describing: expectedModel))") + } + } + } + + enum AssertListState { + case isNotLoaded(associatedIdentifiers: [String], associatedFields: [String]) + case isLoaded(count: Int) + } + + func assertList(_ list: List, state: AssertListState) { + switch state { + case .isNotLoaded(let expectedAssociatedIdentifiers, let expectedAssociatedFields): + if case .notLoaded(let associatedIdentifiers, let associatedFields) = list.listProvider.getState() { + XCTAssertEqual(associatedIdentifiers, expectedAssociatedIdentifiers) + XCTAssertEqual(associatedFields, expectedAssociatedFields) + } else { + XCTFail("It should be not loaded with expected associatedIds \(expectedAssociatedIdentifiers) associatedFields \(expectedAssociatedFields)") + } + case .isLoaded(let count): + if case .loaded(let loadedList) = list.listProvider.getState() { + XCTAssertEqual(loadedList.count, count) + } else { + XCTFail("It should be loaded with expected count \(count)") + } + } + } + + func assertModelExists(_ model: M) async throws { + let modelExists = try await query(for: model) != nil + XCTAssertTrue(modelExists) + } + + func assertModelDoesNotExist(_ model: M) async throws { + let modelExists = try await query(for: model) != nil + XCTAssertFalse(modelExists) + } + + func query(for model: M, includes: IncludedAssociations = { _ in [] }) async throws -> M? { + let id = M.identifier(model)(schema: model.schema) + + var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: model.schema, + operationType: .query) + documentBuilder.add(decorator: DirectiveNameDecorator(type: .get)) + + if let modelPath = M.rootPath as? ModelPath { + let associations = includes(modelPath) + documentBuilder.add(decorator: IncludeAssociationDecorator(associations)) + } + documentBuilder.add(decorator: ModelIdDecorator(identifierFields: id.fields)) + let document = documentBuilder.build() + + let request = GraphQLRequest(document: document.stringValue, + variables: document.variables, + responseType: M?.self, + decodePath: document.name) + return try await query(request) + } + + func subscribe( + of modelType: M.Type, + type: GraphQLSubscriptionType, + verifyChange: @escaping (M) async throws -> Bool + ) async throws -> (XCTestExpectation, AmplifyAsyncThrowingSequence>) { + let connected = expectation(description: "Subscription connected") + let eventReceived = expectation(description: "\(type.rawValue) received") + let subscription = Amplify.API.subscribe(request: .subscription(of: modelType, type: type)) + + Task { + for try await subscriptionEvent in subscription { + if subscriptionEvent.isConnected() { + connected.fulfill() + } + + if let error = subscriptionEvent.extractError() { + XCTFail("Failed to \(type.rawValue) \(modelType), error: \(error.errorDescription)") + } + + if let data = subscriptionEvent.extractData(), + try await verifyChange(data) + { + eventReceived.fulfill() + } + } + } + + await fulfillment(of: [connected], timeout: 10) + return (eventReceived, subscription) + } +} + +extension LazyReferenceIdentifier: Equatable { + public static func == (lhs: LazyReferenceIdentifier, rhs: LazyReferenceIdentifier) -> Bool { + return lhs.name == rhs.name && lhs.value == rhs.value + } +} + + +extension GraphQLSubscriptionEvent { + func isConnected() -> Bool { + if case .connection(.connected) = self { + return true + } + return false + } + + func extractData() -> T? { + if case .data(.success(let data)) = self { + return data + } + return nil + } + + func extractError() -> GraphQLResponseError? { + if case .data(.failure(let error)) = self { + return error + } + return nil + } + +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Comment4V2+Schema.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Comment4V2+Schema.swift new file mode 100644 index 0000000000..e02097061d --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Comment4V2+Schema.swift @@ -0,0 +1,66 @@ +// swiftlint:disable all +import Amplify +import Foundation + +extension Comment4V2 { + // MARK: - CodingKeys + public enum CodingKeys: String, ModelKey { + case id + case content + case post + case createdAt + case updatedAt + } + + public static let keys = CodingKeys.self + // MARK: - ModelSchema + + public static let schema = defineSchema { model in + let comment4V2 = Comment4V2.keys + + model.authRules = [ + rule(allow: .public, provider: .apiKey, operations: [.create, .update, .delete, .read]) + ] + + model.listPluralName = "Comment4V2s" + model.syncPluralName = "Comment4V2s" + + model.attributes( + .index(fields: ["id"], name: nil), + .primaryKey(fields: [comment4V2.id]) + ) + + model.fields( + .field(comment4V2.id, is: .required, ofType: .string), + .field(comment4V2.content, is: .required, ofType: .string), + .belongsTo(comment4V2.post, is: .optional, ofType: Post4V2.self, targetNames: ["postID"]), + .field(comment4V2.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime), + .field(comment4V2.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime) + ) + } + public class Path: ModelPath { } + + public static var rootPath: PropertyContainerPath? { Path() } +} + +extension Comment4V2: ModelIdentifiable { + public typealias IdentifierFormat = ModelIdentifierFormat.Default + public typealias IdentifierProtocol = DefaultModelIdentifier +} +extension ModelPath where ModelType == Comment4V2 { + public var id: FieldPath { + string("id") + } + public var content: FieldPath { + string("content") + } + public var post: ModelPath { + Post4V2.Path(name: "post", parent: self) + } + public var createdAt: FieldPath { + datetime("createdAt") + } + public var updatedAt: FieldPath { + datetime("updatedAt") + } +} \ No newline at end of file diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Comment4V2.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Comment4V2.swift new file mode 100644 index 0000000000..1c6a50f04f --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Comment4V2.swift @@ -0,0 +1,56 @@ +// swiftlint:disable all +import Amplify +import Foundation + +public struct Comment4V2: Model { + public let id: String + public var content: String + internal var _post: LazyReference + public var post: Post4V2? { + get async throws { + try await _post.get() + } + } + public var createdAt: Temporal.DateTime? + public var updatedAt: Temporal.DateTime? + + public init(id: String = UUID().uuidString, + content: String, + post: Post4V2? = nil) { + self.init(id: id, + content: content, + post: post, + createdAt: nil, + updatedAt: nil) + } + internal init(id: String = UUID().uuidString, + content: String, + post: Post4V2? = nil, + createdAt: Temporal.DateTime? = nil, + updatedAt: Temporal.DateTime? = nil) { + self.id = id + self.content = content + self._post = LazyReference(post) + self.createdAt = createdAt + self.updatedAt = updatedAt + } + public mutating func setPost(_ post: Post4V2? = nil) { + self._post = LazyReference(post) + } + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decode(String.self, forKey: .id) + content = try values.decode(String.self, forKey: .content) + _post = try values.decodeIfPresent(LazyReference.self, forKey: .post) ?? LazyReference(identifiers: nil) + createdAt = try? values.decode(Temporal.DateTime?.self, forKey: .createdAt) + updatedAt = try? values.decode(Temporal.DateTime?.self, forKey: .updatedAt) + } + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(content, forKey: .content) + try container.encode(_post, forKey: .post) + try container.encode(createdAt, forKey: .createdAt) + try container.encode(updatedAt, forKey: .updatedAt) + } +} \ No newline at end of file diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/GraphQLLazyLoadPostComment4V2Tests.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/GraphQLLazyLoadPostComment4V2Tests.swift new file mode 100644 index 0000000000..6f7750cdb3 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/GraphQLLazyLoadPostComment4V2Tests.swift @@ -0,0 +1,474 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + + +import Foundation +import Combine +import XCTest + +@testable import Amplify +import AWSPluginsCore + +final class GraphQLLazyLoadPostComment4V2Tests: AWSAPIPluginGen2GraphQLBaseTest { + + func testSave() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + try await mutate(.create(post)) + try await mutate(.create(comment)) + } + + // Without `includes` and latest codegenerated types with the model path, the post should be lazy loaded + func testCommentWithLazyLoadPost() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + let createdPost = try await mutate(.create(post)) + let createdComment = try await mutate(.create(comment)) + + // The comment's post should not be loaded, since no `includes` is passed in. + // And the codegenerated swift models have the new modelPath properties. + assertLazyReference(createdComment._post, state: .notLoaded(identifiers: [.init(name: "id", value: createdPost.id)])) + let loadedPost = try await createdComment.post! + XCTAssertEqual(loadedPost.id, createdPost.id) + + // The loaded post should have comments that are also not loaded + let comments = loadedPost.comments! + assertList(comments, state: .isNotLoaded(associatedIdentifiers: [createdPost.id], associatedFields: ["post"])) + // load the comments + try await comments.fetch() + assertList(comments, state: .isLoaded(count: 1)) + // the loaded comment's post should not be loaded + assertLazyReference(comments.first!._post, state: .notLoaded(identifiers: [.init(name: "id", value: createdPost.id)])) + } + + // With `includes` on `comment.post`, the comment's post should be eager loaded. + func testCommentWithEagerLoadPost() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + let createdPost = try await mutate(.create(post)) + let createdComment = try await mutate(.create(comment, includes: { comment in [comment.post]})) + // The comment's post should be loaded, since `includes` include the post + assertLazyReference(createdComment._post, state: .loaded(model: createdPost)) + let loadedPost = try await createdComment.post! + XCTAssertEqual(loadedPost.id, post.id) + // The loaded post should have comments that are not loaded + let comments = loadedPost.comments! + assertList(comments, state: .isNotLoaded(associatedIdentifiers: [post.id], associatedFields: ["post"])) + // load the comments + try await comments.fetch() + assertList(comments, state: .isLoaded(count: 1)) + // further nested models should not be loaded + assertLazyReference(comments.first!._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id)])) + } + + // With `includes` on `comment.post.comments`, + func testCommentWithEagerLoadPostAndPostComments() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + let createdPost = try await mutate(.create(post)) + let request = GraphQLRequest.create(comment, includes: { comment in [comment.post.comments]}) + let expectedDocument = """ + mutation CreateComment4V2($input: CreateComment4V2Input!) { + createComment4V2(input: $input) { + id + content + createdAt + updatedAt + post { + id + __typename + createdAt + title + updatedAt + comments { + items { + id + content + createdAt + updatedAt + post { + id + __typename + } + __typename + } + } + } + __typename + } + } + """ + XCTAssertEqual(request.document, expectedDocument) + let createdComment = try await mutate(request) + assertLazyReference(createdComment._post, state: .loaded(model: createdPost)) + let loadedPost = try await createdComment.post! + // The loaded post should have comments that are also loaded, since `includes` include the `post.comments` + let comments = loadedPost.comments! + assertList(comments, state: .isLoaded(count: 1)) + // further nested models should not be loaded + assertLazyReference(comments.first!._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.identifier)])) + } + + + /* + - Given: Api plugin is cleared + - When: + - Initialize with PostComment4V2Models + - Create post + - Create comment with [comment.post.comments.post] association path + - Then: + - The comment creation request GraphQL Selection Set represents the assocation path + */ + func testCommentWithEagerLoadPostAndPostCommentsAndPostCommentsPost() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + try await mutate(.create(post)) + let request = GraphQLRequest.create(comment, includes: { comment in [comment.post.comments.post]}) + let expectedDocument = """ + mutation CreateComment4V2($input: CreateComment4V2Input!) { + createComment4V2(input: $input) { + id + content + createdAt + updatedAt + post { + id + __typename + createdAt + title + updatedAt + comments { + items { + id + content + createdAt + updatedAt + post { + id + createdAt + title + updatedAt + __typename + } + __typename + } + } + } + __typename + } + } + """ + XCTAssertEqual(request.document, expectedDocument) + try await mutate(request) + } + + // Without `includes` and latest codegenerated types with the model path, the post's comments should be lazy loaded + func testPostWithLazyLoadComments() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + _ = try await mutate(.create(post)) + _ = try await mutate(.create(comment)) + let queriedPost = try await query(.get(Post.self, byId: post.id))! + let comments = queriedPost.comments! + assertList(comments, state: .isNotLoaded(associatedIdentifiers: [post.id], associatedFields: ["post"])) + try await comments.fetch() + assertList(comments, state: .isLoaded(count: 1)) + assertLazyReference(comments.first!._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id)])) + } + + // With `includes` on `post.comments` should eager load the post's comments + func testPostWithEagerLoadComments() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + _ = try await mutate(.create(post)) + _ = try await mutate(.create(comment)) + let queriedPost = try await query(.get(Post.self, byId: post.id, includes: { post in [post.comments]}))! + let comments = queriedPost.comments! + assertList(comments, state: .isLoaded(count: 1)) + assertLazyReference(comments.first!._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id)])) + } + + // With `includes` on `post.comments.post` should eager load the post's comments' post + func testPostWithEagerLoadCommentsAndPost() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + let createdPost = try await mutate(.create(post)) + _ = try await mutate(.create(comment)) + let queriedPost = try await query(.get(Post.self, byId: post.id, includes: { post in [post.comments.post]}))! + let comments = queriedPost.comments! + assertList(comments, state: .isLoaded(count: 1)) + assertLazyReference(comments.first!._post, state: .loaded(model: createdPost)) + } + + func testListPostsListComments() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + try await mutate(.create(post)) + try await mutate(.create(comment)) + + let queriedPosts = try await listQuery(.list(Post.self, where: Post.keys.id == post.id)) + assertList(queriedPosts, state: .isLoaded(count: 1)) + assertList(queriedPosts.first!.comments!, + state: .isNotLoaded(associatedIdentifiers: [post.id], associatedFields: ["post"])) + + let queriedComments = try await listQuery(.list(Comment.self, where: Comment.keys.id == comment.id)) + assertList(queriedComments, state: .isLoaded(count: 1)) + assertLazyReference(queriedComments.first!._post, + state: .notLoaded(identifiers: [.init(name: "id", value: post.id)])) + } + + func testCreateWithoutPost() async throws { + await setup(withModels: PostComment4V2Models()) + let comment = Comment(content: "content") + try await mutate(.create(comment)) + var queriedComment = try await query(.get(Comment.self, byId: comment.id))! + assertLazyReference(queriedComment._post, state: .notLoaded(identifiers: nil)) + let post = Post(title: "title") + let createdPost = try await mutate(.create(post)) + queriedComment.setPost(createdPost) + let updateCommentWithPost = try await mutate(.update(queriedComment)) + let queriedCommentAfterUpdate = try await query(.get(Comment.self, byId: updateCommentWithPost.id))! + assertLazyReference(queriedCommentAfterUpdate._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id)])) + let queriedCommentWithPost = try await query(.get(Comment.self, byId: queriedCommentAfterUpdate.id, includes: { comment in [comment.post]}))! + assertLazyReference(queriedCommentWithPost._post, state: .loaded(model: createdPost)) + } + + func testUpdateToNewPost() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + try await mutate(.create(post)) + try await mutate(.create(comment)) + var queriedComment = try await query(.get(Comment.self, byId: comment.id))! + assertLazyReference(queriedComment._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id)])) + + let newPost = Post(title: "title") + let createdNewPost = try await mutate(.create(newPost)) + queriedComment.setPost(newPost) + let updateCommentWithPost = try await mutate(.update(queriedComment)) + let queriedCommentAfterUpdate = try await query(.get(Comment.self, byId: updateCommentWithPost.id))! + assertLazyReference(queriedCommentAfterUpdate._post, state: .notLoaded(identifiers: [.init(name: "id", value: newPost.id)])) + let queriedCommentWithPost = try await query(.get(Comment.self, byId: queriedCommentAfterUpdate.id, includes: { comment in [comment.post]}))! + assertLazyReference(queriedCommentWithPost._post, state: .loaded(model: createdNewPost)) + } + + func testUpdateRemovePost() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + try await mutate(.create(post)) + try await mutate(.create(comment)) + var queriedComment = try await query(.get(Comment.self, byId: comment.id))! + assertLazyReference(queriedComment._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id)])) + + queriedComment.setPost(nil) + let updateCommentRemovePost = try await mutate(.update(queriedComment)) + let queriedCommentAfterUpdate = try await query(.get(Comment.self, byId: updateCommentRemovePost.id))! + assertLazyReference(queriedCommentAfterUpdate._post, state: .notLoaded(identifiers: nil)) + } + + func testDelete() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + let createdPost = try await mutate(.create(post)) + try await mutate(.create(comment)) + + try await mutate(.delete(createdPost)) + let queriedPost = try await query(.get(Post.self, byId: post.id)) + XCTAssertNil(queriedPost) + let queriedComment = try await query(.get(Comment.self, byId: comment.id))! + assertLazyReference(queriedComment._post, state: .notLoaded(identifiers: nil)) + try await mutate(.delete(queriedComment)) + let queryDeletedComment = try await query(.get(Comment.self, byId: comment.id)) + XCTAssertNil(queryDeletedComment) + } + + func testSubscribeToComments() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + try await mutate(.create(post)) + let connected = expectation(description: "subscription connected") + let onCreatedComment = expectation(description: "onCreatedComment received") + let subscription = Amplify.API.subscribe(request: .subscription(of: Comment.self, type: .onCreate)) + Task { + do { + for try await subscriptionEvent in subscription { + switch subscriptionEvent { + case .connection(let subscriptionConnectionState): + log.verbose("Subscription connect state is \(subscriptionConnectionState)") + if case .connected = subscriptionConnectionState { + connected.fulfill() + } + case .data(let result): + switch result { + case .success(let createdComment): + log.verbose("Successfully got createdComment from subscription: \(createdComment)") + assertLazyReference(createdComment._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id)])) + onCreatedComment.fulfill() + case .failure(let error): + XCTFail("Got failed result with \(error.errorDescription)") + } + } + } + } catch { + XCTFail("Subscription has terminated with \(error)") + } + } + + await fulfillment(of: [connected], timeout: 10) + let comment = Comment(content: "content", post: post) + try await mutate(.create(comment)) + await fulfillment(of: [onCreatedComment], timeout: 10) + subscription.cancel() + } + + // The identical `includes` parameter should be used because the selection set of the mutation + // has to match the selection set of the subscription. + func testSubscribeToCommentsIncludesPost() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + try await mutate(.create(post)) + let connected = expectation(description: "subscription connected") + let onCreatedComment = expectation(description: "onCreatedComment received") + let subscriptionIncludes = Amplify.API.subscribe(request: .subscription(of: Comment.self, + type: .onCreate, + includes: { comment in [comment.post]})) + Task { + do { + for try await subscriptionEvent in subscriptionIncludes { + switch subscriptionEvent { + case .connection(let subscriptionConnectionState): + log.verbose("Subscription connect state is \(subscriptionConnectionState)") + if case .connected = subscriptionConnectionState { + connected.fulfill() + } + case .data(let result): + switch result { + case .success(let createdComment): + log.verbose("Successfully got createdComment from subscription: \(createdComment)") + assertLazyReference(createdComment._post, state: .loaded(model: post)) + onCreatedComment.fulfill() + case .failure(let error): + XCTFail("Got failed result with \(error.errorDescription)") + } + } + } + } catch { + XCTFail("Subscription has terminated with \(error)") + } + } + + await fulfillment(of: [connected], timeout: 20) + let comment = Comment(content: "content", post: post) + try await mutate(.create(comment, includes: { comment in [comment.post] })) + await fulfillment(of: [onCreatedComment], timeout: 20) + subscriptionIncludes.cancel() + } + + func testSubscribeToPosts() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + + let connected = expectation(description: "subscription connected") + let onCreatedPost = expectation(description: "onCreatedPost received") + let subscription = Amplify.API.subscribe(request: .subscription(of: Post.self, type: .onCreate)) + Task { + do { + for try await subscriptionEvent in subscription { + switch subscriptionEvent { + case .connection(let subscriptionConnectionState): + log.verbose("Subscription connect state is \(subscriptionConnectionState)") + if case .connected = subscriptionConnectionState { + connected.fulfill() + } + case .data(let result): + switch result { + case .success(let createdPost): + log.verbose("Successfully got createdPost from subscription: \(createdPost)") + assertList(createdPost.comments!, state: .isNotLoaded(associatedIdentifiers: [post.id], associatedFields: ["post"])) + onCreatedPost.fulfill() + case .failure(let error): + XCTFail("Got failed result with \(error.errorDescription)") + } + } + } + } catch { + XCTFail("Subscription has terminated with \(error)") + } + } + + await fulfillment(of: [connected], timeout: 10) + try await mutate(.create(post)) + await fulfillment(of: [onCreatedPost], timeout: 10) + subscription.cancel() + } + + func testSubscribeToPostsIncludes() async throws { + await setup(withModels: PostComment4V2Models()) + let post = Post(title: "title") + + let connected = expectation(description: "subscription connected") + let onCreatedPost = expectation(description: "onCreatedPost received") + let subscriptionIncludes = Amplify.API.subscribe(request: .subscription(of: Post.self, + type: .onCreate, + includes: { post in [post.comments]})) + Task { + do { + for try await subscriptionEvent in subscriptionIncludes { + switch subscriptionEvent { + case .connection(let subscriptionConnectionState): + log.verbose("Subscription connect state is \(subscriptionConnectionState)") + if case .connected = subscriptionConnectionState { + connected.fulfill() + } + case .data(let result): + switch result { + case .success(let createdPost): + log.verbose("Successfully got createdPost from subscription: \(createdPost)") + assertList(createdPost.comments!, state: .isLoaded(count: 0)) + onCreatedPost.fulfill() + case .failure(let error): + XCTFail("Got failed result with \(error.errorDescription)") + } + } + } + } catch { + XCTFail("Subscription has terminated with \(error)") + } + } + + await fulfillment(of: [connected], timeout: 10) + try await mutate(.create(post, includes: { post in [post.comments]})) + await fulfillment(of: [onCreatedPost], timeout: 10) + subscriptionIncludes.cancel() + } +} + +extension GraphQLLazyLoadPostComment4V2Tests: DefaultLogger { } + +extension GraphQLLazyLoadPostComment4V2Tests { + typealias Post = Post4V2 + typealias Comment = Comment4V2 + + struct PostComment4V2Models: AmplifyModelRegistration { + public let version: String = "version" + func registerModels(registry: ModelRegistry.Type) { + ModelRegistry.register(modelType: Post4V2.self) + ModelRegistry.register(modelType: Comment4V2.self) + } + } +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Post4V2+Schema.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Post4V2+Schema.swift new file mode 100644 index 0000000000..0bbc928091 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Post4V2+Schema.swift @@ -0,0 +1,66 @@ +// swiftlint:disable all +import Amplify +import Foundation + +extension Post4V2 { + // MARK: - CodingKeys + public enum CodingKeys: String, ModelKey { + case id + case title + case comments + case createdAt + case updatedAt + } + + public static let keys = CodingKeys.self + // MARK: - ModelSchema + + public static let schema = defineSchema { model in + let post4V2 = Post4V2.keys + + model.authRules = [ + rule(allow: .public, provider: .apiKey, operations: [.create, .update, .delete, .read]) + ] + + model.listPluralName = "Post4V2s" + model.syncPluralName = "Post4V2s" + + model.attributes( + .index(fields: ["id"], name: nil), + .primaryKey(fields: [post4V2.id]) + ) + + model.fields( + .field(post4V2.id, is: .required, ofType: .string), + .field(post4V2.title, is: .required, ofType: .string), + .hasMany(post4V2.comments, is: .optional, ofType: Comment4V2.self, associatedFields: [Comment4V2.keys.post]), + .field(post4V2.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime), + .field(post4V2.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime) + ) + } + public class Path: ModelPath { } + + public static var rootPath: PropertyContainerPath? { Path() } +} + +extension Post4V2: ModelIdentifiable { + public typealias IdentifierFormat = ModelIdentifierFormat.Default + public typealias IdentifierProtocol = DefaultModelIdentifier +} +extension ModelPath where ModelType == Post4V2 { + public var id: FieldPath { + string("id") + } + public var title: FieldPath { + string("title") + } + public var comments: ModelPath { + Comment4V2.Path(name: "comments", isCollection: true, parent: self) + } + public var createdAt: FieldPath { + datetime("createdAt") + } + public var updatedAt: FieldPath { + datetime("updatedAt") + } +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Post4V2.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Post4V2.swift new file mode 100644 index 0000000000..08f3e4b429 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL1/Post4V2.swift @@ -0,0 +1,32 @@ +// swiftlint:disable all +import Amplify +import Foundation + +public struct Post4V2: Model { + public let id: String + public var title: String + public var comments: List? + public var createdAt: Temporal.DateTime? + public var updatedAt: Temporal.DateTime? + + public init(id: String = UUID().uuidString, + title: String, + comments: List? = []) { + self.init(id: id, + title: title, + comments: comments, + createdAt: nil, + updatedAt: nil) + } + internal init(id: String = UUID().uuidString, + title: String, + comments: List? = [], + createdAt: Temporal.DateTime? = nil, + updatedAt: Temporal.DateTime? = nil) { + self.id = id + self.title = title + self.comments = comments + self.createdAt = createdAt + self.updatedAt = updatedAt + } +} \ No newline at end of file diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/CommentWithCompositeKey+Schema.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/CommentWithCompositeKey+Schema.swift new file mode 100644 index 0000000000..8e4fbcf9d2 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/CommentWithCompositeKey+Schema.swift @@ -0,0 +1,73 @@ +// swiftlint:disable all +import Amplify +import Foundation + +extension CommentWithCompositeKey { + // MARK: - CodingKeys + public enum CodingKeys: String, ModelKey { + case id + case content + case post + case createdAt + case updatedAt + } + + public static let keys = CodingKeys.self + // MARK: - ModelSchema + + public static let schema = defineSchema { model in + let commentWithCompositeKey = CommentWithCompositeKey.keys + + model.authRules = [ + rule(allow: .public, provider: .apiKey, operations: [.create, .update, .delete, .read]) + ] + + model.listPluralName = "CommentWithCompositeKeys" + model.syncPluralName = "CommentWithCompositeKeys" + + model.attributes( + .index(fields: ["id", "content"], name: nil), + .primaryKey(fields: [commentWithCompositeKey.id, commentWithCompositeKey.content]) + ) + + model.fields( + .field(commentWithCompositeKey.id, is: .required, ofType: .string), + .field(commentWithCompositeKey.content, is: .required, ofType: .string), + .belongsTo(commentWithCompositeKey.post, is: .optional, ofType: PostWithCompositeKey.self, targetNames: ["postWithCompositeKeyCommentsId", "postWithCompositeKeyCommentsTitle"]), + .field(commentWithCompositeKey.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime), + .field(commentWithCompositeKey.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime) + ) + } + public class Path: ModelPath { } + + public static var rootPath: PropertyContainerPath? { Path() } +} + +extension CommentWithCompositeKey: ModelIdentifiable { + public typealias IdentifierFormat = ModelIdentifierFormat.Custom + public typealias IdentifierProtocol = ModelIdentifier +} + +extension CommentWithCompositeKey.IdentifierProtocol { + public static func identifier(id: String, + content: String) -> Self { + .make(fields:[(name: "id", value: id), (name: "content", value: content)]) + } +} +extension ModelPath where ModelType == CommentWithCompositeKey { + public var id: FieldPath { + string("id") + } + public var content: FieldPath { + string("content") + } + public var post: ModelPath { + PostWithCompositeKey.Path(name: "post", parent: self) + } + public var createdAt: FieldPath { + datetime("createdAt") + } + public var updatedAt: FieldPath { + datetime("updatedAt") + } +} \ No newline at end of file diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/CommentWithCompositeKey.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/CommentWithCompositeKey.swift new file mode 100644 index 0000000000..2ac30be7be --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/CommentWithCompositeKey.swift @@ -0,0 +1,56 @@ +// swiftlint:disable all +import Amplify +import Foundation + +public struct CommentWithCompositeKey: Model { + public let id: String + public let content: String + internal var _post: LazyReference + public var post: PostWithCompositeKey? { + get async throws { + try await _post.get() + } + } + public var createdAt: Temporal.DateTime? + public var updatedAt: Temporal.DateTime? + + public init(id: String = UUID().uuidString, + content: String, + post: PostWithCompositeKey? = nil) { + self.init(id: id, + content: content, + post: post, + createdAt: nil, + updatedAt: nil) + } + internal init(id: String = UUID().uuidString, + content: String, + post: PostWithCompositeKey? = nil, + createdAt: Temporal.DateTime? = nil, + updatedAt: Temporal.DateTime? = nil) { + self.id = id + self.content = content + self._post = LazyReference(post) + self.createdAt = createdAt + self.updatedAt = updatedAt + } + public mutating func setPost(_ post: PostWithCompositeKey? = nil) { + self._post = LazyReference(post) + } + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decode(String.self, forKey: .id) + content = try values.decode(String.self, forKey: .content) + _post = try values.decodeIfPresent(LazyReference.self, forKey: .post) ?? LazyReference(identifiers: nil) + createdAt = try? values.decode(Temporal.DateTime?.self, forKey: .createdAt) + updatedAt = try? values.decode(Temporal.DateTime?.self, forKey: .updatedAt) + } + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(content, forKey: .content) + try container.encode(_post, forKey: .post) + try container.encode(createdAt, forKey: .createdAt) + try container.encode(updatedAt, forKey: .updatedAt) + } +} \ No newline at end of file diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/GraphQLLazyLoadPostCommentWithCompositeKeyTests.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/GraphQLLazyLoadPostCommentWithCompositeKeyTests.swift new file mode 100644 index 0000000000..f6af16533f --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/GraphQLLazyLoadPostCommentWithCompositeKeyTests.swift @@ -0,0 +1,447 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Combine +import XCTest + +@testable import Amplify +import AWSPluginsCore + +final class GraphQLLazyLoadPostCommentWithCompositeKeyTests: AWSAPIPluginGen2GraphQLBaseTest { + + func testSave() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + let savedPost = try await mutate(.create(post)) + let savedComment = try await mutate(.create(comment)) + } + + func testCommentWithLazyLoadPost() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + let createdPost = try await mutate(.create(post)) + let createdComment = try await mutate(.create(comment)) + + // The comment's post should not be loaded, since no `includes` is passed in. + // And the codegenerated swift models have the new modelPath properties. + assertLazyReference(createdComment._post, state: .notLoaded(identifiers: [.init(name: "id", value: createdPost.id), + .init(name: "title", value: createdPost.title)])) + let loadedPost = try await createdComment.post! + XCTAssertEqual(loadedPost.id, createdPost.id) + + // The loaded post should have comments that are also not loaded + let comments = loadedPost.comments! + assertList(comments, state: .isNotLoaded(associatedIdentifiers: [post.id, post.title], + associatedFields: ["post"])) + try await comments.fetch() + assertList(comments, state: .isLoaded(count: 1)) + assertLazyReference(comments.first!._post, state: .notLoaded(identifiers: [.init(name: "id", value: createdPost.id), + .init(name: "title", value: createdPost.title)])) + } + + // With `includes` on `comment.post`, the comment's post should be eager loaded. + func testCommentWithEagerLoadPost() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + let createdPost = try await mutate(.create(post)) + let createdComment = try await mutate(.create(comment, includes: { comment in [comment.post]})) + // The comment's post should be loaded, since `includes` include the post + assertLazyReference(createdComment._post, state: .loaded(model: createdPost)) + let loadedPost = try await createdComment.post! + XCTAssertEqual(loadedPost.id, post.id) + // The loaded post should have comments that are not loaded + let comments = loadedPost.comments! + assertList(comments, state: .isNotLoaded(associatedIdentifiers: [post.id, post.title], + associatedFields: ["post"])) + // load the comments + try await comments.fetch() + assertList(comments, state: .isLoaded(count: 1)) + // further nested models should not be loaded + assertLazyReference(comments.first!._post, state: .notLoaded(identifiers: [.init(name: "id", value: createdPost.id), + .init(name: "title", value: createdPost.title)])) + } + + // With `includes` on `comment.post.comments`, + func testCommentWithEagerLoadPostAndPostComments() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + let createdPost = try await mutate(.create(post)) + let request = GraphQLRequest.create(comment, includes: { comment in [comment.post.comments]}) + let expectedDocument = """ + mutation CreateCommentWithCompositeKey($input: CreateCommentWithCompositeKeyInput!) { + createCommentWithCompositeKey(input: $input) { + id + content + createdAt + updatedAt + post { + id + title + __typename + createdAt + updatedAt + comments { + items { + id + content + createdAt + updatedAt + post { + id + title + __typename + } + __typename + } + } + } + __typename + } + } + """ + XCTAssertEqual(request.document, expectedDocument) + let createdComment = try await mutate(request) + assertLazyReference(createdComment._post, state: .loaded(model: createdPost)) + let loadedPost = try await createdComment.post! + // The loaded post should have comments that are also loaded, since `includes` include the `post.comments` + let comments = loadedPost.comments! + assertList(comments, state: .isLoaded(count: 1)) + // further nested models should not be loaded + assertLazyReference(comments.first!._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id), + .init(name: "title", value: post.title)])) + } + + // Without `includes` and latest codegenerated types with the model path, the post's comments should be lazy loaded + func testPostWithLazyLoadComments() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + _ = try await mutate(.create(post)) + _ = try await mutate(.create(comment)) + let queriedPost = try await query(.get(Post.self, byIdentifier: .identifier(id: post.id, title: post.title)))! + let comments = queriedPost.comments! + assertList(comments, state: .isNotLoaded(associatedIdentifiers: [post.id, post.title], associatedFields: ["post"])) + try await comments.fetch() + assertList(comments, state: .isLoaded(count: 1)) + assertLazyReference(comments.first!._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id), + .init(name: "title", value: post.title)])) + } + + // With `includes` on `post.comments` should eager load the post's comments + func testPostWithEagerLoadComments() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + _ = try await mutate(.create(post)) + _ = try await mutate(.create(comment)) + let queriedPost = try await query(.get(Post.self, byIdentifier: .identifier(id: post.id, title: post.title), includes: { post in [post.comments]}))! + let comments = queriedPost.comments! + assertList(comments, state: .isLoaded(count: 1)) + assertLazyReference(comments.first!._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id), + .init(name: "title", value: post.title)])) + } + + // With `includes` on `post.comments.post` should eager load the post's comments' post + func testPostWithEagerLoadCommentsAndPost() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + let createdPost = try await mutate(.create(post)) + _ = try await mutate(.create(comment)) + let queriedPost = try await query(.get(Post.self, + byIdentifier: .identifier(id: post.id, + title: post.title), + includes: { post in [post.comments.post]}))! + let comments = queriedPost.comments! + assertList(comments, state: .isLoaded(count: 1)) + assertLazyReference(comments.first!._post, state: .loaded(model: createdPost)) + } + + func testListPostsListComments() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + try await mutate(.create(post)) + try await mutate(.create(comment)) + + let queriedPosts = try await listQuery(.list(Post.self, where: Post.keys.id == post.id)) + assertList(queriedPosts, state: .isLoaded(count: 1)) + assertList(queriedPosts.first!.comments!, + state: .isNotLoaded(associatedIdentifiers: [post.id, post.title], associatedFields: ["post"])) + + let queriedComments = try await listQuery(.list(Comment.self, where: Comment.keys.id == comment.id)) + assertList(queriedComments, state: .isLoaded(count: 1)) + assertLazyReference(queriedComments.first!._post, + state: .notLoaded(identifiers: [ + .init(name: "id", value: post.id), + .init(name: "title", value: "title")])) + } + + func testCreateWithoutPost() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let comment = Comment(content: "content") + try await mutate(.create(comment)) + var queriedComment = try await query(.get(Comment.self, byIdentifier: .identifier(id: comment.id, content: comment.content)))! + assertLazyReference(queriedComment._post, state: .notLoaded(identifiers: nil)) + let post = Post(title: "title") + let createdPost = try await mutate(.create(post)) + queriedComment.setPost(createdPost) + let updateCommentWithPost = try await mutate(.update(queriedComment)) + let queriedCommentAfterUpdate = try await query(.get(Comment.self, + byIdentifier: .identifier(id: updateCommentWithPost.id, + content: updateCommentWithPost.content)))! + assertLazyReference(queriedCommentAfterUpdate._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id), + .init(name: "title", value: post.title)])) + let queriedCommentWithPost = try await query(.get(Comment.self, + byIdentifier: .identifier(id: queriedCommentAfterUpdate.id, + content: queriedCommentAfterUpdate.content), + includes: { comment in [comment.post]}))! + assertLazyReference(queriedCommentWithPost._post, state: .loaded(model: createdPost)) + } + + func testUpdateToNewPost() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + try await mutate(.create(post)) + try await mutate(.create(comment)) + var queriedComment = try await query(.get(Comment.self, byIdentifier: .identifier(id: comment.id, content: comment.content)))! + assertLazyReference(queriedComment._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id), + .init(name: "title", value: post.title)])) + + let newPost = Post(title: "title") + let createdNewPost = try await mutate(.create(newPost)) + queriedComment.setPost(newPost) + let updateCommentWithPost = try await mutate(.update(queriedComment)) + let queriedCommentAfterUpdate = try await query(.get(Comment.self, + byIdentifier: .identifier(id: updateCommentWithPost.id, + content: updateCommentWithPost.content)))! + assertLazyReference(queriedCommentAfterUpdate._post, state: .notLoaded(identifiers: [.init(name: "id", value: newPost.id), + .init(name: "title", value: newPost.title)])) + let queriedCommentWithPost = try await query(.get(Comment.self, + byIdentifier: .identifier(id: queriedCommentAfterUpdate.id, + content: queriedCommentAfterUpdate.content), + includes: { comment in [comment.post]}))! + assertLazyReference(queriedCommentWithPost._post, state: .loaded(model: createdNewPost)) + } + + func testUpdateRemovePost() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + try await mutate(.create(post)) + try await mutate(.create(comment)) + var queriedComment = try await query(.get(Comment.self, byIdentifier: .identifier(id: comment.id, content: comment.content)))! + assertLazyReference(queriedComment._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id), + .init(name: "title", value: post.title)])) + + queriedComment.setPost(nil) + let updateCommentRemovePost = try await mutate(.update(queriedComment)) + let queriedCommentAfterUpdate = try await query(.get(Comment.self, byIdentifier: .identifier(id: updateCommentRemovePost.id, content: updateCommentRemovePost.content)))! + assertLazyReference(queriedCommentAfterUpdate._post, state: .notLoaded(identifiers: nil)) + } + + func testDelete() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + let comment = Comment(content: "content", post: post) + let createdPost = try await mutate(.create(post)) + try await mutate(.create(comment)) + + try await mutate(.delete(createdPost)) + let queriedPost = try await query(.get(Post.self, byIdentifier: .identifier(id: post.id, title: post.title))) + XCTAssertNil(queriedPost) + let queriedComment = try await query(.get(Comment.self, byIdentifier: .identifier(id: comment.id, content: comment.content)))! + assertLazyReference(queriedComment._post, state: .notLoaded(identifiers: nil)) + try await mutate(.delete(queriedComment)) + let queryDeletedComment = try await query(.get(Comment.self, byIdentifier: .identifier(id: comment.id, content: comment.content))) + XCTAssertNil(queryDeletedComment) + } + + func testSubscribeToComments() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + try await mutate(.create(post)) + let connected = expectation(description: "subscription connected") + let onCreatedComment = expectation(description: "onCreatedComment received") + let subscription = Amplify.API.subscribe(request: .subscription(of: Comment.self, type: .onCreate)) + Task { + do { + for try await subscriptionEvent in subscription { + switch subscriptionEvent { + case .connection(let subscriptionConnectionState): + log.verbose("Subscription connect state is \(subscriptionConnectionState)") + if case .connected = subscriptionConnectionState { + connected.fulfill() + } + case .data(let result): + switch result { + case .success(let createdComment): + log.verbose("Successfully got createdComment from subscription: \(createdComment)") + assertLazyReference(createdComment._post, state: .notLoaded(identifiers: [.init(name: "id", value: post.id), + .init(name: "title", value: post.title)])) + onCreatedComment.fulfill() + case .failure(let error): + XCTFail("Got failed result with \(error.errorDescription)") + } + } + } + } catch { + XCTFail("Subscription has terminated with \(error)") + } + } + + await fulfillment(of: [connected], timeout: 10) + let comment = Comment(content: "content", post: post) + try await mutate(.create(comment)) + await fulfillment(of: [onCreatedComment], timeout: 10) + subscription.cancel() + } + + // The identical `includes` parameter should be used because the selection set of the mutation + // has to match the selection set of the subscription. + func testSubscribeToCommentsIncludesPost() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + try await mutate(.create(post)) + let connected = expectation(description: "subscription connected") + let onCreatedComment = expectation(description: "onCreatedComment received") + let subscriptionIncludes = Amplify.API.subscribe(request: .subscription(of: Comment.self, + type: .onCreate, + includes: { comment in [comment.post]})) + Task { + do { + for try await subscriptionEvent in subscriptionIncludes { + switch subscriptionEvent { + case .connection(let subscriptionConnectionState): + log.verbose("Subscription connect state is \(subscriptionConnectionState)") + if case .connected = subscriptionConnectionState { + connected.fulfill() + } + case .data(let result): + switch result { + case .success(let createdComment): + log.verbose("Successfully got createdComment from subscription: \(createdComment)") + assertLazyReference(createdComment._post, state: .loaded(model: post)) + onCreatedComment.fulfill() + case .failure(let error): + XCTFail("Got failed result with \(error.errorDescription)") + } + } + } + } catch { + XCTFail("Subscription has terminated with \(error)") + } + } + + await fulfillment(of: [connected], timeout: 20) + let comment = Comment(content: "content", post: post) + try await mutate(.create(comment, includes: { comment in [comment.post] })) + await fulfillment(of: [onCreatedComment], timeout: 20) + subscriptionIncludes.cancel() + } + + func testSubscribeToPosts() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + + let connected = expectation(description: "subscription connected") + let onCreatedPost = expectation(description: "onCreatedPost received") + let subscription = Amplify.API.subscribe(request: .subscription(of: Post.self, type: .onCreate)) + Task { + do { + for try await subscriptionEvent in subscription { + switch subscriptionEvent { + case .connection(let subscriptionConnectionState): + log.verbose("Subscription connect state is \(subscriptionConnectionState)") + if case .connected = subscriptionConnectionState { + connected.fulfill() + } + case .data(let result): + switch result { + case .success(let createdPost): + log.verbose("Successfully got createdPost from subscription: \(createdPost)") + assertList(createdPost.comments!, state: .isNotLoaded(associatedIdentifiers: [post.id, post.title], associatedFields: ["post"])) + onCreatedPost.fulfill() + case .failure(let error): + XCTFail("Got failed result with \(error.errorDescription)") + } + } + } + } catch { + XCTFail("Subscription has terminated with \(error)") + } + } + + await fulfillment(of: [connected], timeout: 10) + try await mutate(.create(post)) + await fulfillment(of: [onCreatedPost], timeout: 10) + subscription.cancel() + } + + func testSubscribeToPostsIncludes() async throws { + await setup(withModels: PostCommentWithCompositeKeyModels()) + let post = Post(title: "title") + + let connected = expectation(description: "subscription connected") + let onCreatedPost = expectation(description: "onCreatedPost received") + let subscriptionIncludes = Amplify.API.subscribe(request: .subscription(of: Post.self, + type: .onCreate, + includes: { post in [post.comments]})) + Task { + do { + for try await subscriptionEvent in subscriptionIncludes { + switch subscriptionEvent { + case .connection(let subscriptionConnectionState): + log.verbose("Subscription connect state is \(subscriptionConnectionState)") + if case .connected = subscriptionConnectionState { + connected.fulfill() + } + case .data(let result): + switch result { + case .success(let createdPost): + log.verbose("Successfully got createdPost from subscription: \(createdPost)") + assertList(createdPost.comments!, state: .isLoaded(count: 0)) + onCreatedPost.fulfill() + case .failure(let error): + XCTFail("Got failed result with \(error.errorDescription)") + } + } + } + } catch { + XCTFail("Subscription has terminated with \(error)") + } + } + + await fulfillment(of: [connected], timeout: 10) + try await mutate(.create(post, includes: { post in [post.comments]})) + await fulfillment(of: [onCreatedPost], timeout: 10) + subscriptionIncludes.cancel() + } +} + +extension GraphQLLazyLoadPostCommentWithCompositeKeyTests: DefaultLogger { } + +extension GraphQLLazyLoadPostCommentWithCompositeKeyTests { + typealias Post = PostWithCompositeKey + typealias Comment = CommentWithCompositeKey + + struct PostCommentWithCompositeKeyModels: AmplifyModelRegistration { + public let version: String = "version" + func registerModels(registry: ModelRegistry.Type) { + ModelRegistry.register(modelType: PostWithCompositeKey.self) + ModelRegistry.register(modelType: CommentWithCompositeKey.self) + } + } +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/PostWithCompositeKey+Schema.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/PostWithCompositeKey+Schema.swift new file mode 100644 index 0000000000..a11b35aa0d --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/PostWithCompositeKey+Schema.swift @@ -0,0 +1,73 @@ +// swiftlint:disable all +import Amplify +import Foundation + +extension PostWithCompositeKey { + // MARK: - CodingKeys + public enum CodingKeys: String, ModelKey { + case id + case title + case comments + case createdAt + case updatedAt + } + + public static let keys = CodingKeys.self + // MARK: - ModelSchema + + public static let schema = defineSchema { model in + let postWithCompositeKey = PostWithCompositeKey.keys + + model.authRules = [ + rule(allow: .public, provider: .apiKey, operations: [.create, .update, .delete, .read]) + ] + + model.listPluralName = "PostWithCompositeKeys" + model.syncPluralName = "PostWithCompositeKeys" + + model.attributes( + .index(fields: ["id", "title"], name: nil), + .primaryKey(fields: [postWithCompositeKey.id, postWithCompositeKey.title]) + ) + + model.fields( + .field(postWithCompositeKey.id, is: .required, ofType: .string), + .field(postWithCompositeKey.title, is: .required, ofType: .string), + .hasMany(postWithCompositeKey.comments, is: .optional, ofType: CommentWithCompositeKey.self, associatedWith: CommentWithCompositeKey.keys.post), + .field(postWithCompositeKey.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime), + .field(postWithCompositeKey.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime) + ) + } + public class Path: ModelPath { } + + public static var rootPath: PropertyContainerPath? { Path() } +} + +extension PostWithCompositeKey: ModelIdentifiable { + public typealias IdentifierFormat = ModelIdentifierFormat.Custom + public typealias IdentifierProtocol = ModelIdentifier +} + +extension PostWithCompositeKey.IdentifierProtocol { + public static func identifier(id: String, + title: String) -> Self { + .make(fields:[(name: "id", value: id), (name: "title", value: title)]) + } +} +extension ModelPath where ModelType == PostWithCompositeKey { + public var id: FieldPath { + string("id") + } + public var title: FieldPath { + string("title") + } + public var comments: ModelPath { + CommentWithCompositeKey.Path(name: "comments", isCollection: true, parent: self) + } + public var createdAt: FieldPath { + datetime("createdAt") + } + public var updatedAt: FieldPath { + datetime("updatedAt") + } +} \ No newline at end of file diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/PostWithCompositeKey.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/PostWithCompositeKey.swift new file mode 100644 index 0000000000..e9de7de986 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/LL3/PostWithCompositeKey.swift @@ -0,0 +1,32 @@ +// swiftlint:disable all +import Amplify +import Foundation + +public struct PostWithCompositeKey: Model { + public let id: String + public let title: String + public var comments: List? + public var createdAt: Temporal.DateTime? + public var updatedAt: Temporal.DateTime? + + public init(id: String = UUID().uuidString, + title: String, + comments: List? = []) { + self.init(id: id, + title: title, + comments: comments, + createdAt: nil, + updatedAt: nil) + } + internal init(id: String = UUID().uuidString, + title: String, + comments: List? = [], + createdAt: Temporal.DateTime? = nil, + updatedAt: Temporal.DateTime? = nil) { + self.id = id + self.title = title + self.comments = comments + self.createdAt = createdAt + self.updatedAt = updatedAt + } +} \ No newline at end of file diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/README.md b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/README.md new file mode 100644 index 0000000000..033e2c4ea2 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/README.md @@ -0,0 +1,138 @@ +## Schema: AWSAPIPluginGen2GraphQLTests + +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.15.0", + "@aws-amplify/backend-cli": "^0.15.0", + "aws-cdk": "^2.139.0", + "aws-cdk-lib": "^2.139.0", + "constructs": "^10.3.0", + "esbuild": "^0.20.2", + "tsx": "^4.7.3", + "typescript": "^5.4.5" + }, + "dependencies": { + "aws-amplify": "^6.2.0" + }, +} +``` + +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({ + //# LL.1. Explicit Bi-Directional Belongs-to Has-many PostComment4V2 + //# 11 Explicit Bi-Directional Belongs to Relationship + Post4V2: a.model({ + id: a.id().required(), // You can omit this + title: a.string().required(), + comments: a.hasMany('Comment4V2', 'postID') + }) + .authorization(allow => [allow.publicApiKey()]), + Comment4V2: a.model({ + id: a.id().required(), // You can omit this + postID: a.id(), + content: a.string().required(), + post: a.belongsTo('Post4V2', 'postID') + }) + .authorization(allow => [allow.publicApiKey()]), + + //# LL.3. Has-Many/Belongs-To With Composite Key + //# iOS.7. A Has-Many/Belongs-To relationship, each with a composite key + //# Post with `id` and `title`, Comment with `id` and `content` + PostWithCompositeKey: a + .model({ + id: a.id().required(), + title: a.string().required(), + comments: a.hasMany("CommentWithCompositeKey", []), + }) + .identifier(["id", "title"]) + .authorization((allow) => [allow.publicApiKey()]), + + CommentWithCompositeKey: a + .model({ + id: a.id().required(), + content: a.string().required(), + post: a.belongsTo("PostWithCompositeKey", []), + }) + .identifier(["id", "content"]) + .authorization((allow) => [allow.publicApiKey()]), +}); + +``` + +3. 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 --profile [PROFILE] +``` + +5. Copy `amplify_outputs.json` to a new file named `Gen2GraphQLTests-amplify_outputs.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/` + +```perl +cp amplify_outputs.json ~/.aws-amplify/amplify-ios/testconfiguration/Gen2GraphQLTests-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 --out=models --profile lawmicha +``` + + +### 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 outputs --branch main --app-id [APP_ID] --profile [AWS_PROFILE] +``` + +8. (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] +``` diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/TestConfigHelper.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/TestConfigHelper.swift new file mode 100644 index 0000000000..6818b3b76b --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/TestConfigHelper.swift @@ -0,0 +1,32 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +@_spi(InternalAmplifyConfiguration) @testable import Amplify + +class TestConfigHelper { + + static func retrieveAmplifyOutputsData(forResource: String) throws -> AmplifyOutputsData { + let data = try retrieve(forResource: forResource) + return try AmplifyOutputsData.decodeAmplifyOutputsData(from: data) + } + + static func retrieve(forResource: String) throws -> Data { + guard let path = Bundle(for: self).path(forResource: forResource, ofType: "json") else { + throw "Could not retrieve configuration file: \(forResource)" + } + + let url = URL(fileURLWithPath: path) + return try Data(contentsOf: url) + } +} + +extension String { + var withUUID: String { + "\(self)-\(UUID().uuidString)" + } +} From 9fa521ffc9cc6cb9390760f36182db63795e82b6 Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Tue, 14 May 2024 11:45:09 -0500 Subject: [PATCH 77/86] fix(predictions): update liveness websocket send event to be synchronous (#3693) --- .../Liveness/Service/FaceLivenessSession.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift index fa0ca0df9a..dfd57c1b1b 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift @@ -84,7 +84,7 @@ public final class FaceLivenessSession: LivenessService { _ event: LivenessEvent, eventDate: @escaping () -> Date = Date.init ) { - livenessServiceDispatchQueue.async { + livenessServiceDispatchQueue.sync { let encodedPayload = self.eventStreamEncoder.encode( payload: event.payload, headers: [ From e9144eddcbbe190e971cb0076185aead2effd68f Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Tue, 14 May 2024 20:44:33 +0000 Subject: [PATCH 78/86] chore: release 2.33.5 [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 8fa1912171..594c61db18 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.33.4" + public static let amplifyVersion = "2.33.5" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From b3784993fad05645798c274e3e58fa816e9eca79 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Tue, 14 May 2024 20:46:20 +0000 Subject: [PATCH 79/86] chore: finalize release 2.33.5 [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c790f6f67..5d6214eaad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.33.5 (2024-05-14) + +### Bug Fixes + +- **predictions**: update liveness websocket send event to be synchronous (#3693) + ## 2.33.4 (2024-05-08) ### Bug Fixes From 89e86a0bcd82a1568409216de87ceffd8604db21 Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Wed, 15 May 2024 10:56:49 -0500 Subject: [PATCH 80/86] chore(predictions): update liveness websocket queue QoS (#3698) --- .../Liveness/Service/FaceLivenessSession.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift index dfd57c1b1b..ffc2a9abac 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift @@ -20,8 +20,7 @@ public final class FaceLivenessSession: LivenessService { private let livenessServiceDispatchQueue = DispatchQueue( label: "com.amazon.aws.amplify.liveness.service", - target: .global() - ) + qos: .userInteractive) init( websocket: WebSocketSession, From 72a738e4221578ce1c943d853154d7ac2452f9fa Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 15 May 2024 17:54:51 +0000 Subject: [PATCH 81/86] chore: release 2.33.6 [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 594c61db18..a0ddc97132 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.33.5" + public static let amplifyVersion = "2.33.6" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From 6c73bf77e8df9634b311de325927610188261ab4 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Wed, 15 May 2024 17:56:44 +0000 Subject: [PATCH 82/86] chore: finalize release 2.33.6 [skip ci] --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d6214eaad..3c33c1df4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## 2.33.6 (2024-05-15) + ## 2.33.5 (2024-05-14) ### Bug Fixes From c04ae942a9f505e822ffc51dc2858de53ea40296 Mon Sep 17 00:00:00 2001 From: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> Date: Wed, 15 May 2024 14:29:36 -0400 Subject: [PATCH 83/86] chore: Disabling patch status check [skip ci] (#3702) --- .codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.codecov.yml b/.codecov.yml index b36f658506..79188b8fad 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -3,6 +3,7 @@ codecov: coverage: status: + patch: off project: default: threshold: 1% From 8bdfb671654dfedc653610a3a49f1376ea21623d Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Thu, 16 May 2024 10:12:48 -0500 Subject: [PATCH 84/86] chore(core): resolve swift 6 errors in dev menu code (#3692) --- Amplify/DevMenu/AmplifyDevMenu.swift | 1 + AmplifyTests/DevMenuTests/GestureRecognizerTests.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Amplify/DevMenu/AmplifyDevMenu.swift b/Amplify/DevMenu/AmplifyDevMenu.swift index 796027de86..b54767e3db 100644 --- a/Amplify/DevMenu/AmplifyDevMenu.swift +++ b/Amplify/DevMenu/AmplifyDevMenu.swift @@ -12,6 +12,7 @@ import UIKit /// Presents a developer menu using the provided `DevMenuPresentationContextProvider` /// upon notification from a `TriggerRecognizer`. Default recognizer is a `LongPressGestureRecognizer` +@MainActor public final class AmplifyDevMenu: DevMenuBehavior, TriggerDelegate { weak var devMenuPresentationContextProvider: DevMenuPresentationContextProvider? diff --git a/AmplifyTests/DevMenuTests/GestureRecognizerTests.swift b/AmplifyTests/DevMenuTests/GestureRecognizerTests.swift index 72dad242cf..5c82b10d6f 100644 --- a/AmplifyTests/DevMenuTests/GestureRecognizerTests.swift +++ b/AmplifyTests/DevMenuTests/GestureRecognizerTests.swift @@ -22,6 +22,7 @@ class GestureRecognizerTests: XCTestCase { /// - Then: /// - It should return true + @MainActor func testGestureRecognizerAddedToWindow() { let contextProvider = MockDevMenuContextProvider() let longPressGestureRecognizer = From 19367e01e24ba1ad2a8628712bd4df0091c74d60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 12:44:39 -0700 Subject: [PATCH 85/86] chore(deps): bump rexml from 3.2.5 to 3.2.8 (#3704) Bumps [rexml](https://github.com/ruby/rexml) from 3.2.5 to 3.2.8. - [Release notes](https://github.com/ruby/rexml/releases) - [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md) - [Commits](https://github.com/ruby/rexml/compare/v3.2.5...v3.2.8) --- updated-dependencies: - dependency-name: rexml dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 41b693fbca..b8a900ce0d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -249,7 +249,8 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.5) + rexml (3.2.8) + strscan (>= 3.0.9) rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) @@ -267,6 +268,7 @@ GEM naturally sqlite3 (1.6.0) mini_portile2 (~> 2.8.0) + strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) From d6d5a23b780f7bdabfc53baed8772c08b3cb2190 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 13:08:18 -0700 Subject: [PATCH 86/86] chore(deps): bump rexml from 3.2.5 to 3.2.8 in /canaries/example (#3706) Bumps [rexml](https://github.com/ruby/rexml) from 3.2.5 to 3.2.8. - [Release notes](https://github.com/ruby/rexml/releases) - [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md) - [Commits](https://github.com/ruby/rexml/compare/v3.2.5...v3.2.8) --- updated-dependencies: - dependency-name: rexml dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- canaries/example/Gemfile.lock | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/canaries/example/Gemfile.lock b/canaries/example/Gemfile.lock index f9a653355b..3b9e88f59c 100644 --- a/canaries/example/Gemfile.lock +++ b/canaries/example/Gemfile.lock @@ -168,7 +168,8 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.5) + rexml (3.2.8) + strscan (>= 3.0.9) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -181,6 +182,7 @@ GEM simctl (1.6.8) CFPropertyList naturally + strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) @@ -213,6 +215,7 @@ PLATFORMS universal-darwin-20 x86_64-darwin-19 x86_64-darwin-21 + x86_64-linux DEPENDENCIES fastlane