diff --git a/Sources/Authenticator/Authenticator.swift b/Sources/Authenticator/Authenticator.swift index 094e2ea..3607022 100644 --- a/Sources/Authenticator/Authenticator.swift +++ b/Sources/Authenticator/Authenticator.swift @@ -13,6 +13,9 @@ public struct Authenticator = .weakObjects() private let loadingContent: LoadingContent private let signInContent: SignInContent private let confirmSignInContentWithMFACodeContent: ConfirmSignInWithMFACodeContent + private let confirmSignInWithTOTPCodeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithTOTPCodeContent + private let continueSignInWithMFASelectionContent: (ContinueSignInWithMFASelectionState) -> ContinueSignInWithMFASelectionContent + private let continueSignInWithTOTPSetupContent: (ContinueSignInWithTOTPSetupState) -> ContinueSignInWithTOTPSetupContent private let confirmSignInContentWithCustomChallengeContent: ConfirmSignInWithCustomChallengeContent private let confirmSignInContentWithNewPasswordContent: ConfirmSignInWithNewPasswordContent private let signUpContent: SignUpContent @@ -50,13 +57,21 @@ public struct Authenticator LoadingContent = { ProgressView() }, @@ -90,6 +106,15 @@ public struct Authenticator ConfirmSignInWithMFACodeContent = { state in ConfirmSignInWithMFACodeView(state: state) }, + @ViewBuilder confirmSignInWithTOTPCodeContent: @escaping (ConfirmSignInWithCodeState) -> ConfirmSignInWithTOTPCodeContent = { state in + ConfirmSignInWithTOTPView(state: state) + }, + @ViewBuilder continueSignInWithMFASelectionContent: @escaping (ContinueSignInWithMFASelectionState) -> ContinueSignInWithMFASelectionContent = { state in + ContinueSignInWithMFASelectionView(state: state) + }, + @ViewBuilder continueSignInWithTOTPSetupContent: @escaping (ContinueSignInWithTOTPSetupState) -> ContinueSignInWithTOTPSetupContent = { state in + ContinueSignInWithTOTPSetupView(state: state) + }, @ViewBuilder confirmSignInWithCustomChallengeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithCustomChallengeContent = { state in ConfirmSignInWithCustomChallengeView(state: state) }, @@ -122,6 +147,7 @@ public struct Authenticator SignedInContent ) { self.initialStep = initialStep + self.totpOptions = totpOptions self.loadingContent = loadingContent() let credentials = Credentials() @@ -135,6 +161,10 @@ public struct Authenticator String? { + if let issuer = issuer { + return issuer + } + log.warn("`totpOptions` not provided as part of initialization. Falling back to extract application name from Bundle.") + + if let applicationName = Bundle.main.applicationName { + return applicationName + } + log.error("Unable to extract the application name from Bundle") + return nil + } + + /// Attempts to continue the user's sign in using the provided confirmation code. + /// + /// Automatically sets the Authenticator's next step accordingly, as well as the + /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties. + /// - Throws: An `Amplify.AuthenticationError` if the operation fails + public func continueSignIn() async throws { + setBusy(true) + + do { + log.verbose("Attempting to confirm Sign In with Code") + let result = try await authenticationService.confirmSignIn( + challengeResponse: confirmationCode, + options: nil + ) + let nextStep = try await nextStep(for: result) + + setBusy(false) + + authenticatorState.setCurrentStep(nextStep) + } catch { + log.error("Confirm Sign In with Code failed") + let authenticationError = self.error(for: error) + setMessage(authenticationError) + throw authenticationError + } + } +} diff --git a/Sources/Authenticator/Theming/AuthenticatorTheme.swift b/Sources/Authenticator/Theming/AuthenticatorTheme.swift index dc22fe6..fe47642 100644 --- a/Sources/Authenticator/Theming/AuthenticatorTheme.swift +++ b/Sources/Authenticator/Theming/AuthenticatorTheme.swift @@ -167,6 +167,7 @@ extension AuthenticatorTheme.Components { public var cornerRadius: CGFloat = 0 public var borderWidth: CGFloat = 1 public var backgroundColor: SwiftUI.Color = .clear + public var qrCodeSize: CGFloat = 200 } public struct Button { @@ -180,6 +181,11 @@ extension AuthenticatorTheme.Components { cornerRadius: 0, padding: 10 ) + public var capsule = Variation( + font: Platform.isMacOS ? .body.weight(.regular) : .subheadline.weight(.regular), + cornerRadius: .infinity, + padding: .init(top: 10, bottom: 10, trailing: 30, leading: 30) + ) } public struct Field { diff --git a/Sources/Authenticator/Views/ConfirmSignInWithCustomChallengeView.swift b/Sources/Authenticator/Views/ConfirmSignInWithCustomChallengeView.swift index f697dc0..a491c29 100644 --- a/Sources/Authenticator/Views/ConfirmSignInWithCustomChallengeView.swift +++ b/Sources/Authenticator/Views/ConfirmSignInWithCustomChallengeView.swift @@ -31,7 +31,8 @@ public struct ConfirmSignInWithCustomChallengeView Header = { ConfirmSignInWithMFACodeHeader() }, @ViewBuilder footerContent: () -> Footer = { - EmptyView() + ConfirmSignInWithMFACodeFooter() } ) { self.state = state self.content = ConfirmSignInWithCodeView( state: state, headerContent: headerContent, - footerContent: footerContent + footerContent: footerContent, + mfaType: .sms ) } @@ -62,3 +63,17 @@ public struct ConfirmSignInWithMFACodeHeader: View { ) } } + +/// Default footer for the ``ConfirmSignInWithMFACodeView``. It displays the "Back to Sign In" button +public struct ConfirmSignInWithMFACodeFooter: 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 new file mode 100644 index 0000000..9afc20a --- /dev/null +++ b/Sources/Authenticator/Views/ConfirmSignInWithTOTPCodeView.swift @@ -0,0 +1,74 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +import SwiftUI + +/// Represents the content being displayed when the ``Authenticator`` is in the ``AuthenticatorStep/confirmSignInWithTOTPCode`` step. +public struct ConfirmSignInWithTOTPView: View { + @Environment(\.authenticatorState) private var authenticatorState + @ObservedObject private var state: ConfirmSignInWithCodeState + private let content: ConfirmSignInWithCodeView + + /// Creates a `ConfirmSignInWithTOTPView` + /// - Parameter state: The ``ConfirmSignInWithCodeState`` that is observed by this view + /// - Parameter headerContent: The content displayed above the fields. Defaults to ``ConfirmSignInWithTOTPHeader`` + /// - Parameter footerContent: The content displayed bellow the fields. Defaults to ``ConfirmSignInWithTOTPFooter`` + public init( + state: ConfirmSignInWithCodeState, + @ViewBuilder headerContent: () -> Header = { + ConfirmSignInWithTOTPHeader() + }, + @ViewBuilder footerContent: () -> Footer = { + ConfirmSignInWithTOTPFooter() + } + ) { + self.state = state + self.content = ConfirmSignInWithCodeView( + state: state, + headerContent: headerContent, + footerContent: footerContent, + mfaType: .totp + ) + } + + public var body: some View { + content + } + + /// 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 ``ConfirmSignInWithTOTPCodeView``. It displays the view's title +public struct ConfirmSignInWithTOTPHeader: View { + public init() {} + public var body: some View { + DefaultHeader( + title: "authenticator.confirmSignInWithCode.totp.title".localized() + ) + } +} + +/// Default footer for the ``ConfirmSignInWithTOTPCodeView``. It displays the "Back to Sign In" button +public struct ConfirmSignInWithTOTPFooter: View { + @Environment(\.authenticatorState) private var authenticatorState + + public init() {} + public var body: some View { + Button("authenticator.confirmSignInWithCode.totp.button.backToSignIn".localized()) { + authenticatorState.move(to: .signIn) + } + .buttonStyle(.link) + } +} + diff --git a/Sources/Authenticator/Views/ContinueSignInWithMFASelectionView.swift b/Sources/Authenticator/Views/ContinueSignInWithMFASelectionView.swift new file mode 100644 index 0000000..b2cd7f6 --- /dev/null +++ b/Sources/Authenticator/Views/ContinueSignInWithMFASelectionView.swift @@ -0,0 +1,122 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +import SwiftUI + +/// Represents the content being displayed when the ``Authenticator`` is in the ``AuthenticatorStep/continueSignInWithMFASelection`` step. +public struct ContinueSignInWithMFASelectionView: View { + @Environment(\.authenticatorState) private var authenticatorState + @ObservedObject private var state: ContinueSignInWithMFASelectionState + + private let headerContent: Header + private let footerContent: Footer + + /// Creates a `ContinueSignInWithMFASelectionView` + /// - Parameter state: The ``ConfirmSignInWithCodeState`` that is observed by this view + /// - Parameter headerContent: The content displayed above the fields. Defaults to ``ConfirmSignInWithMFASelectionHeader`` + /// - Parameter footerContent: The content displayed bellow the fields. Defaults to ``ConfirmSignInWithMFASelectionFooter`` + public init( + state: ContinueSignInWithMFASelectionState, + @ViewBuilder headerContent: () -> Header = { + ConfirmSignInWithMFASelectionHeader() + }, + @ViewBuilder footerContent: () -> Footer = { + ConfirmSignInWithMFASelectionFooter() + } + ) { + self.state = state + self.headerContent = headerContent() + self.footerContent = footerContent() + } + + public var body: some View { + AuthenticatorView(isBusy: state.isBusy) { + headerContent + + /// Only add TOTP option if it is allowed for selection by the service + if(state.allowedMFATypes.contains(.totp)) { + RadioButton( + label: "authenticator.continueSignInWithMFASelection.totp.radioButton.title".localized(), + isSelected: .constant(state.selectedMFAType == .totp) + ) { + state.selectedMFAType = .totp + } + .accessibilityAddTraits(state.selectedMFAType == .totp ? .isSelected : .isButton) + .animation(.none, value: state.selectedMFAType) + } + + /// Only add SMS option if it is allowed for selection by the service + if(state.allowedMFATypes.contains(.sms)) { + RadioButton( + label: "authenticator.continueSignInWithMFASelection.sms.radioButton.title".localized(), + isSelected: .constant(state.selectedMFAType == .sms) + ) { + state.selectedMFAType = .sms + } + .accessibilityAddTraits(state.selectedMFAType == .sms ? .isSelected : .isButton) + .animation(.none, value: state.selectedMFAType) + } + + Button("authenticator.continueSignInWithMFASelection.button.submit".localized()) { + Task { await continueSignIn() } + } + .buttonStyle(.primary) + .disabled(state.selectedMFAType == nil) + .opacity(state.selectedMFAType == nil ? 0.5 : 1) + + footerContent + } + .messageBanner($state.message) + .onSubmit { + Task { + await continueSignIn() + } + } + .onDisappear{ + state.selectedMFAType = 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 ContinueSignInWithMFASelectionView: AuthenticatorLogging {} + +/// Default header for the ``ContinueSignInWithMFASelectionView``. It displays the view's title +public struct ConfirmSignInWithMFASelectionHeader: View { + public init() {} + public var body: some View { + DefaultHeader( + title: "authenticator.continueSignInWithMFASelection.title".localized() + ) + } +} + +/// Default footer for the ``ContinueSignInWithMFASelectionView``. It displays the "Back to Sign In" button +public struct ConfirmSignInWithMFASelectionFooter: View { + @Environment(\.authenticatorState) private var authenticatorState + + public init() {} + public var body: some View { + Button("authenticator.continueSignInWithMFASelection.button.backToSignIn".localized()) { + authenticatorState.move(to: .signIn) + } + .buttonStyle(.link) + } +} + diff --git a/Sources/Authenticator/Views/ContinueSignInWithTOTPCopyKeyView.swift b/Sources/Authenticator/Views/ContinueSignInWithTOTPCopyKeyView.swift new file mode 100644 index 0000000..5650f82 --- /dev/null +++ b/Sources/Authenticator/Views/ContinueSignInWithTOTPCopyKeyView.swift @@ -0,0 +1,34 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +/// Default QRCodeContent for the ``ContinueSignInWithTOTPSetupView``. It displays the view's QR Code +public struct ContinueSignInWithTOTPCopyKeyView: View { + + @ObservedObject private var state: ContinueSignInWithTOTPSetupState + + public init(state: ContinueSignInWithTOTPSetupState) { + self.state = state + } + + public var body: some View { + Button("authenticator.continueSignInWithTOTPSetup.button.copyKey".localized()) { +#if os(iOS) + UIPasteboard.general.string = state.sharedSecret +#elseif os(macOS) + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(state.sharedSecret, forType: .string) +#endif + } + .buttonStyle(.capsule) + } + +} + +extension ContinueSignInWithTOTPCopyKeyView: AuthenticatorLogging {} diff --git a/Sources/Authenticator/Views/ContinueSignInWithTOTPSetupQRCodeView.swift b/Sources/Authenticator/Views/ContinueSignInWithTOTPSetupQRCodeView.swift new file mode 100644 index 0000000..d2170ac --- /dev/null +++ b/Sources/Authenticator/Views/ContinueSignInWithTOTPSetupQRCodeView.swift @@ -0,0 +1,50 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +/// Default QRCodeContent for the ``ContinueSignInWithTOTPSetupView``. It displays the view's QR Code +public struct ContinueSignInWithTOTPSetupQRCodeView: View { + + @Environment(\.authenticatorTheme) private var theme + @ObservedObject private var state: ContinueSignInWithTOTPSetupState + + public init(state: ContinueSignInWithTOTPSetupState) { + self.state = state + } + + public var body: some View { + if let qrCodeImage = generateQRCode(qrCodeURIString: state.setupURI) { + Image(decorative: qrCodeImage, scale: 1) + .interpolation(.none) + .resizable() + .scaledToFit() + .frame(width: theme.components.authenticator.qrCodeSize, + height: theme.components.authenticator.qrCodeSize) + } + } + + private func generateQRCode(qrCodeURIString: String?) -> CGImage? { + guard let qrCodeURIString = qrCodeURIString else { + return nil + } + + let filter = CIFilter.qrCodeGenerator() + filter.message = Data(qrCodeURIString.utf8) + guard let outputImage = filter.outputImage else { + log.error("Unable to create a CI Image for TOTP Setup QRCode") + return nil + } + guard let cgImage = CIContext().createCGImage(outputImage, from: outputImage.extent) else { + log.error("Unable to create a CGImage from CIImage for TOTP Setup QRCode ") + return nil + } + return cgImage + } +} + +extension ContinueSignInWithTOTPSetupQRCodeView: AuthenticatorLogging {} diff --git a/Sources/Authenticator/Views/ContinueSignInWithTOTPSetupView.swift b/Sources/Authenticator/Views/ContinueSignInWithTOTPSetupView.swift new file mode 100644 index 0000000..28e76b7 --- /dev/null +++ b/Sources/Authenticator/Views/ContinueSignInWithTOTPSetupView.swift @@ -0,0 +1,153 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +import SwiftUI +import CoreImage.CIFilterBuiltins + + +/// Represents the content being displayed when the ``Authenticator`` is in the ``AuthenticatorStep/continueSignInWithTOTPSetup`` step. +public struct ContinueSignInWithTOTPSetupView: View { + @Environment(\.authenticatorState) private var authenticatorState + @Environment(\.authenticatorTheme) private var theme + @Environment(\.authenticatorOptions) private var options + @ObservedObject private var state: ContinueSignInWithTOTPSetupState + @StateObject private var codeValidator: Validator + private let headerContent: Header + private let qrCodeContent: QRCodeContent + private let copyKeyContent: CopyKeyContent + private let footerContent: Footer + + /// Creates a `ContinueSignInWithTOTPSetupView` + /// - Parameter state: The ``ContinueSignInWithTOTPSetupState`` that is observed by this view + /// - Parameter headerContent: The content displayed above the fields. Defaults to ``ContinueSignInWithTOTPSetupHeader`` + /// - Parameter qrCodeContent: The content displayed for the QR code. Defaults to ``ContinueSignInWithTOTPSetupQRCodeView`` + /// - Parameter copyKeyContent: The content displayed for copying the code. Defaults to ``ContinueSignInWithTOTPCopyKeyView`` + /// - Parameter footerContent: The content displayed bellow the fields. Defaults to ``ContinueSignInWithTOTPSetupFooter`` + public init( + state: ContinueSignInWithTOTPSetupState, + @ViewBuilder headerContent: () -> Header = { + ContinueSignInWithTOTPSetupHeader() + }, + @ViewBuilder qrCodeContent: (ContinueSignInWithTOTPSetupState) -> QRCodeContent = { state in + ContinueSignInWithTOTPSetupQRCodeView(state: state) + }, + @ViewBuilder copyKeyContent: (ContinueSignInWithTOTPSetupState) -> CopyKeyContent = { state in + ContinueSignInWithTOTPCopyKeyView(state: state) + }, + @ViewBuilder footerContent: () -> Footer = { + ContinueSignInWithTOTPSetupFooter() + } + ) { + self.state = state + self.headerContent = headerContent() + self.qrCodeContent = qrCodeContent(state) + self.copyKeyContent = copyKeyContent(state) + self.footerContent = footerContent() + self._codeValidator = StateObject(wrappedValue: Validator( + using: FieldValidators.required + )) + } + + public var body: some View { + AuthenticatorView(isBusy: state.isBusy) { + + headerContent + + Spacer() + + AuthenticatorTextWithHeader( + title: "authenticator.continueSignInWithTOTPSetup.step1.label.title".localized(), + content: "authenticator.continueSignInWithTOTPSetup.step1.label.content".localized() + ) + + AuthenticatorTextWithHeader( + title: "authenticator.continueSignInWithTOTPSetup.step2.label.title".localized(), + content: "authenticator.continueSignInWithTOTPSetup.step2.label.content".localized() + ) + + qrCodeContent + + copyKeyContent + + AuthenticatorTextWithHeader( + title: "authenticator.continueSignInWithTOTPSetup.step3.label.title".localized(), + content: "authenticator.continueSignInWithTOTPSetup.step3.label.content".localized() + ) + + TextField( + "authenticator.continueSignInWithTOTPSetup.field.code.placeholder".localized(), + text: $state.confirmationCode, + validator: codeValidator + ) + .textContentType(.oneTimeCode) +#if os(iOS) + .keyboardType(.default) +#endif + + Button("authenticator.continueSignInWithTOTPSetup.button.submit".localized()) { + Task { await continueSignIn() } + } + .buttonStyle(.primary) + .disabled(state.confirmationCode.isEmpty) + .opacity(state.confirmationCode.isEmpty ? 0.5 : 1) + + footerContent + } + .messageBanner($state.message) + .onSubmit { + Task { + await continueSignIn() + } + } + } + + private func continueSignIn() async { + guard codeValidator.validate() else { + log.verbose("Code 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 + } +} + +extension ContinueSignInWithTOTPSetupView: AuthenticatorLogging {} + +/// Default header for the ``ContinueSignInWithTOTPSetupView``. It displays the view's title +public struct ContinueSignInWithTOTPSetupHeader: View { + public init() {} + public var body: some View { + DefaultHeader( + title: "authenticator.continueSignInWithTOTPSetup.title".localized() + ) + .alignment(.center) + } +} + +/// Default footer for the ``ContinueSignInWithTOTPSetupView``. It displays the "Back to Sign In" button +public struct ContinueSignInWithTOTPSetupFooter: View { + @Environment(\.authenticatorState) private var authenticatorState + + public init() {} + public var body: some View { + Button("authenticator.continueSignInWithTOTPSetup.button.backToSignIn".localized()) { + authenticatorState.move(to: .signIn) + } + .buttonStyle(.link) + } +} diff --git a/Sources/Authenticator/Views/Internal/AuthenticatorTextWithHeader.swift b/Sources/Authenticator/Views/Internal/AuthenticatorTextWithHeader.swift new file mode 100644 index 0000000..598609f --- /dev/null +++ b/Sources/Authenticator/Views/Internal/AuthenticatorTextWithHeader.swift @@ -0,0 +1,43 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct AuthenticatorTextWithHeader: View { + @Environment(\.authenticatorTheme) private var theme + private var title: String + private var content: String + + init(title: String, content: String) { + self.title = title + self.content = content + } + + var body: some View { + VStack { + + SwiftUI.Text(title) + .font(theme.fonts.headline) + .foregroundColor(theme.colors.foreground.primary) + .accessibilityAddTraits(.isStaticText) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer(minLength: 10) + + SwiftUI.Text(content) + .font(theme.fonts.body) + .foregroundColor(theme.colors.foreground.primary) + .accessibilityAddTraits(.isStaticText) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + } + } + +} diff --git a/Sources/Authenticator/Views/Internal/ConfirmSignInWithCodeView.swift b/Sources/Authenticator/Views/Internal/ConfirmSignInWithCodeView.swift index c468fd9..4362cc7 100644 --- a/Sources/Authenticator/Views/Internal/ConfirmSignInWithCodeView.swift +++ b/Sources/Authenticator/Views/Internal/ConfirmSignInWithCodeView.swift @@ -13,6 +13,7 @@ struct ConfirmSignInWithCodeView Footer = { EmptyView() }, - errorTransform: ((AuthError) -> AuthenticatorError)? = nil + errorTransform: ((AuthError) -> AuthenticatorError)? = nil, + mfaType: AuthenticatorMFAType ) { self.state = state self.headerContent = headerContent() @@ -32,6 +34,34 @@ struct ConfirmSignInWithCodeView DefaultHeader { + var view = self + view.alignment = alignment + return view + } } diff --git a/Sources/Authenticator/Views/Primitives/Button.swift b/Sources/Authenticator/Views/Primitives/Button.swift index 2d3112c..785a361 100644 --- a/Sources/Authenticator/Views/Primitives/Button.swift +++ b/Sources/Authenticator/Views/Primitives/Button.swift @@ -31,7 +31,7 @@ struct Button: View { switch viewModifiers.style { case .primary: return theme.colors.background.interactive - case .link: + case .link, .capsule: return .clear default: return theme.colors.background.error @@ -42,7 +42,7 @@ struct Button: View { switch viewModifiers.style { case .primary: return theme.colors.foreground.inverse - case .link: + case .link, .capsule: return theme.colors.foreground.interactive default: return theme.colors.foreground.primary @@ -55,6 +55,8 @@ struct Button: View { return theme.components.button.primary.cornerRadius case .link: return theme.components.button.link.cornerRadius + case .capsule: + return theme.components.button.capsule.cornerRadius default: return theme.components.authenticator.cornerRadius } @@ -62,7 +64,7 @@ struct Button: View { private var borderColor: Color { switch viewModifiers.style { - case .default: + case .default, .capsule: return theme.colors.border.interactive default: return .clear @@ -71,7 +73,7 @@ struct Button: View { private var borderWidth: CGFloat { switch viewModifiers.style { - case .default: + case .default, .capsule: return theme.components.authenticator.borderWidth default: return 0 @@ -84,17 +86,30 @@ struct Button: View { return theme.components.button.primary.font case .link: return theme.components.button.link.font + case .capsule: + return theme.components.button.capsule.font default: return theme.fonts.body } } + private var maxWidth: CGFloat? { + switch viewModifiers.style { + case .capsule: + return nil + default: + return viewModifiers.frame.maxWidth + } + } + private var padding: AuthenticatorTheme.Padding? { switch viewModifiers.style { case .primary: return theme.components.button.primary.padding case .link: return theme.components.button.link.padding + case .capsule: + return theme.components.button.capsule.padding default: return theme.components.authenticator.padding } @@ -107,7 +122,10 @@ struct Button: View { backgroundColor: backgroundColor, cornerRadius: cornerRadius, padding: padding, - maxWidth: viewModifiers.frame.maxWidth + maxWidth: maxWidth, + borderWidth: borderWidth, + borderColor: borderColor, + useOverlay: viewModifiers.style == .capsule ) } } @@ -137,6 +155,7 @@ extension Button { case `default` case primary case link + case capsule } func frame( @@ -173,8 +192,26 @@ private struct AuthenticatorButtonStyle: ButtonStyle { let cornerRadius: CGFloat let padding: AuthenticatorTheme.Padding? let maxWidth: CGFloat? + let borderWidth: CGFloat + let borderColor: Color + let useOverlay: Bool func makeBody(configuration: Self.Configuration) -> some View { + if useOverlay { + content(for: configuration) + .overlay( + RoundedRectangle(cornerRadius: .infinity) + .stroke(borderColor, + lineWidth: borderWidth) + ) + + } else { + content(for: configuration) + .border(borderColor, width: borderWidth) + } + } + + @ViewBuilder private func content(for configuration: Self.Configuration) -> some View { configuration.label .font(font) .padding(padding) @@ -182,6 +219,5 @@ private struct AuthenticatorButtonStyle: ButtonStyle { .frame(maxWidth: maxWidth) .foregroundColor(configuration.isPressed ? foregroundColor.opacity(0.5) : foregroundColor) .background(configuration.isPressed ? backgroundColor.opacity(0.5) : backgroundColor) - .cornerRadius(cornerRadius) } } diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.pbxproj b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..90fbaf2 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.pbxproj @@ -0,0 +1,618 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 60; + objects = { + +/* Begin PBXBuildFile section */ + 482D01142ABE21F7000A3140 /* AuthenticatorUITestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 482D01122ABE21F7000A3140 /* AuthenticatorUITestUtils.swift */; }; + 482D01172ABE2344000A3140 /* Authenticator in Frameworks */ = {isa = PBXBuildFile; productRef = 482D01162ABE2344000A3140 /* Authenticator */; }; + 482D01192ABE238E000A3140 /* Authenticator in Frameworks */ = {isa = PBXBuildFile; productRef = 482D01182ABE238E000A3140 /* Authenticator */; }; + 482D011B2AC1E824000A3140 /* SignInViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 482D011A2AC1E824000A3140 /* SignInViewTests.swift */; }; + 482D011D2AC1E839000A3140 /* SignUpViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 482D011C2AC1E839000A3140 /* SignUpViewTests.swift */; }; + 482D011F2AC1E85C000A3140 /* ResetPasswordViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 482D011E2AC1E85C000A3140 /* ResetPasswordViewTests.swift */; }; + 482D01212AC1EB69000A3140 /* AuthenticatorUITestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 482D01122ABE21F7000A3140 /* AuthenticatorUITestUtils.swift */; }; + 482D01242AC1F00D000A3140 /* ConfirmSignInWithTOTPCodeViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 482D01232AC1F00D000A3140 /* ConfirmSignInWithTOTPCodeViewTests.swift */; }; + 483E09412ABBC0D800EFD1D7 /* AuthenticatorHostApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 483E09402ABBC0D800EFD1D7 /* AuthenticatorHostApp.swift */; }; + 483E09432ABBC0D800EFD1D7 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 483E09422ABBC0D800EFD1D7 /* ContentView.swift */; }; + 483E09452ABBC0D900EFD1D7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 483E09442ABBC0D900EFD1D7 /* Assets.xcassets */; }; + 483E09492ABBC0D900EFD1D7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 483E09482ABBC0D900EFD1D7 /* Preview Assets.xcassets */; }; + 483E09512ABBC17A00EFD1D7 /* Authenticator in Frameworks */ = {isa = PBXBuildFile; productRef = 483E09502ABBC17A00EFD1D7 /* Authenticator */; }; + 483E09542ABBC26C00EFD1D7 /* MockAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 483E09532ABBC26C00EFD1D7 /* MockAuthenticationService.swift */; }; + 483E09572ABBC2C100EFD1D7 /* AuthCategoryConfigurationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 483E09562ABBC2C100EFD1D7 /* AuthCategoryConfigurationFactory.swift */; }; + 4873E7532ABC99E4001EDA1D /* ImageDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4873E7522ABC99E4001EDA1D /* ImageDiff.swift */; }; + 4873E7572ABC9B51001EDA1D /* CleanCounterBetweenTestCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4873E7562ABC9B51001EDA1D /* CleanCounterBetweenTestCases.swift */; }; + 4873E7592ABC9C58001EDA1D /* AuthenticatorBaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4873E7582ABC9C58001EDA1D /* AuthenticatorBaseTestCase.swift */; }; + 48DED8F12AC2011100F44908 /* ContinueSignInWithMFASelectionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48DED8F02AC2011100F44908 /* ContinueSignInWithMFASelectionViewTests.swift */; }; + 48DED8F32AC201E600F44908 /* ContinueSignInWithTOTPSetupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48DED8F22AC201E600F44908 /* ContinueSignInWithTOTPSetupViewTests.swift */; }; + 48DED8F52AC2050800F44908 /* Snapshotter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48DED8F42AC2050800F44908 /* Snapshotter.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 483E09622ABBC4AC00EFD1D7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 483E09352ABBC0D800EFD1D7 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 483E093C2ABBC0D800EFD1D7; + remoteInfo = AuthenticatorHostApp; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 482D01122ABE21F7000A3140 /* AuthenticatorUITestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatorUITestUtils.swift; sourceTree = ""; }; + 482D011A2AC1E824000A3140 /* SignInViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewTests.swift; sourceTree = ""; }; + 482D011C2AC1E839000A3140 /* SignUpViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpViewTests.swift; sourceTree = ""; }; + 482D011E2AC1E85C000A3140 /* ResetPasswordViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPasswordViewTests.swift; sourceTree = ""; }; + 482D01222AC1EC3B000A3140 /* AuthenticatorHostApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AuthenticatorHostApp.xctestplan; sourceTree = ""; }; + 482D01232AC1F00D000A3140 /* ConfirmSignInWithTOTPCodeViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmSignInWithTOTPCodeViewTests.swift; sourceTree = ""; }; + 483E093D2ABBC0D800EFD1D7 /* AuthenticatorHostApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AuthenticatorHostApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 483E09402ABBC0D800EFD1D7 /* AuthenticatorHostApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatorHostApp.swift; sourceTree = ""; }; + 483E09422ABBC0D800EFD1D7 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 483E09442ABBC0D900EFD1D7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 483E09462ABBC0D900EFD1D7 /* AuthenticatorHostApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AuthenticatorHostApp.entitlements; sourceTree = ""; }; + 483E09482ABBC0D900EFD1D7 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 483E09532ABBC26C00EFD1D7 /* MockAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationService.swift; sourceTree = ""; }; + 483E09562ABBC2C100EFD1D7 /* AuthCategoryConfigurationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCategoryConfigurationFactory.swift; sourceTree = ""; }; + 483E095C2ABBC4AC00EFD1D7 /* AuthenticatorHostAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthenticatorHostAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4873E7522ABC99E4001EDA1D /* ImageDiff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDiff.swift; sourceTree = ""; }; + 4873E7562ABC9B51001EDA1D /* CleanCounterBetweenTestCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanCounterBetweenTestCases.swift; sourceTree = ""; }; + 4873E7582ABC9C58001EDA1D /* AuthenticatorBaseTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatorBaseTestCase.swift; sourceTree = ""; }; + 48DED8F02AC2011100F44908 /* ContinueSignInWithMFASelectionViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueSignInWithMFASelectionViewTests.swift; sourceTree = ""; }; + 48DED8F22AC201E600F44908 /* ContinueSignInWithTOTPSetupViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueSignInWithTOTPSetupViewTests.swift; sourceTree = ""; }; + 48DED8F42AC2050800F44908 /* Snapshotter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Snapshotter.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 483E093A2ABBC0D800EFD1D7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 482D01172ABE2344000A3140 /* Authenticator in Frameworks */, + 483E09512ABBC17A00EFD1D7 /* Authenticator in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 483E09592ABBC4AC00EFD1D7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 482D01192ABE238E000A3140 /* Authenticator in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 482D01202AC1E939000A3140 /* TestCases */ = { + isa = PBXGroup; + children = ( + 482D011A2AC1E824000A3140 /* SignInViewTests.swift */, + 482D011C2AC1E839000A3140 /* SignUpViewTests.swift */, + 482D011E2AC1E85C000A3140 /* ResetPasswordViewTests.swift */, + 482D01232AC1F00D000A3140 /* ConfirmSignInWithTOTPCodeViewTests.swift */, + 48DED8F02AC2011100F44908 /* ContinueSignInWithMFASelectionViewTests.swift */, + 48DED8F22AC201E600F44908 /* ContinueSignInWithTOTPSetupViewTests.swift */, + ); + path = TestCases; + sourceTree = ""; + }; + 483E09342ABBC0D800EFD1D7 = { + isa = PBXGroup; + children = ( + 482D01222AC1EC3B000A3140 /* AuthenticatorHostApp.xctestplan */, + 483E093F2ABBC0D800EFD1D7 /* AuthenticatorHostApp */, + 483E095D2ABBC4AC00EFD1D7 /* AuthenticatorHostAppUITests */, + 483E093E2ABBC0D800EFD1D7 /* Products */, + ); + sourceTree = ""; + }; + 483E093E2ABBC0D800EFD1D7 /* Products */ = { + isa = PBXGroup; + children = ( + 483E093D2ABBC0D800EFD1D7 /* AuthenticatorHostApp.app */, + 483E095C2ABBC4AC00EFD1D7 /* AuthenticatorHostAppUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 483E093F2ABBC0D800EFD1D7 /* AuthenticatorHostApp */ = { + isa = PBXGroup; + children = ( + 483E09552ABBC2A800EFD1D7 /* Utils */, + 483E09522ABBC25900EFD1D7 /* Mocks */, + 483E09402ABBC0D800EFD1D7 /* AuthenticatorHostApp.swift */, + 483E09422ABBC0D800EFD1D7 /* ContentView.swift */, + 483E09442ABBC0D900EFD1D7 /* Assets.xcassets */, + 483E09462ABBC0D900EFD1D7 /* AuthenticatorHostApp.entitlements */, + 483E09472ABBC0D900EFD1D7 /* Preview Content */, + ); + path = AuthenticatorHostApp; + sourceTree = ""; + }; + 483E09472ABBC0D900EFD1D7 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 483E09482ABBC0D900EFD1D7 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 483E09522ABBC25900EFD1D7 /* Mocks */ = { + isa = PBXGroup; + children = ( + 483E09532ABBC26C00EFD1D7 /* MockAuthenticationService.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + 483E09552ABBC2A800EFD1D7 /* Utils */ = { + isa = PBXGroup; + children = ( + 483E09562ABBC2C100EFD1D7 /* AuthCategoryConfigurationFactory.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 483E095D2ABBC4AC00EFD1D7 /* AuthenticatorHostAppUITests */ = { + isa = PBXGroup; + children = ( + 482D01122ABE21F7000A3140 /* AuthenticatorUITestUtils.swift */, + 4873E7582ABC9C58001EDA1D /* AuthenticatorBaseTestCase.swift */, + 482D01202AC1E939000A3140 /* TestCases */, + 4873E74E2ABC9944001EDA1D /* SnapshotLogic */, + ); + path = AuthenticatorHostAppUITests; + sourceTree = ""; + }; + 4873E74E2ABC9944001EDA1D /* SnapshotLogic */ = { + isa = PBXGroup; + children = ( + 48DED8F42AC2050800F44908 /* Snapshotter.swift */, + 4873E7522ABC99E4001EDA1D /* ImageDiff.swift */, + 4873E7562ABC9B51001EDA1D /* CleanCounterBetweenTestCases.swift */, + ); + path = SnapshotLogic; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 483E093C2ABBC0D800EFD1D7 /* AuthenticatorHostApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 483E094C2ABBC0D900EFD1D7 /* Build configuration list for PBXNativeTarget "AuthenticatorHostApp" */; + buildPhases = ( + 483E09392ABBC0D800EFD1D7 /* Sources */, + 483E093A2ABBC0D800EFD1D7 /* Frameworks */, + 483E093B2ABBC0D800EFD1D7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AuthenticatorHostApp; + packageProductDependencies = ( + 483E09502ABBC17A00EFD1D7 /* Authenticator */, + 482D01162ABE2344000A3140 /* Authenticator */, + ); + productName = AuthenticatorHostApp; + productReference = 483E093D2ABBC0D800EFD1D7 /* AuthenticatorHostApp.app */; + productType = "com.apple.product-type.application"; + }; + 483E095B2ABBC4AC00EFD1D7 /* AuthenticatorHostAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 483E09642ABBC4AC00EFD1D7 /* Build configuration list for PBXNativeTarget "AuthenticatorHostAppUITests" */; + buildPhases = ( + 483E09582ABBC4AC00EFD1D7 /* Sources */, + 483E09592ABBC4AC00EFD1D7 /* Frameworks */, + 483E095A2ABBC4AC00EFD1D7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 483E09632ABBC4AC00EFD1D7 /* PBXTargetDependency */, + ); + name = AuthenticatorHostAppUITests; + packageProductDependencies = ( + 482D01182ABE238E000A3140 /* Authenticator */, + ); + productName = AuthenticatorHostAppUITests; + productReference = 483E095C2ABBC4AC00EFD1D7 /* AuthenticatorHostAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 483E09352ABBC0D800EFD1D7 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + 483E093C2ABBC0D800EFD1D7 = { + CreatedOnToolsVersion = 15.0; + }; + 483E095B2ABBC4AC00EFD1D7 = { + CreatedOnToolsVersion = 15.0; + TestTargetID = 483E093C2ABBC0D800EFD1D7; + }; + }; + }; + buildConfigurationList = 483E09382ABBC0D800EFD1D7 /* Build configuration list for PBXProject "AuthenticatorHostApp" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 483E09342ABBC0D800EFD1D7; + packageReferences = ( + 482D01152ABE2344000A3140 /* XCLocalSwiftPackageReference "../.." */, + ); + productRefGroup = 483E093E2ABBC0D800EFD1D7 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 483E093C2ABBC0D800EFD1D7 /* AuthenticatorHostApp */, + 483E095B2ABBC4AC00EFD1D7 /* AuthenticatorHostAppUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 483E093B2ABBC0D800EFD1D7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 483E09492ABBC0D900EFD1D7 /* Preview Assets.xcassets in Resources */, + 483E09452ABBC0D900EFD1D7 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 483E095A2ABBC4AC00EFD1D7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 483E09392ABBC0D800EFD1D7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 483E09432ABBC0D800EFD1D7 /* ContentView.swift in Sources */, + 482D01212AC1EB69000A3140 /* AuthenticatorUITestUtils.swift in Sources */, + 483E09572ABBC2C100EFD1D7 /* AuthCategoryConfigurationFactory.swift in Sources */, + 483E09542ABBC26C00EFD1D7 /* MockAuthenticationService.swift in Sources */, + 483E09412ABBC0D800EFD1D7 /* AuthenticatorHostApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 483E09582ABBC4AC00EFD1D7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4873E7572ABC9B51001EDA1D /* CleanCounterBetweenTestCases.swift in Sources */, + 4873E7592ABC9C58001EDA1D /* AuthenticatorBaseTestCase.swift in Sources */, + 482D011B2AC1E824000A3140 /* SignInViewTests.swift in Sources */, + 48DED8F52AC2050800F44908 /* Snapshotter.swift in Sources */, + 482D01142ABE21F7000A3140 /* AuthenticatorUITestUtils.swift in Sources */, + 4873E7532ABC99E4001EDA1D /* ImageDiff.swift in Sources */, + 48DED8F12AC2011100F44908 /* ContinueSignInWithMFASelectionViewTests.swift in Sources */, + 482D011D2AC1E839000A3140 /* SignUpViewTests.swift in Sources */, + 482D011F2AC1E85C000A3140 /* ResetPasswordViewTests.swift in Sources */, + 482D01242AC1F00D000A3140 /* ConfirmSignInWithTOTPCodeViewTests.swift in Sources */, + 48DED8F32AC201E600F44908 /* ContinueSignInWithTOTPSetupViewTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 483E09632ABBC4AC00EFD1D7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 483E093C2ABBC0D800EFD1D7 /* AuthenticatorHostApp */; + targetProxy = 483E09622ABBC4AC00EFD1D7 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 483E094A2ABBC0D900EFD1D7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 483E094B2ABBC0D900EFD1D7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 483E094D2ABBC0D900EFD1D7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AuthenticatorHostApp/AuthenticatorHostApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"AuthenticatorHostApp/Preview Content\""; + DEVELOPMENT_TEAM = 94KV3E626L; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amplify.AuthenticatorHostApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 483E094E2ABBC0D900EFD1D7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AuthenticatorHostApp/AuthenticatorHostApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"AuthenticatorHostApp/Preview Content\""; + DEVELOPMENT_TEAM = 94KV3E626L; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amplify.AuthenticatorHostApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 483E09652ABBC4AC00EFD1D7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 94KV3E626L; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amplify.AuthenticatorHostAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = AuthenticatorHostApp; + }; + name = Debug; + }; + 483E09662ABBC4AC00EFD1D7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 94KV3E626L; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.amplify.AuthenticatorHostAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = AuthenticatorHostApp; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 483E09382ABBC0D800EFD1D7 /* Build configuration list for PBXProject "AuthenticatorHostApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 483E094A2ABBC0D900EFD1D7 /* Debug */, + 483E094B2ABBC0D900EFD1D7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 483E094C2ABBC0D900EFD1D7 /* Build configuration list for PBXNativeTarget "AuthenticatorHostApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 483E094D2ABBC0D900EFD1D7 /* Debug */, + 483E094E2ABBC0D900EFD1D7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 483E09642ABBC4AC00EFD1D7 /* Build configuration list for PBXNativeTarget "AuthenticatorHostAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 483E09652ABBC4AC00EFD1D7 /* Debug */, + 483E09662ABBC4AC00EFD1D7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 482D01152ABE2344000A3140 /* XCLocalSwiftPackageReference "../.." */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../..; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 482D01162ABE2344000A3140 /* Authenticator */ = { + isa = XCSwiftPackageProductDependency; + productName = Authenticator; + }; + 482D01182ABE238E000A3140 /* Authenticator */ = { + isa = XCSwiftPackageProductDependency; + productName = Authenticator; + }; + 483E09502ABBC17A00EFD1D7 /* Authenticator */ = { + isa = XCSwiftPackageProductDependency; + productName = Authenticator; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 483E09352ABBC0D800EFD1D7 /* Project object */; +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..1c4d179 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,104 @@ +{ + "pins" : [ + { + "identity" : "amplify-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/amplify-swift", + "state" : { + "revision" : "94a3f341254a7e9c5b78ef2f1a9fe0ec289f0305", + "version" : "2.17.2" + } + }, + { + "identity" : "amplify-swift-utils-notifications", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/amplify-swift-utils-notifications.git", + "state" : { + "revision" : "f970384ad1035732f99259255cd2f97564807e41", + "version" : "1.1.0" + } + }, + { + "identity" : "aws-appsync-realtime-client-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/aws-appsync-realtime-client-ios.git", + "state" : { + "revision" : "c7ec93dcbbcd8abc90c74203937f207a7fcaa611", + "version" : "3.1.1" + } + }, + { + "identity" : "aws-crt-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awslabs/aws-crt-swift", + "state" : { + "revision" : "6feec6c3787877807aa9a00fad09591b96752376", + "version" : "0.6.1" + } + }, + { + "identity" : "aws-sdk-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awslabs/aws-sdk-swift.git", + "state" : { + "revision" : "24bae88a2391fe75da8a940a544d1ef6441f5321", + "version" : "0.13.0" + } + }, + { + "identity" : "smithy-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awslabs/smithy-swift", + "state" : { + "revision" : "7b28da158d92cd06a3549140d43b8fbcf64a94a6", + "version" : "0.15.0" + } + }, + { + "identity" : "sqlite.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stephencelis/SQLite.swift.git", + "state" : { + "revision" : "5f5ad81ac0d0a0f3e56e39e646e8423c617df523", + "version" : "0.13.2" + } + }, + { + "identity" : "starscream", + "kind" : "remoteSourceControl", + "location" : "https://github.com/daltoniam/Starscream", + "state" : { + "revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21", + "version" : "4.0.4" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" + } + }, + { + "identity" : "xmlcoder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MaxDesiatov/XMLCoder.git", + "state" : { + "revision" : "b1e944cbd0ef33787b13f639a5418d55b3bed501", + "version" : "0.17.1" + } + } + ], + "version" : 2 +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/xcshareddata/xcschemes/AuthenticatorHostApp.xcscheme b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/xcshareddata/xcschemes/AuthenticatorHostApp.xcscheme new file mode 100644 index 0000000..2774ecc --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/xcshareddata/xcschemes/AuthenticatorHostApp.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xctestplan b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xctestplan new file mode 100644 index 0000000..5254574 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xctestplan @@ -0,0 +1,32 @@ +{ + "configurations" : [ + { + "id" : "6A2C8881-BF18-41B8-8578-D487AB88126F", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "targetForVariableExpansion" : { + "containerPath" : "container:AuthenticatorHostApp.xcodeproj", + "identifier" : "483E093C2ABBC0D800EFD1D7", + "name" : "AuthenticatorHostApp" + }, + "testExecutionOrdering" : "random" + }, + "testTargets" : [ + { + "skippedTests" : [ + "AuthenticatorBaseTestCase" + ], + "target" : { + "containerPath" : "container:AuthenticatorHostApp.xcodeproj", + "identifier" : "483E095B2ABBC4AC00EFD1D7", + "name" : "AuthenticatorHostAppUITests" + } + } + ], + "version" : 1 +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/AccentColor.colorset/Contents.json b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..532cd72 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,63 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/Contents.json b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp/AuthenticatorHostApp.entitlements b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/AuthenticatorHostApp.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/AuthenticatorHostApp.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp/AuthenticatorHostApp.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/AuthenticatorHostApp.swift new file mode 100644 index 0000000..28df781 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/AuthenticatorHostApp.swift @@ -0,0 +1,89 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +@testable import Authenticator +import AWSCognitoAuthPlugin +import SwiftUI + +@main +struct AuthenticatorHostApp: App { + + private let factory = AuthCategoryConfigurationFactory.shared + private var hidesSignUpButton = false + private var initialStep = AuthenticatorInitialStep.signIn + private var authSignInNextStep = AuthSignInStep.done + + var body: some Scene { + WindowGroup { + ContentView( + hidesSignUpButton: hidesSignUpButton, + initialStep: initialStep, + authSignInStep: authSignInNextStep) + } + } + + init() { + processUITestLaunchArguments() + do { + try Amplify.add(plugin: AWSCognitoAuthPlugin()) + try Amplify.configure(AmplifyConfiguration(auth: factory.createConfiguration())) + } catch { + print("Unable to configure Amplify \(error)") + } + } + + mutating func modifyConfiguration(for argument: ProcessArgument) { + switch argument { + case .initialStep(let step): + self.initialStep = step + case .hidesSignUpButton(let hidesSignUpButton): + self.hidesSignUpButton = hidesSignUpButton + case .userAttributes(let userAttributes): + factory.setUserAtributes(userAttributes) + case .authSignInStep(let authUITestNextStep): + self.authSignInNextStep = getMockedNextStepResult(from: authUITestNextStep) + } + } + + mutating func processUITestLaunchArguments() { + let uiTestArguments = ProcessInfo.processInfo.arguments + var arguments: [ProcessArgument] = [] + for (index, argument) in uiTestArguments.enumerated() { + if argument.isEqual(UITestKeyKey) { + arguments = try! JSONDecoder().decode([ProcessArgument].self, from: uiTestArguments[index + 1].data(using: .utf8)!) + break + } + } + for argument in arguments { + modifyConfiguration(for: argument) + } + } + + private func getMockedNextStepResult(from authUITestSignInStep: AuthUITestSignInStep) -> AuthSignInStep { + switch authUITestSignInStep { + case .confirmSignInWithSMSMFACode: + return .confirmSignInWithSMSMFACode(.init(destination: .email("testEmail@test.com")), nil) + case .confirmSignInWithCustomChallenge: + return .confirmSignInWithCustomChallenge(nil) + case .confirmSignInWithNewPassword: + return .confirmSignInWithNewPassword(nil) + case .confirmSignInWithTOTPCode: + return .confirmSignInWithTOTPCode + case .continueSignInWithTOTPSetup: + return .continueSignInWithTOTPSetup(.init(sharedSecret: "secret", username: "username")) + case .continueSignInWithMFASelection: + return .continueSignInWithMFASelection([.totp, .sms]) + case .resetPassword: + return .resetPassword(nil) + case .confirmSignUp: + return .confirmSignUp(nil) + case .done: + return .done + } + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp/ContentView.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/ContentView.swift new file mode 100644 index 0000000..2d21e34 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/ContentView.swift @@ -0,0 +1,49 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import Amplify +@testable import Authenticator +import AWSCognitoAuthPlugin +import SwiftUI + +struct ContentView: View { + private let hidesSignUpButton: Bool + private let initialStep: AuthenticatorInitialStep + + init(hidesSignUpButton: Bool, + initialStep: AuthenticatorInitialStep, + authSignInStep: AuthSignInStep) { + self.hidesSignUpButton = hidesSignUpButton + self.initialStep = initialStep + MockAuthenticationService.shared.mockedSignInResult = .init(nextStep: authSignInStep) + } + + var body: some View { + Authenticator(initialStep: initialStep) { state in + VStack { + Text("Hello, \(state.user.username)") + Button("Sign out") { + Task { await state.signOut() } + } + .buttonStyle(.bordered) + } + } + .hidesSignUpButton(hidesSignUpButton) + .signUpFields(signUpFields) + .authenticationService(MockAuthenticationService.shared) + .onAppear { + print("Appeared!") + } + .statusBar(hidden: true) + } + + + + private var signUpFields: [SignUpField] { + return [] + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Mocks/MockAuthenticationService.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Mocks/MockAuthenticationService.swift new file mode 100644 index 0000000..02086e9 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Mocks/MockAuthenticationService.swift @@ -0,0 +1,202 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +@testable import Authenticator +import Foundation + +class MockAuthenticationService: AuthenticationService { + + static var shared = MockAuthenticationService() + + private init() {} + + // MARK: - Sign In + + var signInCount = 0 + var mockedSignInResult: AuthSignInResult? + func signIn(username: String?, password: String?, options: AuthSignInRequest.Options?) async throws -> AuthSignInResult { + signInCount += 1 + if let mockedSignInResult = mockedSignInResult { + return mockedSignInResult + } + + throw AuthenticatorError.error(message: "Unable to sign in") + } + + var confirmSignInCount = 0 + var mockedConfirmSignInResult: AuthSignInResult? + func confirmSignIn(challengeResponse: String, options: AuthConfirmSignInRequest.Options?) async throws -> AuthSignInResult { + confirmSignInCount += 1 + if let mockedConfirmSignInResult = mockedConfirmSignInResult { + return mockedConfirmSignInResult + } + + throw AuthenticatorError.error(message: "Unable to confirm sign in") + } + + var mockedCurrentUser: AuthUser? + func getCurrentUser() async throws -> AuthUser { + if let mockedCurrentUser = mockedCurrentUser { + return mockedCurrentUser + } + + throw AuthenticatorError.error(message: "Unable to retrieve Current User") + } + + // MARK: - Reset Password + + var resetPasswordCount = 0 + var mockedResetPasswordResult: AuthResetPasswordResult? + func resetPassword(for username: String, options: AuthResetPasswordRequest.Options?) async throws -> AuthResetPasswordResult { + resetPasswordCount += 1 + if let mockedResetPasswordResult = mockedResetPasswordResult { + return mockedResetPasswordResult + } + + throw AuthenticatorError.error(message: "Unable to reset password") + } + + var confirmResetPasswordCount = 0 + var mockedConfirmResetPasswordError: AuthenticatorError? + func confirmResetPassword(for username: String, with newPassword: String, confirmationCode: String, options: AuthConfirmResetPasswordRequest.Options?) async throws { + confirmResetPasswordCount += 1 + if let error = mockedConfirmResetPasswordError { + throw error + } + } + + // MARK: - Sign Up + + var signUpCount = 0 + var mockedSignUpResult: AuthSignUpResult? + func signUp(username: String, password: String?, options: AuthSignUpRequest.Options?) async throws -> AuthSignUpResult { + signUpCount += 1 + if let mockedSignUpResult = mockedSignUpResult { + return mockedSignUpResult + } + throw AuthenticatorError.error(message: "Unable to sign up") + } + + var confirmSignUpCount = 0 + var mockedConfirmSignUpResult: AuthSignUpResult? + func confirmSignUp(for username: String, confirmationCode: String, options: AuthConfirmSignUpRequest.Options?) async throws -> AuthSignUpResult { + confirmSignUpCount += 1 + if let mockedConfirmSignUpResult = mockedConfirmSignUpResult { + return mockedConfirmSignUpResult + } + + throw AuthenticatorError.error(message: "Unable to confirm sign up") + } + + var resendSignUpCodeCount = 0 + var mockedResendSignUpCodeResult: AuthCodeDeliveryDetails? + func resendSignUpCode(for username: String, options: AuthResendSignUpCodeRequest.Options?) async throws -> AuthCodeDeliveryDetails { + resendSignUpCodeCount += 1 + if let mockedResendSignUpCodeResult = mockedResendSignUpCodeResult { + return mockedResendSignUpCodeResult + } + throw AuthenticatorError.error(message: "Unable to resend sign up code") + } + + // MARK: - Verify User + + var fetchUserAttributesCount = 0 + var mockedUnverifiedAttributes: [AuthUserAttribute] = [] + func fetchUserAttributes(options: AuthFetchUserAttributesRequest.Options?) async throws -> [AuthUserAttribute] { + fetchUserAttributesCount += 1 + return mockedUnverifiedAttributes + } + + var resendConfirmationCodeForAttributeCount = 0 + var mockedResendConfirmationCodeForAttributeResult: AuthCodeDeliveryDetails? + func resendConfirmationCode(forUserAttributeKey userAttributeKey: AuthUserAttributeKey, options: AuthAttributeResendConfirmationCodeRequest.Options?) async throws -> AuthCodeDeliveryDetails { + resendConfirmationCodeForAttributeCount += 1 + if let mockedResendConfirmationCodeForAttributeResult = mockedResendConfirmationCodeForAttributeResult { + return mockedResendConfirmationCodeForAttributeResult + } + + throw AuthenticatorError.error(message: "Unable to resend confirmation code for attribute") + } + + var confirmUserAttributeCount = 0 + var mockedConfirmUserAttributeError: AuthenticatorError? + func confirm(userAttribute: AuthUserAttributeKey, confirmationCode: String, options: AuthConfirmUserAttributeRequest.Options?) async throws { + confirmUserAttributeCount += 1 + if let mockedConfirmUserAttributeError = mockedConfirmUserAttributeError { + throw mockedConfirmUserAttributeError + } + } + + // MARK: - Sign Out + + var signOutCount = 0 + var mockedSignOutResult: AuthSignOutResult? + func signOut(options: AuthSignOutRequest.Options?) async -> AuthSignOutResult { + signOutCount += 1 + return SignOutResult() + } +#if os(iOS) || os(macOS) + // MARK: - Web UI + func signInWithWebUI(presentationAnchor: AuthUIPresentationAnchor?, options: AuthWebUISignInRequest.Options?) async throws -> AuthSignInResult { + return .init(nextStep: .done) + } + + func signInWithWebUI(for authProvider: AuthProvider, presentationAnchor: AuthUIPresentationAnchor?, options: AuthWebUISignInRequest.Options?) async throws -> AuthSignInResult { + return .init(nextStep: .done) + } +#endif + + // MARK: - User management + + func fetchAuthSession(options: AuthFetchSessionRequest.Options?) async throws -> AuthSession { + return Session(isSignedIn: true) + } + + func update(userAttribute: AuthUserAttribute, options: AuthUpdateUserAttributeRequest.Options?) async throws -> AuthUpdateAttributeResult { + return .init(isUpdated: true, nextStep: .done) + } + + func update(userAttributes: [AuthUserAttribute], options: AuthUpdateUserAttributesRequest.Options?) async throws -> [AuthUserAttributeKey: AuthUpdateAttributeResult] { + return [:] + } + + func update(oldPassword: String, to newPassword: String, options: AuthChangePasswordRequest.Options?) async throws {} + + func deleteUser() async throws {} + + // MARK: - Device management + + func fetchDevices(options: AuthFetchDevicesRequest.Options?) async throws -> [AuthDevice] { + return [] + } + + func forgetDevice(_ device: AuthDevice?, options: AuthForgetDeviceRequest.Options?) async throws {} + + func rememberDevice(options: AuthRememberDeviceRequest.Options?) async throws {} + + func setUpTOTP() async throws -> TOTPSetupDetails { + return .init(sharedSecret: "", username: "") + } + + func verifyTOTPSetup(code: String, options: VerifyTOTPSetupRequest.Options?) async throws { + + } +} + +extension MockAuthenticationService { + struct User: AuthUser { + var username: String + var userId: String + } + + struct SignOutResult: AuthSignOutResult {} + + struct Session: AuthSession { + var isSignedIn: Bool + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Preview Content/Preview Assets.xcassets/Contents.json b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Utils/AuthCategoryConfigurationFactory.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Utils/AuthCategoryConfigurationFactory.swift new file mode 100644 index 0000000..ace021a --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Utils/AuthCategoryConfigurationFactory.swift @@ -0,0 +1,59 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +@testable import Authenticator +@_spi(InternalAmplifyConfiguration) +@testable import AWSCognitoAuthPlugin +import Foundation + +class AuthCategoryConfigurationFactory { + static var shared = AuthCategoryConfigurationFactory() + + private var usernameAttributes: [JSONValue] = [] + private var signupAttributes: [JSONValue] = [] + private var verificationMechanisms: [JSONValue] = [ + .string("EMAIL") + ] + + func createConfiguration() -> AuthCategoryConfiguration { + return AuthCategoryConfiguration(plugins: [ + "awsCognitoAuthPlugin": [ + "CognitoUserPool": [ + "Default": [ + "PoolId": "PoolId", + "AppClientId": "AppClientId", + "Region": "us-east-1" + ] + ], + "CredentialsProvider": [ + "CognitoIdentity": [ + "Default": [ + "PoolId": "PoolId", + "Region": "us-east-1" + ] + ] + ], + "Auth": [ + "Default": [ + "usernameAttributes": .array(usernameAttributes), + "signupAttributes": .array(signupAttributes), + "verificationMechanisms": .array(verificationMechanisms), + "passwordProtectionSettings": [ + "passwordPolicyMinLength": 8, + "passwordPolicyCharacters": [] + ] + ] + ] + ] + ]) + } + + func setUserAtributes(_ userAttributesArg: [UserAttribute]) { + usernameAttributes = userAttributesArg.map({ .string($0.rawValue) }) + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorBaseTestCase.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorBaseTestCase.swift new file mode 100644 index 0000000..69f2aa1 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorBaseTestCase.swift @@ -0,0 +1,87 @@ +// +// File.swift +// AuthenticatorHostAppUITests +// +// Created by Singh, Harshdeep on 2023-09-21. +// + +import XCTest + +class AuthenticatorBaseTestCase: XCTestCase { + + override func setUpWithError() throws { + continueAfterFailure = false + } + + override func tearDownWithError() throws { + XCUIApplication().terminate() + } + + func assertSnapshot( + named name: String? = nil, + snapshotDirectory: String? = nil, + timeout: TimeInterval = 5, + file: StaticString = #file, + testName: String = #function, + line: UInt = #line + ) { + let result = Snapshotter.captureAndVerifySnapshot( + for: XCUIApplication().screenshot().image, + named: name, + snapshotDirectory: snapshotDirectory, + timeout: timeout, + file: file, + testName: testName, + line: line) + + // Add the attachments to the test case + result.attachments.forEach( { add($0) }) + + XCTAssertTrue( + result.didSucceed, + "Snapshot Assertion failed for test. Description:\n\n\(result.message ?? "No description")") + } + + func launchApp(with args: [ProcessArgument]) { + // Launch Application + let app = XCUIApplication() + + if let encodedData = try? JSONEncoder().encode(args), + let stringJSON = String(data: encodedData, encoding: .utf8) { + app.launchArguments = [ + UITestKeyKey, stringJSON, + ] + } else { + print("Unable to encode process args") + } + + app.launch() + } + + func launchAppAndLogin(with args: [ProcessArgument]) { + + // Launch Application + launchApp(with: args) + // Get app instance + let app = XCUIApplication() + + // Enter some username + app.textFields.firstMatch.tap() + app.textFields.firstMatch.typeText("username") + + // Enter some password + app.secureTextFields.firstMatch.tap() + app.secureTextFields.firstMatch.typeText("password") + + // Tap Sign in button + app.buttons["Sign In"].firstMatch.tap() + + // Wait for Sign In view to disappear + let expectation = expectation( + for: .init(format: "exists == false"), + evaluatedWith: app.staticTexts["Sign In"]) + let result = XCTWaiter.wait(for: [expectation], timeout: 5.0) + XCTAssertEqual(result, .completed) + } + +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorUITestUtils.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorUITestUtils.swift new file mode 100644 index 0000000..4be4fca --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorUITestUtils.swift @@ -0,0 +1,36 @@ +// +// ProcessArgument.swift +// AuthenticatorHostApp +// +// Created by Singh, Harshdeep on 2023-09-22. +// + +import Foundation +@testable import Authenticator + +let UITestKeyKey = "-uiTestArgsData" + +enum ProcessArgument: Codable { + case hidesSignUpButton(Bool) + case initialStep(AuthenticatorInitialStep) + case authSignInStep(AuthUITestSignInStep) + case userAttributes([UserAttribute]) +} + +enum UserAttribute: String, Codable { + case username = "USERNAME" + case email = "EMAIL" + case phoneNumber = "PHONE_NUMBER" +} + +public enum AuthUITestSignInStep: Codable { + case confirmSignInWithSMSMFACode + case confirmSignInWithCustomChallenge + case confirmSignInWithNewPassword + case confirmSignInWithTOTPCode + case continueSignInWithTOTPSetup + case continueSignInWithMFASelection + case resetPassword + case confirmSignUp + case done +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/SnapshotLogic/CleanCounterBetweenTestCases.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/SnapshotLogic/CleanCounterBetweenTestCases.swift new file mode 100644 index 0000000..9bf7fdb --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/SnapshotLogic/CleanCounterBetweenTestCases.swift @@ -0,0 +1,33 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +let counterQueue = DispatchQueue(label: "com.amplify.authenticator.counter") +var counterMap: [URL: Int] = [:] + +// We need to clean counter between tests executions in order to support test-iterations. +class CleanCounterBetweenTestCases: NSObject, XCTestObservation { + private static var registered = false + private static var registerQueue = DispatchQueue( + label: "com.amplify.authenticator.testObserver") + + static func registerIfNeeded() { + registerQueue.sync { + if !registered { + registered = true + XCTestObservationCenter.shared.addTestObserver(CleanCounterBetweenTestCases()) + } + } + } + + func testCaseDidFinish(_ testCase: XCTestCase) { + counterQueue.sync { + counterMap = [:] + } + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/SnapshotLogic/ImageDiff.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/SnapshotLogic/ImageDiff.swift new file mode 100644 index 0000000..c2fe49d --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/SnapshotLogic/ImageDiff.swift @@ -0,0 +1,93 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit + +enum ImageDiffError: Error { + case unableToGetCGImageFromData + case unableToGetColorSpaceFromCGImage + case imagesHasDifferentSizes + case unableToInitializeContext +} + +struct ImageDiff { + + static func compare(_ old: UIImage, _ new: UIImage) throws -> Bool { + return try compare(tolerance: 1, expected: old, observed: new) + } + + /// Value in range 0...100 % + typealias Percentage = Float + private static func compare(tolerance: Percentage, expected: UIImage, observed: UIImage) throws -> Bool { + guard let expectedCGImage = expected.cgImage, let observedCGImage = observed.cgImage else { + throw ImageDiffError.unableToGetCGImageFromData + } + guard let expectedColorSpace = expectedCGImage.colorSpace, let observedColorSpace = observedCGImage.colorSpace else { + throw ImageDiffError.unableToGetColorSpaceFromCGImage + } + if expectedCGImage.width != observedCGImage.width || expectedCGImage.height != observedCGImage.height { + throw ImageDiffError.imagesHasDifferentSizes + } + let imageSize = CGSize(width: expectedCGImage.width, height: expectedCGImage.height) + let numberOfPixels = Int(imageSize.width * imageSize.height) + + // Checking that our `UInt32` buffer has same number of bytes as image has. + let bytesPerRow = min(expectedCGImage.bytesPerRow, observedCGImage.bytesPerRow) + assert(MemoryLayout.stride == bytesPerRow / Int(imageSize.width)) + + let expectedPixels = UnsafeMutablePointer.allocate(capacity: numberOfPixels) + let observedPixels = UnsafeMutablePointer.allocate(capacity: numberOfPixels) + + let expectedPixelsRaw = UnsafeMutableRawPointer(expectedPixels) + let observedPixelsRaw = UnsafeMutableRawPointer(observedPixels) + + let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) + guard let expectedContext = CGContext(data: expectedPixelsRaw, width: Int(imageSize.width), height: Int(imageSize.height), + bitsPerComponent: expectedCGImage.bitsPerComponent, bytesPerRow: bytesPerRow, + space: expectedColorSpace, bitmapInfo: bitmapInfo.rawValue) else { + expectedPixels.deallocate() + observedPixels.deallocate() + throw ImageDiffError.unableToInitializeContext + } + guard let observedContext = CGContext(data: observedPixelsRaw, width: Int(imageSize.width), height: Int(imageSize.height), + bitsPerComponent: observedCGImage.bitsPerComponent, bytesPerRow: bytesPerRow, + space: observedColorSpace, bitmapInfo: bitmapInfo.rawValue) else { + expectedPixels.deallocate() + observedPixels.deallocate() + throw ImageDiffError.unableToInitializeContext + } + + expectedContext.draw(expectedCGImage, in: CGRect(origin: .zero, size: imageSize)) + observedContext.draw(observedCGImage, in: CGRect(origin: .zero, size: imageSize)) + + let expectedBuffer = UnsafeBufferPointer(start: expectedPixels, count: numberOfPixels) + let observedBuffer = UnsafeBufferPointer(start: observedPixels, count: numberOfPixels) + + var isEqual = true + if tolerance == 0 { + isEqual = expectedBuffer.elementsEqual(observedBuffer) + } else { + // Go through each pixel in turn and see if it is different + var numDiffPixels = 0 + for pixel in 0 ..< numberOfPixels where expectedBuffer[pixel] != observedBuffer[pixel] { + // If this pixel is different, increment the pixel diff count and see if we have hit our limit. + numDiffPixels += 1 + let percentage = 100 * Float(numDiffPixels) / Float(numberOfPixels) + if percentage > tolerance { + isEqual = false + break + } + } + } + + expectedPixels.deallocate() + observedPixels.deallocate() + + return isEqual + } + +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/SnapshotLogic/Snapshotter.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/SnapshotLogic/Snapshotter.swift new file mode 100644 index 0000000..e68ccb0 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/SnapshotLogic/Snapshotter.swift @@ -0,0 +1,120 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import UIKit +import XCTest + +struct Snapshotter { + + public static func captureAndVerifySnapshot( + for newImage: UIImage, + named name: String? = nil, + snapshotDirectory: String? = nil, + timeout: TimeInterval = 5, + file: StaticString = #file, + testName: String = #function, + line: UInt = #line + ) -> (didSucceed: Bool, message: String?, attachments: [XCTAttachment]) { + + do { + let (snapshotDirectoryUrl, snapshotFileUrl) = getSnapshotUrl( + named: name, + snapshotDirectory: snapshotDirectory, + file: file, + testName: testName + ) + let fileManager = FileManager.default + try fileManager.createDirectory(at: snapshotDirectoryUrl, withIntermediateDirectories: true) + + guard fileManager.fileExists(atPath: snapshotFileUrl.path) else { + try newImage.pngData()?.write(to: snapshotFileUrl) + + print("File written at \(snapshotFileUrl.absoluteString)") + return (didSucceed: false, message: "Re-run test, image saved to directory", attachments: []) + } + + print("Reference file already exists, asserting..") + + guard let fileData = fileManager.contents(atPath: snapshotFileUrl.path), + let oldImage = UIImage(data: fileData) else { + return (didSucceed: false, message: "Unable to get already existing file in data format", attachments: []) + } + + var attachments: [XCTAttachment] = [] + var message: String? = nil + var didSucceed: Bool = false + + do { + didSucceed = try ImageDiff.compare(oldImage, newImage) + } catch { + message = error.localizedDescription + } + + if !didSucceed { + message = "Images did not match. Please review test reference and failure images" + + let oldAttachment = XCTAttachment(image: oldImage) + oldAttachment.name = "Reference Image" + attachments.append(oldAttachment) + + let newAttachment = XCTAttachment(image: newImage) + newAttachment.name = "Failure Image" + attachments.append(newAttachment) + } + + return (didSucceed: didSucceed, message: message, attachments: attachments) + } catch { + return (didSucceed: false, message: error.localizedDescription, attachments: []) + } + } + + + // MARK: - Private + + private static func getSnapshotUrl( + named name: String? = nil, + snapshotDirectory: String? = nil, + file: StaticString = #file, + testName: String = #function + ) -> (URL, URL) { + let fileUrl = URL(fileURLWithPath: "\(file)", isDirectory: false) + let fileName = fileUrl.deletingPathExtension().lastPathComponent + + let snapshotDirectoryUrl = + snapshotDirectory.map { URL(fileURLWithPath: $0, isDirectory: true) } + ?? fileUrl + .deletingLastPathComponent() + .appendingPathComponent("__Snapshots__") + .appendingPathComponent(fileName) + + let identifier: String + if let name = name { + identifier = sanitizePathComponent(name) + } else { + let counter = counterQueue.sync { () -> Int in + let key = snapshotDirectoryUrl.appendingPathComponent(testName) + counterMap[key, default: 0] += 1 + return counterMap[key]! + } + identifier = String(counter) + } + + let testName = sanitizePathComponent(testName) + let snapshotFileUrl = + snapshotDirectoryUrl + .appendingPathComponent("\(testName).\(identifier)") + .appendingPathExtension("png") + return (snapshotDirectoryUrl, snapshotFileUrl) + } + + private static func sanitizePathComponent(_ string: String) -> String { + return string + .replacingOccurrences(of: "\\W+", with: "-", options: .regularExpression) + .replacingOccurrences(of: "^-|-$", with: "", options: .regularExpression) + } + +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithTOTPCodeViewTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithTOTPCodeViewTests.swift new file mode 100644 index 0000000..fe4470e --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithTOTPCodeViewTests.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 ConfirmSignInWithTOTPCodeViewTests: AuthenticatorBaseTestCase { + + func testConfirmSignInWithTOTPCodeView() throws { + launchAppAndLogin(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .authSignInStep(.confirmSignInWithTOTPCode) + ]) + assertSnapshot() + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithMFASelectionViewTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithMFASelectionViewTests.swift new file mode 100644 index 0000000..288d40b --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithMFASelectionViewTests.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 ContinueSignInWithMFASelectionViewTests: AuthenticatorBaseTestCase { + + func testContinueSignInWithMFASelectionView() throws { + launchAppAndLogin(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .authSignInStep(.continueSignInWithMFASelection) + ]) + assertSnapshot() + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithTOTPSetupViewTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithTOTPSetupViewTests.swift new file mode 100644 index 0000000..a6600eb --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithTOTPSetupViewTests.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 ContinueSignInWithTOTPSetupViewTests: AuthenticatorBaseTestCase { + + func testContinueSignInWithTOTPSetupView() throws { + launchAppAndLogin(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .authSignInStep(.continueSignInWithTOTPSetup) + ]) + assertSnapshot() + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ResetPasswordViewTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ResetPasswordViewTests.swift new file mode 100644 index 0000000..1dc83fe --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ResetPasswordViewTests.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 ResetPasswordViewTests: AuthenticatorBaseTestCase { + + func testResetPasswordViewWithUsernameAsPhoneNumber() throws { + launchApp(with: [ + .hidesSignUpButton(false), + .initialStep(.resetPassword), + .userAttributes([ .phoneNumber ]) + ]) + assertSnapshot() + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/SignInViewTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/SignInViewTests.swift new file mode 100644 index 0000000..f6e329d --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/SignInViewTests.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 SignInViewTests: AuthenticatorBaseTestCase { + + func testSignInViewWithWithUsernameAsPhoneNumber() throws { + launchApp(with: [ + .hidesSignUpButton(false), + .initialStep(.signIn), + .userAttributes([ .phoneNumber ]) + ]) + assertSnapshot() + } + + func testSignInViewWithoutSignUp() throws { + launchApp(with: [ + .hidesSignUpButton(true), + .initialStep(.signIn), + .userAttributes([ .phoneNumber ]) + ]) + assertSnapshot() + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/SignUpViewTests.swift b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/SignUpViewTests.swift new file mode 100644 index 0000000..01b99d6 --- /dev/null +++ b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/SignUpViewTests.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest + +final class SignUpViewTests: AuthenticatorBaseTestCase { + + func testDefaultSignUpView() throws { + launchApp(with: [ + .initialStep(.signUp), + ]) + assertSnapshot() + } +} diff --git a/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithTOTPCodeViewTests/testConfirmSignInWithTOTPCodeView.1.png b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithTOTPCodeViewTests/testConfirmSignInWithTOTPCodeView.1.png new file mode 100644 index 0000000..ba65d99 Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithTOTPCodeViewTests/testConfirmSignInWithTOTPCodeView.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 new file mode 100644 index 0000000..fb8ab03 Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASelectionViewTests/testContinueSignInWithMFASelectionView.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 new file mode 100644 index 0000000..53f3fba Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithTOTPSetupViewTests/testContinueSignInWithTOTPSetupView.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 new file mode 100644 index 0000000..d39e723 Binary files /dev/null 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 new file mode 100644 index 0000000..6696a76 Binary files /dev/null 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 new file mode 100644 index 0000000..9043075 Binary files /dev/null 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 new file mode 100644 index 0000000..a70419b Binary files /dev/null and b/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignUpViewTests/testDefaultSignUpView.1.png differ diff --git a/Tests/AuthenticatorTests/States/ContinueSignInWithMFASelectionStateTests.swift b/Tests/AuthenticatorTests/States/ContinueSignInWithMFASelectionStateTests.swift new file mode 100644 index 0000000..28bf3e0 --- /dev/null +++ b/Tests/AuthenticatorTests/States/ContinueSignInWithMFASelectionStateTests.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 ContinueSignInWithMFASelectionStateTests: XCTestCase { + private var state: ContinueSignInWithMFASelectionState! + private var authenticatorState: MockAuthenticatorState! + private var authenticationService: MockAuthenticationService! + + override func setUp() { + authenticatorState = MockAuthenticatorState() + state = ContinueSignInWithMFASelectionState( + authenticatorState: authenticatorState, + allowedMFATypes: [.sms, .totp]) + state.selectedMFAType = .totp + + 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: .confirmSignInWithTOTPCode) + 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 .confirmSignInWithTOTPCode = currentStep else { + XCTFail("Expected confirmSignInWithTOTPCode, 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]) + + let allowedMFATypes = try XCTUnwrap(state.allowedMFATypes) + XCTAssertEqual(allowedMFATypes, [.sms, .totp]) + } + +} diff --git a/Tests/AuthenticatorTests/States/ContinueSignInWithTOTPSetupStateTests.swift b/Tests/AuthenticatorTests/States/ContinueSignInWithTOTPSetupStateTests.swift new file mode 100644 index 0000000..ceb5b0c --- /dev/null +++ b/Tests/AuthenticatorTests/States/ContinueSignInWithTOTPSetupStateTests.swift @@ -0,0 +1,116 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +@testable import Authenticator +import XCTest + +class ContinueSignInWithTOTPSetupStateTests: XCTestCase { + private var state: ContinueSignInWithTOTPSetupState! + private var authenticatorState: MockAuthenticatorState! + private var authenticationService: MockAuthenticationService! + + override func setUp() { + authenticatorState = MockAuthenticatorState() + state = ContinueSignInWithTOTPSetupState( + authenticatorState: authenticatorState, + issuer: "issuer", + totpSetupDetails: .init(sharedSecret: "sharedSecret", username: "username")) + + 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: .done) + 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 .signedIn(_) = currentStep else { + XCTFail("Expected signedIn, 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 testSetupUriWithIssuer_onContinueSignInWithTOTPSetup_shouldReturnDetails() throws { + + authenticatorState.mockedStep = .continueSignInWithTOTPSetup(totpSetupDetails: .init(sharedSecret: "sharedSecret", username: "username")) + + let sharedSecret = try XCTUnwrap(state.sharedSecret) + XCTAssertEqual("sharedSecret", sharedSecret) + + let setupURI = try XCTUnwrap(state.setupURI) + XCTAssertEqual("otpauth://totp/issuer:username?secret=sharedSecret&issuer=issuer", setupURI) + } + + func testSetupUriWithoutWithIssuer_onContinueSignInWithTOTPSetup_shouldReturnDetails() throws { + + state = ContinueSignInWithTOTPSetupState( + authenticatorState: authenticatorState, + issuer: nil, + totpSetupDetails: .init(sharedSecret: "sharedSecret", username: "username")) + + authenticatorState.mockedStep = .continueSignInWithTOTPSetup(totpSetupDetails: .init(sharedSecret: "sharedSecret", username: "username")) + + let sharedSecret = try XCTUnwrap(state.sharedSecret) + XCTAssertEqual("sharedSecret", sharedSecret) + + let setupURI = try XCTUnwrap(state.setupURI) + XCTAssertEqual("otpauth://totp/xctest:username?secret=sharedSecret&issuer=xctest", setupURI) + } +}