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] 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) + } +} +