diff --git a/.github/workflows/ui_tests.yml b/.github/workflows/ui_tests.yml new file mode 100644 index 0000000..e59d0b5 --- /dev/null +++ b/.github/workflows/ui_tests.yml @@ -0,0 +1,23 @@ +name: Run UI Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + ui-test-ios: + runs-on: macos-15 + steps: + - name: Checkout + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + + - name: Resolve and update Swift packages + run: xcodebuild -resolvePackageDependencies -scheme Authenticator + + - name: UI test Authenticator on iOS + working-directory: Tests/AuthenticatorHostApp + run: | + xcodebuild -resolvePackageDependencies -scheme Authenticator + xcodebuild test -scheme AuthenticatorHostApp -sdk 'iphonesimulator' -destination 'platform=iOS Simulator,name=iPhone 16 Pro Max,OS=latest' -derivedDataPath Build/ -clonedSourcePackagesDirPath ~/Library/Developer/Xcode/DerivedData/Authenticator | xcpretty --simple --color --report junit && exit ${PIPESTATUS[0]} diff --git a/.gitignore b/.gitignore index db16711..b175364 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,5 @@ amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig -.secret-* \ No newline at end of file +.secret-* +Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/CHANGELOG.md b/CHANGELOG.md index 62660ff..b08ce6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.2.0 (2024-10-31) + +### Feature +- **Authenticator**: Adding support for Email MFA (#96) + ## 1.1.8 (2024-09-20) ### Bug Fixes diff --git a/Package.resolved b/Package.resolved index 93b4449..9247b1d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/aws-amplify/amplify-swift", "state" : { - "revision" : "dbc4a0412f4b5cd96f3e756e78bbd1e8e0a35a2f", - "version" : "2.35.4" + "revision" : "aef29d1665f9fad1c88fa6a781b8c847913dd7c6", + "version" : "2.44.0" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/awslabs/aws-crt-swift", "state" : { - "revision" : "0d0a0cf2e2cb780ceeceac190b4ede94f4f96902", - "version" : "0.26.0" + "revision" : "7b42e0343f28b3451aab20840dc670abd12790bd", + "version" : "0.36.0" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/awslabs/aws-sdk-swift.git", "state" : { - "revision" : "47922c05dd66be717c7bce424651a534456717b7", - "version" : "0.36.2" + "revision" : "828358a2c39d138325b0f87a2d813f4b972e5f4f", + "version" : "1.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/smithy-lang/smithy-swift", "state" : { - "revision" : "8a5b0105c1b8a1d26a9435fb0af3959a7f5de578", - "version" : "0.41.1" + "revision" : "0ed3440f8c41e27a0937364d5035d2d4fefb8aa3", + "version" : "0.71.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", - "version" : "1.5.4" + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" } } ], diff --git a/Package.swift b/Package.swift index aeabb9f..5fb8291 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( targets: ["Authenticator"]), ], dependencies: [ - .package(url: "https://github.com/aws-amplify/amplify-swift", from: "2.35.0"), + .package(url: "https://github.com/aws-amplify/amplify-swift", from: "2.44.0"), ], targets: [ .target( diff --git a/Sources/Authenticator/Authenticator.swift b/Sources/Authenticator/Authenticator.swift index f1e5e7e..e41d78f 100644 --- a/Sources/Authenticator/Authenticator.swift +++ b/Sources/Authenticator/Authenticator.swift @@ -13,8 +13,11 @@ public struct Authenticator = .weakObjects() private let loadingContent: LoadingContent private let signInContent: SignInContent - private let confirmSignInContentWithMFACodeContent: ConfirmSignInWithMFACodeContent + private let confirmSignInWithMFACodeContent: ConfirmSignInWithMFACodeContent + private let confirmSignInWithOTPContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithOTPContent private let confirmSignInWithTOTPCodeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithTOTPCodeContent private let continueSignInWithMFASelectionContent: (ContinueSignInWithMFASelectionState) -> ContinueSignInWithMFASelectionContent + private let continueSignInWithMFASetupSelectionContent: (ContinueSignInWithMFASetupSelectionState) -> ContinueSignInWithMFASetupSelectionContent private let continueSignInWithTOTPSetupContent: (ContinueSignInWithTOTPSetupState) -> ContinueSignInWithTOTPSetupContent + private let continueSignInWithEmailMFASetupContent: (ContinueSignInWithEmailMFASetupState) -> ContinueSignInWithEmailMFASetupContent private let confirmSignInContentWithCustomChallengeContent: ConfirmSignInWithCustomChallengeContent private let confirmSignInContentWithNewPasswordContent: ConfirmSignInWithNewPasswordContent private let signUpContent: SignUpContent @@ -65,12 +71,18 @@ public struct Authenticator ConfirmSignInWithMFACodeContent = { state in ConfirmSignInWithMFACodeView(state: state) }, + @ViewBuilder confirmSignInWithOTPContent: @escaping (ConfirmSignInWithCodeState) -> ConfirmSignInWithOTPContent = { state in + ConfirmSignInWithOTPView(state: state) + }, @ViewBuilder confirmSignInWithTOTPCodeContent: @escaping (ConfirmSignInWithCodeState) -> ConfirmSignInWithTOTPCodeContent = { state in ConfirmSignInWithTOTPView(state: state) }, @ViewBuilder continueSignInWithMFASelectionContent: @escaping (ContinueSignInWithMFASelectionState) -> ContinueSignInWithMFASelectionContent = { state in ContinueSignInWithMFASelectionView(state: state) }, + @ViewBuilder continueSignInWithMFASetupSelectionContent: @escaping (ContinueSignInWithMFASetupSelectionState) -> ContinueSignInWithMFASetupSelectionContent = { state in + ContinueSignInWithMFASetupSelectionView(state: state) + }, @ViewBuilder continueSignInWithTOTPSetupContent: @escaping (ContinueSignInWithTOTPSetupState) -> ContinueSignInWithTOTPSetupContent = { state in ContinueSignInWithTOTPSetupView(state: state) }, + @ViewBuilder continueSignInWithEmailMFASetupContent: @escaping (ContinueSignInWithEmailMFASetupState) -> ContinueSignInWithEmailMFASetupContent = { state in + ContinueSignInWithEmailMFASetupView(state: state) + }, @ViewBuilder confirmSignInWithCustomChallengeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithCustomChallengeContent = { state in ConfirmSignInWithCustomChallengeView(state: state) }, @@ -157,13 +178,15 @@ public struct Authenticator ContinueSignInWithEmailMFASetupState { + return .init(credentials: .init()) + } + + /// Returns an empty and no-op ``ContinueSignInWithMFASetupSelectionState``. + /// - Parameter allowedMFATypes: The ``AllowedMFATypes`` associated with this state + public static func continueSignInWithMFASetupSelection( + allowedMFATypes: AllowedMFATypes + ) -> ContinueSignInWithMFASetupSelectionState { + return .init( + authenticatorState: .empty, + allowedMFATypes: allowedMFATypes + ) + } + } } diff --git a/Sources/Authenticator/Views/ConfirmSignInWithCustomChallengeView.swift b/Sources/Authenticator/Views/ConfirmSignInWithCustomChallengeView.swift index a56daf9..13dd8fb 100644 --- a/Sources/Authenticator/Views/ConfirmSignInWithCustomChallengeView.swift +++ b/Sources/Authenticator/Views/ConfirmSignInWithCustomChallengeView.swift @@ -31,8 +31,7 @@ public struct ConfirmSignInWithCustomChallengeView: View { + @Environment(\.authenticatorState) private var authenticatorState + @ObservedObject private var state: ConfirmSignInWithCodeState + private let content: ConfirmSignInWithCodeView + + /// Creates a `ConfirmSignInWithOTPView` + /// - Parameter state: The ``ConfirmSignInWithCodeState`` that is observed by this view + /// - Parameter headerContent: The content displayed above the fields. Defaults to ``ConfirmSignInWithOTPHeader`` + /// - Parameter footerContent: The content displayed bellow the fields. Defaults to ``ConfirmSignInWithOTPFooter`` + public init( + state: ConfirmSignInWithCodeState, + @ViewBuilder headerContent: () -> Header = { + ConfirmSignInWithOTPHeader() + }, + @ViewBuilder footerContent: () -> Footer = { + ConfirmSignInWithOTPFooter() + } + ) { + self.state = state + self.content = ConfirmSignInWithCodeView( + state: state, + headerContent: headerContent, + footerContent: footerContent + ) + } + + public var body: some View { + content + .onAppear { + state.message = .info( + message: state.localizedMessage(for: state.deliveryDetails) + ) + } + } + + /// Sets a custom error mapping function for the `AuthError`s that are displayed + /// - Parameter errorTransform: A closure that takes an `AuthError` and returns a ``AuthenticatorError`` that will be displayed. + public func errorMap(_ errorTransform: @escaping (AuthError) -> AuthenticatorError?) -> Self { + state.errorTransform = errorTransform + return self + } +} + +/// Default header for the ``ConfirmSignInWithOTPView``. It displays the view's title +public struct ConfirmSignInWithOTPHeader: View { + public init() {} + public var body: some View { + DefaultHeader( + title: "authenticator.confirmSignInWithOTP.title".localized() + ) + } +} + +/// Default footer for the ``ConfirmSignInWithOTPView``. It displays the "Back to Sign In" button +public struct ConfirmSignInWithOTPFooter: View { + @Environment(\.authenticatorState) private var authenticatorState + + public init() {} + public var body: some View { + Button("authenticator.confirmSignInWithCode.button.backToSignIn".localized()) { + authenticatorState.move(to: .signIn) + } + .buttonStyle(.link) + } +} diff --git a/Sources/Authenticator/Views/ConfirmSignInWithTOTPCodeView.swift b/Sources/Authenticator/Views/ConfirmSignInWithTOTPCodeView.swift index dd7111c..f1a4bfa 100644 --- a/Sources/Authenticator/Views/ConfirmSignInWithTOTPCodeView.swift +++ b/Sources/Authenticator/Views/ConfirmSignInWithTOTPCodeView.swift @@ -32,8 +32,7 @@ public struct ConfirmSignInWithTOTPView: View { + @Environment(\.authenticatorState) private var authenticatorState + @ObservedObject private var state: ContinueSignInWithEmailMFASetupState + @StateObject private var emailValidator: Validator + private let headerContent: Header + private let footerContent: Footer + + /// Creates a `ContinueSignInWithEmailMFASetupView` + /// - Parameter state: The ``ContinueSignInWithEmailMFASetupState`` that is observed by this view + /// - Parameter headerContent: The content displayed above the fields. Defaults to ``ContinueSignInWithEmailMFASetupHeader`` + /// - Parameter footerContent: The content displayed bellow the fields. Defaults to ``ContinueSignInWithEmailMFASetupFooter`` + public init( + state: ContinueSignInWithEmailMFASetupState, + @ViewBuilder headerContent: () -> Header = { + ContinueSignInWithEmailMFASetupHeader() + }, + @ViewBuilder footerContent: () -> Footer = { + ContinueSignInWithEmailMFASetupFooter() + } + ) { + self.headerContent = headerContent() + self.footerContent = footerContent() + self.state = state + self._emailValidator = StateObject(wrappedValue: Validator( + using: FieldValidators.email + )) + } + + private var textFieldLabel: String { + return "authenticator.continueSignInWithEmailMFASetup.field.email.label".localized() + } + + private var textFieldPlaceholder: String { + return "authenticator.field.email.placeholder".localized() + } + + private var continueButtonTitle: String { + return "authenticator.continueSignInWithEmailMFASetup.button.continue".localized() + } + + public var body: some View { + AuthenticatorView(isBusy: state.isBusy) { + headerContent + + TextField( + textFieldLabel, + text: $state.email, + placeholder: textFieldPlaceholder, + validator: emailValidator + ) +#if os(iOS) + .textContentType(.emailAddress) + .keyboardType(.emailAddress) +#endif + + Button(continueButtonTitle) { + Task { await continueSignIn() } + } + .buttonStyle(.primary) + + footerContent + } + .messageBanner($state.message) + .onSubmit { + Task { + await continueSignIn() + } + } + } + + private func continueSignIn() async { + guard emailValidator.validate() else { + log.verbose("Email validation failed") + return + } + + try? await state.continueSignIn() + } + + /// Sets a custom error mapping function for the `AuthError`s that are displayed + /// - Parameter errorTransform: A closure that takes an `AuthError` and returns a ``AuthenticatorError`` that will be displayed. + public func errorMap(_ errorTransform: @escaping (AuthError) -> AuthenticatorError?) -> Self { + state.errorTransform = errorTransform + return self + } +} + +/// Default header for the ``ContinueSignInWithEmailMFASetupView``. It displays the view's title +public struct ContinueSignInWithEmailMFASetupHeader: View { + public init() {} + public var body: some View { + DefaultHeader( + title: "authenticator.continueSignInWithEmailMFASetup.title".localized() + ) + } +} + +/// Default footer for the ``ContinueSignInWithEmailMFASetupView``. It displays the "Back to Sign In" button +public struct ContinueSignInWithEmailMFASetupFooter: View { + @Environment(\.authenticatorState) private var authenticatorState + + public init() {} + public var body: some View { + Button("authenticator.continueSignInWithEmailMFASetup.button.backToSignIn".localized()) { + authenticatorState.move(to: .signIn) + } + .buttonStyle(.link) + } +} + +extension ContinueSignInWithEmailMFASetupView: AuthenticatorLogging {} diff --git a/Sources/Authenticator/Views/ContinueSignInWithMFASelectionView.swift b/Sources/Authenticator/Views/ContinueSignInWithMFASelectionView.swift index fc47880..47101a1 100644 --- a/Sources/Authenticator/Views/ContinueSignInWithMFASelectionView.swift +++ b/Sources/Authenticator/Views/ContinueSignInWithMFASelectionView.swift @@ -13,6 +13,7 @@ public struct ContinueSignInWithMFASelectionView: View { @Environment(\.authenticatorState) private var authenticatorState @ObservedObject private var state: ContinueSignInWithMFASelectionState + @Environment(\.authenticatorTheme) private var theme private let headerContent: Header private let footerContent: Footer @@ -39,6 +40,14 @@ public struct ContinueSignInWithMFASelectionView: View { + @Environment(\.authenticatorState) private var authenticatorState + @ObservedObject private var state: ContinueSignInWithMFASetupSelectionState + + @Environment(\.authenticatorTheme) private var theme + private let headerContent: Header + private let footerContent: Footer + + /// Creates a `ContinueSignInWithMFASetupSelectionView` + /// - Parameter state: The ``ContinueSignInWithMFASetupSelectionState`` that is observed by this view + /// - Parameter headerContent: The content displayed above the fields. Defaults to ``ContinueSignInWithMFASetupSelectionHeader`` + /// - Parameter footerContent: The content displayed bellow the fields. Defaults to ``ContinueSignInWithMFASetupSelectionFooter`` + public init( + state: ContinueSignInWithMFASetupSelectionState, + @ViewBuilder headerContent: () -> Header = { + ContinueSignInWithMFASetupSelectionHeader() + }, + @ViewBuilder footerContent: () -> Footer = { + ContinueSignInWithMFASetupSelectionFooter() + } + ) { + self.state = state + self.headerContent = headerContent() + self.footerContent = footerContent() + } + + public var body: some View { + AuthenticatorView(isBusy: state.isBusy) { + headerContent + + SwiftUI.Text("authenticator.continueSignInWithMFASetupSelection.body".localized()) + .font(theme.fonts.body) + .foregroundColor(theme.colors.foreground.primary) + .accessibilityAddTraits(.isStaticText) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + + /// Only add TOTP option if it is allowed for setup selection by the service + if state.allowedMFATypes.contains(.totp) { + RadioButton( + label: "authenticator.continueSignInWithMFASelection.totp.radioButton.title".localized(), + isSelected: .constant(state.selectedMFATypeToSetup == .totp) + ) { + state.selectedMFATypeToSetup = .totp + } + .accessibilityAddTraits(state.selectedMFATypeToSetup == .totp ? .isSelected : .isButton) + .animation(.none, value: state.selectedMFATypeToSetup) + } + + /// Only add Email option if it is allowed for setup selection by the service + if state.allowedMFATypes.contains(.email) { + RadioButton( + label: "authenticator.continueSignInWithMFASetupSelection.email.radioButton.title".localized(), + isSelected: .constant(state.selectedMFATypeToSetup == .email) + ) { + state.selectedMFATypeToSetup = .email + } + .accessibilityAddTraits(state.selectedMFATypeToSetup == .email ? .isSelected : .isButton) + .animation(.none, value: state.selectedMFATypeToSetup) + } + + Button("authenticator.continueSignInWithMFASetupSelection.button.continue".localized()) { + Task { await continueSignIn() } + } + .buttonStyle(.primary) + .disabled(state.selectedMFATypeToSetup == nil) + .opacity(state.selectedMFATypeToSetup == nil ? 0.5 : 1) + + footerContent + } + .messageBanner($state.message) + .onSubmit { + Task { + await continueSignIn() + } + } + .onDisappear { + state.selectedMFATypeToSetup = nil + } + } + + private func continueSignIn() async { + try? await state.continueSignIn() + } + + /// Sets a custom error mapping function for the `AuthError`s that are displayed + /// - Parameter errorTransform: A closure that takes an `AuthError` and returns a ``AuthenticatorError`` that will be displayed. + public func errorMap(_ errorTransform: @escaping (AuthError) -> AuthenticatorError?) -> Self { + state.errorTransform = errorTransform + return self + } +} + +extension ContinueSignInWithMFASetupSelectionView: AuthenticatorLogging {} + +/// Default header for the ``ContinueSignInWithMFASetupSelectionView``. It displays the view's title +public struct ContinueSignInWithMFASetupSelectionHeader: View { + public init() {} + public var body: some View { + DefaultHeader( + title: "authenticator.continueSignInWithMFASetupSelection.title".localized() + ) + } +} + +/// Default footer for the ``ContinueSignInWithMFASetupSelectionView``. It displays the "Back to Sign In" button +public struct ContinueSignInWithMFASetupSelectionFooter: View { + @Environment(\.authenticatorState) private var authenticatorState + + public init() {} + public var body: some View { + Button("authenticator.continueSignInWithMFASetupSelection.button.backToSignIn".localized()) { + authenticatorState.move(to: .signIn) + } + .buttonStyle(.link) + } +} diff --git a/Sources/Authenticator/Views/Internal/ConfirmSignInWithCodeView.swift b/Sources/Authenticator/Views/Internal/ConfirmSignInWithCodeView.swift index 4a40737..de6eaf6 100644 --- a/Sources/Authenticator/Views/Internal/ConfirmSignInWithCodeView.swift +++ b/Sources/Authenticator/Views/Internal/ConfirmSignInWithCodeView.swift @@ -13,7 +13,6 @@ struct ConfirmSignInWithCodeView Footer = { EmptyView() }, - errorTransform: ((AuthError) -> AuthenticatorError?)? = nil, - mfaType: AuthenticatorMFAType + errorTransform: ((AuthError) -> AuthenticatorError?)? = nil ) { self.state = state self.headerContent = headerContent() @@ -34,12 +32,30 @@ struct ConfirmSignInWithCodeView AuthSignInStep { switch authUITestSignInStep { case .confirmSignInWithSMSMFACode: - return .confirmSignInWithSMSMFACode(.init(destination: .email("testEmail@test.com")), nil) + return .confirmSignInWithSMSMFACode(.init(destination: .email("111-222-3333")), nil) case .confirmSignInWithCustomChallenge: return .confirmSignInWithCustomChallenge(nil) case .confirmSignInWithNewPassword: @@ -77,7 +82,13 @@ struct AuthenticatorHostApp: App { case .continueSignInWithTOTPSetup: return .continueSignInWithTOTPSetup(.init(sharedSecret: "secret", username: "username")) case .continueSignInWithMFASelection: - return .continueSignInWithMFASelection([.totp, .sms]) + return .continueSignInWithMFASelection([.totp, .sms, .email]) + case .continueSignInWithMFASetupSelection: + return .continueSignInWithMFASetupSelection([.totp, .email]) + case .continueSignInWithEmailMFASetup: + return .continueSignInWithEmailMFASetup + case .confirmSignInWithEmailMFACode: + return .confirmSignInWithOTP(.init(destination: .email("test@amazon.com"))) case .resetPassword: return .resetPassword(nil) case .confirmSignUp: diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp/ContentView.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/ContentView.swift index 2d21e34..0f1072a 100644 --- a/Tests/AuthenticatorHostApp/AuthenticatorHostApp/ContentView.swift +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/ContentView.swift @@ -10,19 +10,71 @@ import AWSCognitoAuthPlugin import SwiftUI +enum SignInNextStepForTesting: String, CaseIterable, Identifiable { + case done = "Done" + case continueSignInWithMFASelection = "Continue with MFA Selection" + case continueSignInWithEmailMFASetup = "Continue with Email MFA Setup" + case continueSignInWithMFASetupSelection = "Continue with MFA Setup Selection" + case confirmSignInWithEmailMFACode = "Confirm with Email MFA Code" + case confirmSignInWithPhoneMFACode = "Confirm with Phone MFA Code" + case confirmSignInWithTOTP = "Confirm with TOTP" + case customAuth = "Confirm sign in with Custom Auth" + + var id: String { self.rawValue } + + func toAuthSignInStep() -> AuthSignInStep { + switch self { + case .done: + return .done + case .continueSignInWithMFASelection: + return .continueSignInWithMFASelection(.init([.sms, .email, .totp])) + case .continueSignInWithEmailMFASetup: + return .continueSignInWithEmailMFASetup + case .continueSignInWithMFASetupSelection: + return .continueSignInWithMFASetupSelection(.init([.email, .totp])) + case .confirmSignInWithEmailMFACode: + return .confirmSignInWithOTP(.init(destination: .email("h***@a***.com"))) + case .confirmSignInWithPhoneMFACode: + return .confirmSignInWithOTP(.init(destination: .phone("+11***"))) + case .confirmSignInWithTOTP: + return .confirmSignInWithTOTPCode + case .customAuth: + return .confirmSignInWithCustomChallenge(nil) + } + } +} + struct ContentView: View { + @State private var selectedStep: SignInNextStepForTesting = .done private let hidesSignUpButton: Bool private let initialStep: AuthenticatorInitialStep + private let shouldUsePickerForTestingSteps: Bool init(hidesSignUpButton: Bool, initialStep: AuthenticatorInitialStep, - authSignInStep: AuthSignInStep) { + authSignInStep: AuthSignInStep, + shouldUsePickerForTestingSteps: Bool = false) { self.hidesSignUpButton = hidesSignUpButton self.initialStep = initialStep + self.shouldUsePickerForTestingSteps = shouldUsePickerForTestingSteps MockAuthenticationService.shared.mockedSignInResult = .init(nextStep: authSignInStep) } var body: some View { + if shouldUsePickerForTestingSteps { + Picker("Next Step", selection: $selectedStep) { + ForEach(SignInNextStepForTesting.allCases) { step in + Text(step.rawValue).tag(step) + } + } + .pickerStyle(MenuPickerStyle()) + .padding() + .onChange(of: selectedStep) { newStepForTesting in + // Update MockAuthenticationService when picker selection changes + MockAuthenticationService.shared.mockedSignInResult = .init(nextStep: newStepForTesting.toAuthSignInStep()) + } + } + Authenticator(initialStep: initialStep) { state in VStack { Text("Hello, \(state.user.username)") @@ -38,7 +90,10 @@ struct ContentView: View { .onAppear { print("Appeared!") } +#if os(iOS) .statusBar(hidden: true) +#endif + } diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorUITestUtils.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorUITestUtils.swift index 4be4fca..50d0ec2 100644 --- a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorUITestUtils.swift +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorUITestUtils.swift @@ -30,6 +30,9 @@ public enum AuthUITestSignInStep: Codable { case confirmSignInWithTOTPCode case continueSignInWithTOTPSetup case continueSignInWithMFASelection + case continueSignInWithMFASetupSelection + case continueSignInWithEmailMFASetup + case confirmSignInWithEmailMFACode case resetPassword case confirmSignUp case done diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithConfirmSignUpTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithConfirmSignUpTests.swift new file mode 100644 index 0000000..0bd2fc9 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithConfirmSignUpTests.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +final class ConfirmSignInWithConfirmSignUpTests: AuthenticatorBaseTestCase { + + func testConfirmSignInWithConfirmSignUp() throws { + launchAppAndLogin(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .authSignInStep(.confirmSignUp) + ]) + assertSnapshot() + } + +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithCustomAuthChallengeViewTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithCustomAuthChallengeViewTests.swift new file mode 100644 index 0000000..29a1034 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithCustomAuthChallengeViewTests.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +final class ConfirmSignInWithCustomAuthChallengeViewTests: AuthenticatorBaseTestCase { + + func testConfirmSignInWithCustomAuthChallenge() throws { + launchAppAndLogin(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .authSignInStep(.confirmSignInWithCustomChallenge) + ]) + assertSnapshot() + } + +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithNewPasswordTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithNewPasswordTests.swift new file mode 100644 index 0000000..c8cb69e --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithNewPasswordTests.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +final class ConfirmSignInWithNewPasswordTests: AuthenticatorBaseTestCase { + + func testConfirmSignInWithNewPassword() throws { + launchAppAndLogin(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .authSignInStep(.confirmSignInWithNewPassword) + ]) + assertSnapshot() + } + +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithOTPCodeViewTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithOTPCodeViewTests.swift new file mode 100644 index 0000000..d04f5d8 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithOTPCodeViewTests.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +final class ConfirmSignInWithOTPCodeViewTests: AuthenticatorBaseTestCase { + + func testConfirmSignInWithEmail() throws { + launchAppAndLogin(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .authSignInStep(.confirmSignInWithEmailMFACode) + ]) + assertSnapshot() + } + + func testConfirmSignInWithSMS() throws { + launchAppAndLogin(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .authSignInStep(.confirmSignInWithSMSMFACode) + ]) + assertSnapshot() + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithEmailMFASetupViewTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithEmailMFASetupViewTests.swift new file mode 100644 index 0000000..9e65c84 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithEmailMFASetupViewTests.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +final class ContinueSignInWithEmailMFASetupViewTests: AuthenticatorBaseTestCase { + + func testContinueSignInWithEmailMFASetupView() throws { + launchAppAndLogin(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .authSignInStep(.continueSignInWithEmailMFASetup) + ]) + assertSnapshot() + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithMFASetupSelectionViewTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithMFASetupSelectionViewTests.swift new file mode 100644 index 0000000..a243d47 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithMFASetupSelectionViewTests.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +final class ContinueSignInWithMFASetupSelectionViewTests: AuthenticatorBaseTestCase { + + func testContinueSignInWithMFASetupSelectionView() throws { + launchAppAndLogin(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .authSignInStep(.continueSignInWithMFASetupSelection) + ]) + assertSnapshot() + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ResetPasswordViewTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ResetPasswordViewTests.swift index 1dc83fe..6610d04 100644 --- a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ResetPasswordViewTests.swift +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ResetPasswordViewTests.swift @@ -17,4 +17,13 @@ final class ResetPasswordViewTests: AuthenticatorBaseTestCase { ]) assertSnapshot() } + + func testConfirmSignInWithResetPassword() throws { + launchAppAndLogin(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .authSignInStep(.resetPassword) + ]) + assertSnapshot() + } } diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithConfirmSignUpTests/testConfirmSignInWithConfirmSignUp.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithConfirmSignUpTests/testConfirmSignInWithConfirmSignUp.1.png new file mode 100644 index 0000000..d475d31 Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithConfirmSignUpTests/testConfirmSignInWithConfirmSignUp.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithCustomAuthChallengeViewTests/testConfirmSignInWithCustomAuthChallenge.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithCustomAuthChallengeViewTests/testConfirmSignInWithCustomAuthChallenge.1.png new file mode 100644 index 0000000..898847c Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithCustomAuthChallengeViewTests/testConfirmSignInWithCustomAuthChallenge.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithNewPasswordTests/testConfirmSignInWithNewPassword.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithNewPasswordTests/testConfirmSignInWithNewPassword.1.png new file mode 100644 index 0000000..36d0112 Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithNewPasswordTests/testConfirmSignInWithNewPassword.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithOTPCodeViewTests/testConfirmSignInWithEmail.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithOTPCodeViewTests/testConfirmSignInWithEmail.1.png new file mode 100644 index 0000000..dff354d Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithOTPCodeViewTests/testConfirmSignInWithEmail.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithOTPCodeViewTests/testConfirmSignInWithSMS.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithOTPCodeViewTests/testConfirmSignInWithSMS.1.png new file mode 100644 index 0000000..b0d18e1 Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithOTPCodeViewTests/testConfirmSignInWithSMS.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithTOTPCodeViewTests/testConfirmSignInWithTOTPCodeView.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithTOTPCodeViewTests/testConfirmSignInWithTOTPCodeView.1.png index ba65d99..7ab9d61 100644 Binary files a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithTOTPCodeViewTests/testConfirmSignInWithTOTPCodeView.1.png and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithTOTPCodeViewTests/testConfirmSignInWithTOTPCodeView.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithEmailMFASetupViewTests/testContinueSignInWithEmailMFASetupView.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithEmailMFASetupViewTests/testContinueSignInWithEmailMFASetupView.1.png new file mode 100644 index 0000000..c774473 Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithEmailMFASetupViewTests/testContinueSignInWithEmailMFASetupView.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASelectionViewTests/testContinueSignInWithMFASelectionView.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASelectionViewTests/testContinueSignInWithMFASelectionView.1.png index fb8ab03..afc8878 100644 Binary files a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASelectionViewTests/testContinueSignInWithMFASelectionView.1.png and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASelectionViewTests/testContinueSignInWithMFASelectionView.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASetupSelectionViewTests/testContinueSignInWithMFASetupSelectionView.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASetupSelectionViewTests/testContinueSignInWithMFASetupSelectionView.1.png new file mode 100644 index 0000000..e499c34 Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASetupSelectionViewTests/testContinueSignInWithMFASetupSelectionView.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithTOTPSetupViewTests/testContinueSignInWithTOTPSetupView.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithTOTPSetupViewTests/testContinueSignInWithTOTPSetupView.1.png index 53f3fba..57057d6 100644 Binary files a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithTOTPSetupViewTests/testContinueSignInWithTOTPSetupView.1.png and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithTOTPSetupViewTests/testContinueSignInWithTOTPSetupView.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ResetPasswordViewTests/testConfirmSignInWithResetPassword.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ResetPasswordViewTests/testConfirmSignInWithResetPassword.1.png new file mode 100644 index 0000000..58fe6e7 Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ResetPasswordViewTests/testConfirmSignInWithResetPassword.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ResetPasswordViewTests/testResetPasswordViewWithUsernameAsPhoneNumber.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ResetPasswordViewTests/testResetPasswordViewWithUsernameAsPhoneNumber.1.png index d39e723..0d9704f 100644 Binary files a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ResetPasswordViewTests/testResetPasswordViewWithUsernameAsPhoneNumber.1.png and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ResetPasswordViewTests/testResetPasswordViewWithUsernameAsPhoneNumber.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithWithUsernameAsPhoneNumber.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithWithUsernameAsPhoneNumber.1.png index 6696a76..f01aa2f 100644 Binary files a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithWithUsernameAsPhoneNumber.1.png and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithWithUsernameAsPhoneNumber.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithoutSignUp.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithoutSignUp.1.png index 9043075..128fb16 100644 Binary files a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithoutSignUp.1.png and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithoutSignUp.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignUpViewTests/testDefaultSignUpView.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignUpViewTests/testDefaultSignUpView.1.png index a70419b..172549e 100644 Binary files a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignUpViewTests/testDefaultSignUpView.1.png and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignUpViewTests/testDefaultSignUpView.1.png differ diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/Untitled/testConfirmSignInWithCustomAuthChallenge.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/Untitled/testConfirmSignInWithCustomAuthChallenge.1.png new file mode 100644 index 0000000..898847c Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/Untitled/testConfirmSignInWithCustomAuthChallenge.1.png differ diff --git a/Tests/AuthenticatorTests/States/ConfirmSignInWithCodeStateTests.swift b/Tests/AuthenticatorTests/States/ConfirmSignInWithCodeStateTests.swift index c0037ad..20683d0 100644 --- a/Tests/AuthenticatorTests/States/ConfirmSignInWithCodeStateTests.swift +++ b/Tests/AuthenticatorTests/States/ConfirmSignInWithCodeStateTests.swift @@ -83,7 +83,7 @@ class ConfirmSignInWithCodeStateTests: XCTestCase { } } - func testDeliveryDetails_onConfirmSignInWithMFACodeStep_shouldReturnDetails() throws { + func testDeliveryDetails_onConfirmSignInWithSMSMFACodeStep_shouldReturnDetails() throws { let destination = DeliveryDestination.sms("123456789") authenticatorState.mockedStep = .confirmSignInWithMFACode(deliveryDetails: .init(destination: destination)) @@ -91,6 +91,22 @@ class ConfirmSignInWithCodeStateTests: XCTestCase { XCTAssertEqual(deliveryDetails.destination, destination) } + func testDeliveryDetails_onConfirmSignInWithEmailMFACodeStep_shouldReturnDetails() throws { + let destination = DeliveryDestination.email("test@test.com") + authenticatorState.mockedStep = .confirmSignInWithMFACode(deliveryDetails: .init(destination: destination)) + + let deliveryDetails = try XCTUnwrap(state.deliveryDetails) + XCTAssertEqual(deliveryDetails.destination, destination) + } + + func testDeliveryDetails_onConfirmSignInWithOTPCodeStep_shouldReturnDetails() throws { + let destination = DeliveryDestination.email("test@test.com") + authenticatorState.mockedStep = .confirmSignInWithOTP(deliveryDetails: .init(destination: destination)) + + let deliveryDetails = try XCTUnwrap(state.deliveryDetails) + XCTAssertEqual(deliveryDetails.destination, destination) + } + func testDeliveryDetails_onUnexpectedStep_shouldReturnNil() throws { let destination = DeliveryDestination.sms("123456789") authenticatorState.mockedStep = .confirmSignUp(deliveryDetails: .init(destination: destination)) diff --git a/Tests/AuthenticatorTests/States/ContinueSignInWithEmailMFASetupStateTests.swift b/Tests/AuthenticatorTests/States/ContinueSignInWithEmailMFASetupStateTests.swift new file mode 100644 index 0000000..66ae82e --- /dev/null +++ b/Tests/AuthenticatorTests/States/ContinueSignInWithEmailMFASetupStateTests.swift @@ -0,0 +1,87 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +@testable import Authenticator +import XCTest + +class ContinueSignInWithEmailMFASetupStateTests: XCTestCase { + private var state: ContinueSignInWithEmailMFASetupState! + private var authenticatorState: MockAuthenticatorState! + private var authenticationService: MockAuthenticationService! + + override func setUp() { + authenticatorState = MockAuthenticatorState() + state = ContinueSignInWithEmailMFASetupState(credentials: Credentials()) + state.email = "test@test.com" + + authenticationService = MockAuthenticationService() + authenticatorState.authenticationService = authenticationService + state.configure(with: authenticatorState) + } + + override func tearDown() { + state = nil + authenticatorState = nil + authenticationService = nil + } + + func testContinueSignIn_withSuccess_shouldSetNextStep() async throws { + authenticationService.mockedConfirmSignInResult = .init(nextStep: .confirmSignInWithOTP(.init(destination: .email("test@test.com")))) + authenticationService.mockedCurrentUser = MockAuthenticationService.User( + username: "username", + userId: "userId" + ) + + try await state.continueSignIn() + XCTAssertEqual(authenticationService.confirmSignInCount, 1) + XCTAssertEqual(authenticatorState.setCurrentStepCount, 1) + let currentStep = try XCTUnwrap(authenticatorState.setCurrentStepValue) + guard case .confirmSignInWithOTP = currentStep else { + XCTFail("Expected confirmSignInWithOTP, was \(currentStep)") + return + } + } + + func testContinueSignIn_withError_shouldSetErrorMessage() async throws { + do { + try await state.continueSignIn() + XCTFail("Should not succeed") + } catch { + guard let authenticatorError = error as? AuthenticatorError else { + XCTFail("Expected AuthenticatorError, was \(type(of: error))") + return + } + + let task = Task { @MainActor in + XCTAssertNotNil(state.message) + XCTAssertEqual(state.message?.content, authenticatorError.content) + } + await task.value + } + } + + func testContinueSignIn_withSuccess_andFailedToSignIn_shouldSetErrorMessage() async throws { + authenticationService.mockedConfirmSignInResult = .init(nextStep: .done) + do { + try await state.continueSignIn() + XCTFail("Should not succeed") + } catch { + XCTAssertEqual(authenticationService.confirmSignInCount, 1) + guard let authenticatorError = error as? AuthenticatorError else { + XCTFail("Expected AuthenticatorError, was \(type(of: error))") + return + } + + let task = Task { @MainActor in + XCTAssertNotNil(state.message) + XCTAssertEqual(state.message?.content, authenticatorError.content) + } + await task.value + } + } +} diff --git a/Tests/AuthenticatorTests/States/ContinueSignInWithMFASetupSelectionStateTests.swift b/Tests/AuthenticatorTests/States/ContinueSignInWithMFASetupSelectionStateTests.swift new file mode 100644 index 0000000..72e8c8e --- /dev/null +++ b/Tests/AuthenticatorTests/States/ContinueSignInWithMFASetupSelectionStateTests.swift @@ -0,0 +1,98 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +@testable import Authenticator +import XCTest + +class ContinueSignInWithMFASetupSelectionStateTests: XCTestCase { + private var state: ContinueSignInWithMFASetupSelectionState! + private var authenticatorState: MockAuthenticatorState! + private var authenticationService: MockAuthenticationService! + + override func setUp() { + authenticatorState = MockAuthenticatorState() + state = ContinueSignInWithMFASetupSelectionState( + authenticatorState: authenticatorState, + allowedMFATypes: [.sms, .totp, .email]) + state.selectedMFATypeToSetup = MFAType.email + + authenticationService = MockAuthenticationService() + authenticatorState.authenticationService = authenticationService + state.configure(with: authenticatorState) + } + + override func tearDown() { + state = nil + authenticatorState = nil + authenticationService = nil + } + + func testContinueSignIn_withSuccess_shouldSetNextStep() async throws { + authenticationService.mockedConfirmSignInResult = .init(nextStep: .continueSignInWithMFASetupSelection([.sms, .totp, .email])) + authenticationService.mockedCurrentUser = MockAuthenticationService.User( + username: "username", + userId: "userId" + ) + + try await state.continueSignIn() + XCTAssertEqual(authenticationService.confirmSignInCount, 1) + XCTAssertEqual(authenticatorState.setCurrentStepCount, 1) + let currentStep = try XCTUnwrap(authenticatorState.setCurrentStepValue) + guard case .continueSignInWithMFASetupSelection = currentStep else { + XCTFail("Expected continueSignInWithMFASetupSelection, was \(currentStep)") + return + } + } + + func testContinueSignIn_withError_shouldSetErrorMessage() async throws { + do { + try await state.continueSignIn() + XCTFail("Should not succeed") + } catch { + guard let authenticatorError = error as? AuthenticatorError else { + XCTFail("Expected AuthenticatorError, was \(type(of: error))") + return + } + + let task = Task { @MainActor in + XCTAssertNotNil(state.message) + XCTAssertEqual(state.message?.content, authenticatorError.content) + } + await task.value + } + } + + func testContinueSignIn_withSuccess_andFailedToSignIn_shouldSetErrorMessage() async throws { + authenticationService.mockedConfirmSignInResult = .init(nextStep: .done) + do { + try await state.continueSignIn() + XCTFail("Should not succeed") + } catch { + XCTAssertEqual(authenticationService.confirmSignInCount, 1) + guard let authenticatorError = error as? AuthenticatorError else { + XCTFail("Expected AuthenticatorError, was \(type(of: error))") + return + } + + let task = Task { @MainActor in + XCTAssertNotNil(state.message) + XCTAssertEqual(state.message?.content, authenticatorError.content) + } + await task.value + } + } + + func testAllowedMFATypes_onContinueSignInWithMFACodeSelection_shouldReturnDetails() throws { + + authenticatorState.mockedStep = .continueSignInWithMFASelection(allowedMFATypes: [.sms, .totp, .email]) + + let allowedMFATypes = try XCTUnwrap(state.allowedMFATypes) + XCTAssertEqual(allowedMFATypes, [.sms, .totp, .email]) + } + +}