diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AWSCognitoAuthPlugin.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AWSCognitoAuthPlugin.xcscheme index d92bafde07..96d011b6cc 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/AWSCognitoAuthPlugin.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/AWSCognitoAuthPlugin.xcscheme @@ -48,11 +48,6 @@ BlueprintName = "AWSCognitoAuthPluginUnitTests" ReferencedContainer = "container:"> - - - - diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AWSAuthCognitoSession.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AWSAuthCognitoSession.swift index 6c46eceb40..59d799e963 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AWSAuthCognitoSession.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AWSAuthCognitoSession.swift @@ -76,24 +76,6 @@ public struct AWSAuthCognitoSession: AuthSession, } -/// Internal Helpers for managing session tokens -internal extension AWSAuthCognitoSession { - func areTokensExpiring(in seconds: TimeInterval? = nil) -> Bool { - - guard let tokens = try? userPoolTokensResult.get(), - let idTokenClaims = try? AWSAuthService().getTokenClaims(tokenString: tokens.idToken).get(), - let accessTokenClaims = try? AWSAuthService().getTokenClaims(tokenString: tokens.idToken).get(), - let idTokenExpiration = idTokenClaims["exp"]?.doubleValue, - let accessTokenExpiration = accessTokenClaims["exp"]?.doubleValue else { - return true - } - - // If the session expires < X minutes return it - return (Date(timeIntervalSince1970: idTokenExpiration).compare(Date(timeIntervalSinceNow: seconds ?? 0)) == .orderedDescending && - Date(timeIntervalSince1970: accessTokenExpiration).compare(Date(timeIntervalSinceNow: seconds ?? 0)) == .orderedDescending) - } -} - extension AWSAuthCognitoSession: Equatable { public static func == (lhs: AWSAuthCognitoSession, rhs: AWSAuthCognitoSession) -> Bool { switch (lhs.getCognitoTokens(), rhs.getCognitoTokens()) { diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AWSCognitoUserPoolTokens.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AWSCognitoUserPoolTokens.swift index af7d80f96a..c5f4daed06 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AWSCognitoUserPoolTokens.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/AWSCognitoUserPoolTokens.swift @@ -65,10 +65,10 @@ public struct AWSCognitoUserPoolTokens: AuthCognitoTokens { case (.some(let idTokenValue), .none): expirationDoubleValue = idTokenValue case (.none, .none): - expirationDoubleValue = 0 + expirationDoubleValue = Date().timeIntervalSince1970 } - self.expiration = Date().addingTimeInterval(TimeInterval((expirationDoubleValue ?? 0))) + self.expiration = Date(timeIntervalSince1970: TimeInterval(expirationDoubleValue)) } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/AuthCognitoSignedOutSessionHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/AuthCognitoSignedOutSessionHelper.swift index 9ded44922d..8e391355e5 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/AuthCognitoSignedOutSessionHelper.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/AuthCognitoSignedOutSessionHelper.swift @@ -25,27 +25,6 @@ struct AuthCognitoSignedOutSessionHelper { return authSession } - /// Guest/SignedOut session with any unhandled error - /// - /// The unhandled error is passed as identityId and aws credentials result. UserSub and Cognito Tokens will still - /// have signOut error. - /// - /// - Parameter error: Unhandled error - /// - Returns: Session will have isSignedIn = false - private static func makeSignedOutSession(withUnhandledError error: AuthError) -> AWSAuthCognitoSession { - - let identityIdError = error - let awsCredentialsError = error - - let tokensError = makeCognitoTokensSignedOutError() - - let authSession = AWSAuthCognitoSession(isSignedIn: false, - identityIdResult: .failure(identityIdError), - awsCredentialsResult: .failure(awsCredentialsError), - cognitoTokensResult: .failure(tokensError)) - return authSession - } - /// Guest/SignOut session when the guest access is not enabled. /// - Returns: Session with isSignedIn = false static func makeSessionWithNoGuestAccess() -> AWSAuthCognitoSession { @@ -68,26 +47,6 @@ struct AuthCognitoSignedOutSessionHelper { return authSession } - private static func makeOfflineSignedOutSession() -> AWSAuthCognitoSession { - let identityIdError = AuthError.service( - AuthPluginErrorConstants.identityIdOfflineError.errorDescription, - AuthPluginErrorConstants.identityIdOfflineError.recoverySuggestion, - AWSCognitoAuthError.network) - - let awsCredentialsError = AuthError.service( - AuthPluginErrorConstants.awsCredentialsOfflineError.errorDescription, - AuthPluginErrorConstants.awsCredentialsOfflineError.recoverySuggestion, - AWSCognitoAuthError.network) - - let tokensError = makeCognitoTokensSignedOutError() - - let authSession = AWSAuthCognitoSession(isSignedIn: false, - identityIdResult: .failure(identityIdError), - awsCredentialsResult: .failure(awsCredentialsError), - cognitoTokensResult: .failure(tokensError)) - return authSession - } - /// Guest/SignedOut session with couldnot retreive either aws credentials or identity id. /// - Returns: Session will have isSignedIn = false private static func makeSignedOutSessionWithServiceIssue() -> AWSAuthCognitoSession { @@ -109,13 +68,6 @@ struct AuthCognitoSignedOutSessionHelper { return authSession } - private static func makeUserSubSignedOutError() -> AuthError { - let userSubError = AuthError.signedOut( - AuthPluginErrorConstants.userSubSignOutError.errorDescription, - AuthPluginErrorConstants.userSubSignOutError.recoverySuggestion) - return userSubError - } - private static func makeCognitoTokensSignedOutError() -> AuthError { let tokensError = AuthError.signedOut( AuthPluginErrorConstants.cognitoTokensSignOutError.errorDescription, diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/HostedUI/HostedUIASWebAuthenticationSession.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/HostedUI/HostedUIASWebAuthenticationSession.swift index cd9760637a..9c225ec931 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/HostedUI/HostedUIASWebAuthenticationSession.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/HostedUI/HostedUIASWebAuthenticationSession.swift @@ -22,7 +22,7 @@ class HostedUIASWebAuthenticationSession: NSObject, HostedUISessionBehavior { callback: @escaping (Result<[URLQueryItem], HostedUIError>) -> Void) { #if os(iOS) || os(macOS) self.webPresentation = presentationAnchor - let aswebAuthenticationSession = ASWebAuthenticationSession( + let aswebAuthenticationSession = createAuthenticationSession( url: url, callbackURLScheme: callbackScheme, completionHandler: { url, error in @@ -58,6 +58,16 @@ class HostedUIASWebAuthenticationSession: NSObject, HostedUISessionBehavior { } #if os(iOS) || os(macOS) + var authenticationSessionFactory = ASWebAuthenticationSession.init(url:callbackURLScheme:completionHandler:) + + private func createAuthenticationSession( + url: URL, + callbackURLScheme: String?, + completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler + ) -> ASWebAuthenticationSession { + return authenticationSessionFactory(url, callbackURLScheme, completionHandler) + } + private func convertHostedUIError(_ error: Error) -> HostedUIError { if let asWebAuthError = error as? ASWebAuthenticationSessionError { switch asWebAuthError.code { diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/CredentialStore/MigrateLegacyCredentialStoreTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/CredentialStore/MigrateLegacyCredentialStoreTests.swift index d4a0248e34..c7a475990a 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/CredentialStore/MigrateLegacyCredentialStoreTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/CredentialStore/MigrateLegacyCredentialStoreTests.swift @@ -72,7 +72,7 @@ class MigrateLegacyCredentialStoreTests: XCTestCase { let action = MigrateLegacyCredentialStore() await action.execute(withDispatcher: MockDispatcher { _ in }, environment: environment) - await waitForExpectations(timeout: 0.1) + await fulfillment(of: [saveCredentialHandlerInvoked], timeout: 0.1) } /// Test is responsible for making sure that the legacy credential store clearing up is getting called for user pool and identity pool @@ -115,8 +115,109 @@ class MigrateLegacyCredentialStoreTests: XCTestCase { let action = MigrateLegacyCredentialStore() await action.execute(withDispatcher: MockDispatcher { _ in }, environment: environment) - await waitForExpectations(timeout: 0.1) - + await fulfillment(of: [migrationCompletionInvoked], timeout: 0.1) } + + func testInvalidEnvironment() async { + let expectation = expectation(description: "noEnvironment") + let action = MigrateLegacyCredentialStore() + await action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? CredentialStoreEvent, + case let .throwError(error) = event.eventType else { + XCTFail("Expected failure due to no CredentialEnvironment") + expectation.fulfill() + return + } + XCTAssertEqual(error, .configuration(message: AuthPluginErrorConstants.configurationError)) + expectation.fulfill() + }, + environment: MockInvalidEnvironment() + ) + await fulfillment(of: [expectation], timeout: 1) + } + + func testNoUserPoolWithoutLoginsTokens() async { + let expectation = expectation(description: "noUserPoolTokens") + let action = MigrateLegacyCredentialStore() + await action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? CredentialStoreEvent, + case .loadCredentialStore(let type) = event.eventType else { + XCTFail("Expected .loadCredentialStore") + expectation.fulfill() + return + } + XCTAssertEqual(type, .amplifyCredentials) + expectation.fulfill() + }, + environment: CredentialEnvironment( + authConfiguration: .identityPools(.testData), + credentialStoreEnvironment: BasicCredentialStoreEnvironment( + amplifyCredentialStoreFactory: { + MockAmplifyCredentialStoreBehavior( + saveCredentialHandler: { codableCredentials in + guard let amplifyCredentials = codableCredentials as? AmplifyCredentials, + case .identityPoolOnly(_, let credentials) = amplifyCredentials else { + XCTFail("Expected .identityPoolOnly") + return + } + XCTAssertFalse(credentials.sessionToken.isEmpty) + } + ) + }, + legacyKeychainStoreFactory: { _ in + MockKeychainStoreBehavior(data: "hostedUI") + }), + logger: MigrateLegacyCredentialStore.log + ) + ) + await fulfillment(of: [expectation], timeout: 1) + } + + func testNoUserPoolWithLoginsTokens() async { + let expectation = expectation(description: "noUserPoolTokens") + let action = MigrateLegacyCredentialStore() + await action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? CredentialStoreEvent, + case .loadCredentialStore(let type) = event.eventType else { + XCTFail("Expected .loadCredentialStore") + expectation.fulfill() + return + } + XCTAssertEqual(type, .amplifyCredentials) + expectation.fulfill() + }, + environment: CredentialEnvironment( + authConfiguration: .identityPools(.testData), + credentialStoreEnvironment: BasicCredentialStoreEnvironment( + amplifyCredentialStoreFactory: { + MockAmplifyCredentialStoreBehavior( + saveCredentialHandler: { codableCredentials in + guard let amplifyCredentials = codableCredentials as? AmplifyCredentials, + case .identityPoolWithFederation(let token, _, _) = amplifyCredentials else { + XCTFail("Expected .identityPoolWithFederation") + return + } + + XCTAssertEqual(token.token, "token") + XCTAssertEqual(token.provider.userPoolProviderName, "provider") + } + ) + }, + legacyKeychainStoreFactory: { _ in + let data = try! JSONEncoder().encode([ + "provider": "token" + ]) + return MockKeychainStoreBehavior( + data: String(decoding: data, as: UTF8.self) + ) + }), + logger: action.log + ) + ) + await fulfillment(of: [expectation], timeout: 1) + } } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/FetchAuthSession/FetchUserPoolTokens/RefreshHostedUITokensTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/FetchAuthSession/FetchUserPoolTokens/RefreshHostedUITokensTests.swift new file mode 100644 index 0000000000..e00955e096 --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/FetchAuthSession/FetchUserPoolTokens/RefreshHostedUITokensTests.swift @@ -0,0 +1,289 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#if os(iOS) || os(macOS) + +@testable import AWSCognitoAuthPlugin +import AWSCognitoIdentityProvider +import AWSPluginsCore +import XCTest + +class RefreshHostedUITokensTests: XCTestCase { + private let tokenResult: [String: Any] = [ + "id_token": AWSCognitoUserPoolTokens.testData.idToken, + "access_token": AWSCognitoUserPoolTokens.testData.accessToken, + "refresh_token": AWSCognitoUserPoolTokens.testData.refreshToken, + "expires_in": 10 + ] + + override func setUp() { + let result = try! JSONSerialization.data(withJSONObject: tokenResult) + MockURLProtocol.requestHandler = { _ in + return (HTTPURLResponse(), result) + } + } + + override func tearDown() { + MockURLProtocol.requestHandler = nil + } + + func testValidSuccessfulResponse() async { + let expectation = expectation(description: "refreshHostedUITokens") + let action = RefreshHostedUITokens(existingSignedIndata: .testData) + action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? RefreshSessionEvent, + case .refreshIdentityInfo(let data, _) = event.eventType else { + XCTFail("Failed to refresh tokens") + expectation.fulfill() + return + } + + XCTAssertEqual(data.cognitoUserPoolTokens.idToken, self.tokenResult["id_token"] as? String) + XCTAssertEqual(data.cognitoUserPoolTokens.accessToken, self.tokenResult["access_token"] as? String) + XCTAssertEqual(data.cognitoUserPoolTokens.refreshToken, self.tokenResult["refresh_token"] as? String) + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: hostedUIEnvironment + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testServiceError() async { + let expectedError = HostedUIError.serviceMessage("Something went wrong") + MockURLProtocol.requestHandler = { _ in + throw expectedError + } + + let expectation = expectation(description: "refreshHostedUITokens") + let action = RefreshHostedUITokens(existingSignedIndata: .testData) + action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? RefreshSessionEvent, + case let .throwError(error) = event.eventType else { + XCTFail("Expected failure due to Service Error") + expectation.fulfill() + return + } + + XCTAssertEqual(error, .service(expectedError)) + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: hostedUIEnvironment + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testEmptyData() async { + MockURLProtocol.requestHandler = { _ in + return (HTTPURLResponse(), Data()) + } + + let expectation = expectation(description: "refreshHostedUITokens") + let action = RefreshHostedUITokens(existingSignedIndata: .testData) + action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? RefreshSessionEvent, + case let .throwError(error) = event.eventType else { + XCTFail("Expected failure due to Invalid Tokens") + expectation.fulfill() + return + } + + guard case .service(let serviceError) = error else { + XCTFail("Expected FetchSessionError.service, got \(error)") + expectation.fulfill() + return + } + + + XCTAssertEqual((serviceError as NSError).code, NSPropertyListReadCorruptError) + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: hostedUIEnvironment + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testInvalidTokens() async { + let result: [String: Any] = [ + "key": "value" + ] + MockURLProtocol.requestHandler = { _ in + return (HTTPURLResponse(), try! JSONSerialization.data(withJSONObject: result)) + } + + let expectation = expectation(description: "refreshHostedUITokens") + let action = RefreshHostedUITokens(existingSignedIndata: .testData) + action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? RefreshSessionEvent, + case let .throwError(error) = event.eventType else { + XCTFail("Expected failure due to Invalid Tokens") + expectation.fulfill() + return + } + + + XCTAssertEqual(error, .invalidTokens) + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: hostedUIEnvironment + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testErrorResponse() async { + let result: [String: Any] = [ + "error": "Error.", + "error_description": "Something went wrong" + ] + MockURLProtocol.requestHandler = { _ in + return (HTTPURLResponse(), try! JSONSerialization.data(withJSONObject: result)) + } + + let expectation = expectation(description: "refreshHostedUITokens") + let action = RefreshHostedUITokens(existingSignedIndata: .testData) + action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? RefreshSessionEvent, + case let .throwError(error) = event.eventType else { + XCTFail("Expected failure due to Invalid Tokens") + expectation.fulfill() + return + } + + guard case .service(let serviceError) = error, + case .serviceMessage(let errorMessage) = serviceError as? HostedUIError else { + XCTFail("Expected HostedUIError.serviceMessage, got \(error)") + expectation.fulfill() + return + } + + + XCTAssertEqual(errorMessage, "Error. Something went wrong") + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: hostedUIEnvironment + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testNoHostedUIEnvironment() async { + let expectation = expectation(description: "noHostedUIEnvironment") + let action = RefreshHostedUITokens(existingSignedIndata: .testData) + action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? RefreshSessionEvent, + case let .throwError(error) = event.eventType else { + XCTFail("Expected failure due to no HostedUIEnvironment") + expectation.fulfill() + return + } + + XCTAssertEqual(error, .noUserPool) + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: nil + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testNoUserPoolEnvironment() async { + let expectation = expectation(description: "noUserPoolEnvironment") + let action = RefreshHostedUITokens(existingSignedIndata: .testData) + action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? RefreshSessionEvent, + case let .throwError(error) = event.eventType else { + XCTFail("Expected failure due to no UserPoolEnvironment") + expectation.fulfill() + return + } + + XCTAssertEqual(error, .noUserPool) + expectation.fulfill() + }, + environment: MockInvalidEnvironment() + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + private var hostedUIEnvironment: HostedUIEnvironment { + BasicHostedUIEnvironment( + configuration: .init( + clientId: "clientId", + oauth: .init( + domain: "cognitodomain", + scopes: ["name"], + signInRedirectURI: "myapp://", + signOutRedirectURI: "myapp://" + ) + ), + hostedUISessionFactory: sessionFactory, + urlSessionFactory: urlSessionMock, + randomStringFactory: mockRandomString + ) + } + + private func identityProviderFactory() throws -> CognitoUserPoolBehavior { + return MockIdentityProvider( + mockInitiateAuthResponse: { _ in + return InitiateAuthOutputResponse( + authenticationResult: .init( + accessToken: "accessTokenNew", + expiresIn: 100, + idToken: "idTokenNew", + refreshToken: "refreshTokenNew") + ) + } + ) + } + + private func urlSessionMock() -> URLSession { + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [MockURLProtocol.self] + return URLSession(configuration: configuration) + } + + private func sessionFactory() -> HostedUISessionBehavior { + MockHostedUISession(result: .failure(.cancelled)) + } + + private func mockRandomString() -> RandomStringBehavior { + return MockRandomStringGenerator( + mockString: "mockString", + mockUUID: "mockUUID" + ) + } +} +#endif diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/InitiateAuthSRP/VerifyDevicePasswordSRPSignatureTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/InitiateAuthSRP/VerifyDevicePasswordSRPSignatureTests.swift new file mode 100644 index 0000000000..495b2f52dd --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/InitiateAuthSRP/VerifyDevicePasswordSRPSignatureTests.swift @@ -0,0 +1,162 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import AWSCognitoAuthPlugin +import AWSCognitoIdentityProvider +@testable import AWSPluginsTestCommon +import XCTest + +class VerifyDevicePasswordSRPSignatureTests: XCTestCase { + private var srpClient: MockSRPClientBehavior! + + override func setUp() async throws { + MockSRPClientBehavior.reset() + srpClient = MockSRPClientBehavior() + } + + override func tearDown() { + MockSRPClientBehavior.reset() + srpClient = nil + } + + func testSignature_withValidValues_shouldReturnSignature() async { + do { + let signature = try signature() + XCTAssertFalse(signature.isEmpty) + } catch { + XCTFail("Should not throw error: \(error)") + } + } + + func testSignature_withSRPErrorOnSharedSecret_shouldThrowCalculationError() async { + srpClient.sharedSecret = .failure(SRPError.numberConversion) + do { + try signature() + XCTFail("Should not succeed") + } catch { + guard case .calculation(let srpError) = error as? SignInError else { + XCTFail("Expected SRPError.calculation, got \(error)") + return + } + + XCTAssertEqual(srpError, .numberConversion) + } + } + + func testSignature_withOtherErrorOnSharedSecret_shouldThrowCalculationError() async { + srpClient.sharedSecret = .failure(CancellationError()) + do { + try signature() + XCTFail("Should not succeed") + } catch { + guard case .configuration(let message) = error as? SignInError else { + XCTFail("Expected SRPError.configuration, got \(error)") + return + } + + XCTAssertEqual(message, "Could not calculate shared secret") + } + } + + func testSignature_withSRPErrorOnAuthenticationKey_shouldThrowCalculationError() async { + MockSRPClientBehavior.authenticationKey = .failure(SRPError.numberConversion) + do { + try signature() + XCTFail("Should not succeed") + } catch { + guard case .calculation(let srpError) = error as? SignInError else { + XCTFail("Expected SRPError.calculation, got \(error)") + return + } + + XCTAssertEqual(srpError, .numberConversion) + } + } + + func testSignature_withOtherErrorOnAuthenticationKey_shouldThrowCalculationError() async { + MockSRPClientBehavior.authenticationKey = .failure(CancellationError()) + do { + try signature() + XCTFail("Should not succeed") + } catch { + guard case .configuration(let message) = error as? SignInError else { + XCTFail("Expected SRPError.configuration, got \(error)") + return + } + + XCTAssertEqual(message, "Could not calculate signature") + } + } + + @discardableResult + private func signature() throws -> String { + let action = VerifyDevicePasswordSRP( + stateData: .testData, + authResponse: InitiateAuthOutputResponse.validTestData + ) + + return try action.signature( + deviceGroupKey: "deviceGroupKey", + deviceKey: "deviceKey", + deviceSecret: "deviceSecret", + saltHex: "saltHex", + secretBlock: "secretBlock".data(using: .utf8) ?? Data(), + serverPublicBHexString: "serverPublicBHexString", + srpClient: srpClient + ) + } +} + +private class MockSRPClientBehavior: SRPClientBehavior { + var kHexValue: String = "kHexValue" + + static func calculateUHexValue( + clientPublicKeyHexValue: String, + serverPublicKeyHexValue: String + ) throws -> String { + return "UHexValue" + } + + static var authenticationKey: Result = .success("AuthenticationKey".data(using: .utf8)!) + static func generateAuthenticationKey( + sharedSecretHexValue: String, + uHexValue: String + ) throws -> Data { + return try authenticationKey.get() + } + + static func reset() { + authenticationKey = .success("AuthenticationKey".data(using: .utf8)!) + } + + func generateClientKeyPair() -> SRPKeys { + return .init( + publicKeyHexValue: "publicKeyHexValue", + privateKeyHexValue: "privateKeyHexValue" + ) + } + + var sharedSecret: Result = .success("SharedSecret") + func calculateSharedSecret( + username: String, + password: String, + saltHexValue: String, + clientPrivateKeyHexValue: String, + clientPublicKeyHexValue: String, + serverPublicKeyHexValue: String + ) throws -> String { + return try sharedSecret.get() + } + + func generateDevicePasswordVerifier( + deviceGroupKey: String, + deviceKey: String, + password: String + ) -> (salt: Data, passwordVerifier: Data) { + return (salt: Data(), passwordVerifier: Data()) + } +} diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/SignOut/ShowHostedUISignOutTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/SignOut/ShowHostedUISignOutTests.swift new file mode 100644 index 0000000000..abc6dafa87 --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/SignOut/ShowHostedUISignOutTests.swift @@ -0,0 +1,377 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import AWSCognitoAuthPlugin +import AWSCognitoIdentityProvider +import AWSPluginsCore +import XCTest + +class ShowHostedUISignOutTests: XCTestCase { + private var mockHostedUIResult: Result<[URLQueryItem], HostedUIError>! + private var signOutRedirectURI: String! + + override func setUp() { + signOutRedirectURI = "myapp://" + mockHostedUIResult = .success([.init(name: "key", value: "value")]) + } + + override func tearDown() { + signOutRedirectURI = nil + mockHostedUIResult = nil + } + + func testExecute_withGlobalSignOut_andSuccessResult_shouldDispatchSignOutEvent() async { + let expectation = expectation(description: "showHostedUISignOut") + let signInData = SignedInData.testData + let action = ShowHostedUISignOut( + signOutEvent: SignOutEventData(globalSignOut: true), + signInData: signInData + ) + + await action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? SignOutEvent, + case .signOutGlobally(let data, let error) = event.eventType else { + XCTFail("Expected SignOutEvent.signOutGlobally, got \(event)") + expectation.fulfill() + return + } + + XCTAssertNil(error) + XCTAssertEqual(data, signInData) + self.validateDebugInformation(signInData: signInData, action: action) + + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: hostedUIEnvironment + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testExecute_withLocalSignOut_andSuccessResult_shouldDispatchSignOutEvent() async { + let expectation = expectation(description: "showHostedUISignOut") + let signInData = SignedInData.testData + let action = ShowHostedUISignOut( + signOutEvent: SignOutEventData(globalSignOut: false), + signInData: signInData + ) + + await action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? SignOutEvent, + case .revokeToken(let data, let error, let globalSignOutError) = event.eventType else { + XCTFail("Expected SignOutEvent.revokeToken, got \(event)") + expectation.fulfill() + return + } + + XCTAssertNil(error) + XCTAssertNil(globalSignOutError) + XCTAssertEqual(data, signInData) + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: hostedUIEnvironment + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testExecute_withInvalidResult_shouldDispatchUserCancelledEvent() async { + mockHostedUIResult = .failure(.cancelled) + let signInData = SignedInData.testData + + let action = ShowHostedUISignOut( + signOutEvent: .testData, + signInData: signInData + ) + + let expectation = expectation(description: "showHostedUISignOut") + await action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? SignOutEvent else { + XCTFail("Expected SignOutEvent, got \(event)") + expectation.fulfill() + return + } + + XCTAssertEqual(event.eventType, .userCancelled) + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: hostedUIEnvironment + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testExecute_withSignOutURIError_shouldThrowConfigurationError() async { + mockHostedUIResult = .failure(HostedUIError.signOutURI) + let signInData = SignedInData.testData + + let action = ShowHostedUISignOut( + signOutEvent: .testData, + signInData: signInData + ) + + let expectation = expectation(description: "showHostedUISignOut") + await action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? SignOutEvent, + case .signOutGlobally(let data, let hostedUIError) = event.eventType else { + XCTFail("Expected SignOutEvent.signOutGlobally, got \(event)") + expectation.fulfill() + return + } + + guard let hostedUIError = hostedUIError, + case .configuration(let errorDescription, _, let serviceError) = hostedUIError.error else { + XCTFail("Expected AuthError.configuration") + expectation.fulfill() + return + } + + XCTAssertEqual(errorDescription, "Could not create logout URL") + XCTAssertEqual(data, signInData) + XCTAssertNil(serviceError) + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: hostedUIEnvironment + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testExecute_withInvalidContext_shouldThrowInvalidStateError() async { + mockHostedUIResult = .failure(HostedUIError.invalidContext) + let signInData = SignedInData.testData + + let action = ShowHostedUISignOut( + signOutEvent: .testData, + signInData: signInData + ) + + let expectation = expectation(description: "showHostedUISignOut") + await action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? SignOutEvent, + case .signOutGlobally(let data, let hostedUIError) = event.eventType else { + XCTFail("Expected SignOutEvent.signOutGlobally, got \(event)") + expectation.fulfill() + return + } + + guard let hostedUIError = hostedUIError, + case .invalidState(let errorDescription, let recoverySuggestion, let serviceError) = hostedUIError.error else { + XCTFail("Expected AuthError.invalidState") + expectation.fulfill() + return + } + + XCTAssertEqual(errorDescription, AuthPluginErrorConstants.hostedUIInvalidPresentation.errorDescription) + XCTAssertEqual(recoverySuggestion, AuthPluginErrorConstants.hostedUIInvalidPresentation.recoverySuggestion) + XCTAssertEqual(data, signInData) + XCTAssertNil(serviceError) + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: hostedUIEnvironment + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testExecute_withInvalidSignOutURI_shouldThrowConfigurationError() async { + signOutRedirectURI = "invalidURI" + let signInData = SignedInData.testData + + let action = ShowHostedUISignOut( + signOutEvent: .testData, + signInData: signInData + ) + + let expectation = expectation(description: "showHostedUISignOut") + await action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? SignOutEvent, + case .signOutGlobally(let data, let hostedUIError) = event.eventType else { + XCTFail("Expected SignOutEvent.signOutGlobally, got \(event)") + expectation.fulfill() + return + } + + guard let hostedUIError = hostedUIError, + case .configuration(let errorDescription, _, let serviceError) = hostedUIError.error else { + XCTFail("Expected AuthError.configuration") + expectation.fulfill() + return + } + + XCTAssertEqual(errorDescription, "Callback URL could not be retrieved") + XCTAssertEqual(data, signInData) + XCTAssertNil(serviceError) + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: hostedUIEnvironment + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testExecute_withoutHostedUIEnvironment_shouldThrowConfigurationError() async { + let expectation = expectation(description: "noHostedUIEnvironment") + let signInData = SignedInData.testData + let action = ShowHostedUISignOut( + signOutEvent: .testData, + signInData: signInData + ) + await action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? SignOutEvent, + case .signOutGlobally(let data, let hostedUIError) = event.eventType else { + XCTFail("Expected SignOutEvent.signOutGlobally, got \(event)") + expectation.fulfill() + return + } + + guard let hostedUIError = hostedUIError, + case .configuration(let errorDescription, _, let serviceError) = hostedUIError.error else { + XCTFail("Expected AuthError.configuration") + expectation.fulfill() + return + } + + XCTAssertEqual(data, signInData) + XCTAssertEqual(errorDescription, AuthPluginErrorConstants.configurationError) + XCTAssertNil(serviceError) + expectation.fulfill() + }, + environment: Defaults.makeDefaultAuthEnvironment( + userPoolFactory: identityProviderFactory, + hostedUIEnvironment: nil + ) + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testExecute_withInvalidUserPoolEnvironment_shouldThrowConfigurationError() async { + let expectation = expectation(description: "invalidUserPoolEnvironment") + let signInData = SignedInData.testData + let action = ShowHostedUISignOut( + signOutEvent: .testData, + signInData: signInData + ) + await action.execute( + withDispatcher: MockDispatcher { event in + guard let event = event as? SignOutEvent, + case .signOutGlobally(let data, let hostedUIError) = event.eventType else { + XCTFail("Expected SignOutEvent.signOutGlobally, got \(event)") + expectation.fulfill() + return + } + + guard let hostedUIError = hostedUIError, + case .configuration(let errorDescription, _, let serviceError) = hostedUIError.error else { + XCTFail("Expected AuthError.configuration") + expectation.fulfill() + return + } + + XCTAssertEqual(data, signInData) + XCTAssertEqual(errorDescription, AuthPluginErrorConstants.configurationError) + XCTAssertNil(serviceError) + expectation.fulfill() + }, + environment: MockInvalidEnvironment() + ) + + await fulfillment(of: [expectation], timeout: 1) + } + + private func validateDebugInformation(signInData: SignedInData, action: ShowHostedUISignOut) { + XCTAssertFalse(action.debugDescription.isEmpty) + guard let signInDataDictionary = action.debugDictionary["signInData"] as? [String: Any] else { + XCTFail("Expected signInData dictionary") + return + } + XCTAssertEqual(signInDataDictionary.count, signInData.debugDictionary.count) + + for key in signInDataDictionary.keys { + guard let left = signInDataDictionary[key] as? any Equatable, + let right = signInData.debugDictionary[key] as? any Equatable else { + continue + } + XCTAssertTrue(left.isEqual(to: right)) + } + } + + private var hostedUIEnvironment: HostedUIEnvironment { + BasicHostedUIEnvironment( + configuration: .init( + clientId: "clientId", + oauth: .init( + domain: "cognitodomain", + scopes: ["name"], + signInRedirectURI: "myapp://", + signOutRedirectURI: signOutRedirectURI + ) + ), + hostedUISessionFactory: { + MockHostedUISession(result: self.mockHostedUIResult) + }, + urlSessionFactory: { + URLSession.shared + }, + randomStringFactory: { + MockRandomStringGenerator( + mockString: "mockString", + mockUUID: "mockUUID" + ) + } + ) + } + + private func identityProviderFactory() throws -> CognitoUserPoolBehavior { + return MockIdentityProvider( + mockInitiateAuthResponse: { _ in + return InitiateAuthOutputResponse( + authenticationResult: .init( + accessToken: "accessTokenNew", + expiresIn: 100, + idToken: "idTokenNew", + refreshToken: "refreshTokenNew") + ) + } + ) + } +} + +private extension Equatable { + func isEqual(to other: any Equatable) -> Bool { + guard let other = other as? Self else { + return false + } + return self == other + } +} diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/CognitoASFTests/CognitoUserPoolASFTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/CognitoASFTests/CognitoUserPoolASFTests.swift new file mode 100644 index 0000000000..3e7a6e9110 --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/CognitoASFTests/CognitoUserPoolASFTests.swift @@ -0,0 +1,45 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import AWSCognitoAuthPlugin +import XCTest + +class CognitoUserPoolASFTests: XCTestCase { + private var pool: CognitoUserPoolASF! + + override func setUp() { + pool = CognitoUserPoolASF() + } + + override func tearDown() { + pool = nil + } + + func testUserContextData_shouldReturnData() throws { + let result = try pool.userContextData( + for: "TestUser", + deviceInfo: ASFDeviceInfo(id: "mockedDevice"), + appInfo: ASFAppInfo(), + configuration: .testData + ) + XCTAssertFalse(result.isEmpty) + } + + func testcalculateSecretHash_withInvalidClientId_shouldThrowHashKeyError() { + do { + let result = try pool.calculateSecretHash( + contextJson: "contextJson", + clientId: "🕺🏼" + ) + XCTFail("Expected ASFError.hashKey, got \(result)") + } catch let error as ASFError { + XCTAssertEqual(error, .hashKey) + } catch { + XCTFail("Expected ASFError.hashKey, for \(error)") + } + } +} diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/EscapeHatchTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/EscapeHatchTests.swift index d385c628d8..acf3ec1c0d 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/EscapeHatchTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ConfigurationTests/EscapeHatchTests.swift @@ -6,143 +6,116 @@ // import XCTest -@testable import Amplify +@testable import func AmplifyTestCommon.XCTAssertThrowFatalError +import enum Amplify.JSONValue @testable import AWSCognitoAuthPlugin -class EscapeHatchTests: XCTestCase { - - let skipBrokenTests = true - - override func tearDown() async throws { - await Amplify.reset() - } +class EscapeHatchTests: XCTestCase { /// Test escape hatch with valid config for user pool and identity pool /// - /// - Given: Given valid config for user pool and identity pool + /// - Given: A AWSCognitoAuthPlugin configured with User Pool and Identity Pool /// - When: - /// - I configure auth with the given configuration and call getEscapeHatch + /// - I call getEscapeHatch /// - Then: - /// - I should get back user pool and identity pool clients + /// - I should get back both the User Pool and Identity Pool clients /// func testEscapeHatchWithUserPoolAndIdentityPool() throws { - if skipBrokenTests { - throw XCTSkip("TODO: fix this test") - } - - let plugin = AWSCognitoAuthPlugin() - try Amplify.add(plugin: plugin) - - let expectation = expectation(description: "Should get service") - let categoryConfig = AuthCategoryConfiguration(plugins: [ - "awsCognitoAuthPlugin": [ - "CredentialsProvider": ["CognitoIdentity": ["Default": - ["PoolId": "xx", - "Region": "us-east-1"] - ]], - "CognitoUserPool": ["Default": [ + let configuration: JSONValue = [ + "CredentialsProvider": [ + "CognitoIdentity": [ + "Default": [ + "PoolId": "xx", + "Region": "us-east-1" + ] + ] + ], + "CognitoUserPool": [ + "Default": [ "PoolId": "xx", "Region": "us-east-1", "AppClientId": "xx", - "AppClientSecret": "xx"]] + "AppClientSecret": "xx" + ] ] - ]) - let amplifyConfig = AmplifyConfiguration(auth: categoryConfig) - try Amplify.configure(amplifyConfig) - let internalPlugin = try Amplify.Auth.getPlugin( - for: "awsCognitoAuthPlugin" - ) as! AWSCognitoAuthPlugin - let service = internalPlugin.getEscapeHatch() - switch service { - case .userPool: - XCTFail("Should return userPoolAndIdentityPool") - case .identityPool: - XCTFail("Should return userPoolAndIdentityPool") - case .userPoolAndIdentityPool: - expectation.fulfill() + ] + let plugin = AWSCognitoAuthPlugin() + try plugin.configure(using: configuration) + let escapeHatch = plugin.getEscapeHatch() + guard case .userPoolAndIdentityPool = escapeHatch else { + XCTFail("Expected .userPoolAndIdentityPool, got \(escapeHatch)") + return } - wait(for: [expectation], timeout: 1) } /// Test escape hatch with valid config for only identity pool /// - /// - Given: Given valid config for only identity pool + /// - Given: A AWSCognitoAuthPlugin configured with only Identity Pool /// - When: - /// - I configure auth with the given configuration and invoke getEscapeHatch + /// - I call getEscapeHatch /// - Then: - /// - I should get back only identity pool client + /// - I should get back only the Identity Pool client /// func testEscapeHatchWithOnlyIdentityPool() throws { - if skipBrokenTests { - throw XCTSkip("TODO: fix this test") - } - - let plugin = AWSCognitoAuthPlugin() - try Amplify.add(plugin: plugin) - - let categoryConfig = AuthCategoryConfiguration(plugins: [ - "awsCognitoAuthPlugin": [ - "CredentialsProvider": ["CognitoIdentity": ["Default": - ["PoolId": "cc", - "Region": "us-east-1"] - ]] + let configuration: JSONValue = [ + "CredentialsProvider": [ + "CognitoIdentity": [ + "Default": [ + "PoolId": "xx", + "Region": "us-east-1" + ] + ] ] - ]) - let amplifyConfig = AmplifyConfiguration(auth: categoryConfig) - try Amplify.configure(amplifyConfig) - let internalPlugin = try Amplify.Auth.getPlugin( - for: "awsCognitoAuthPlugin" - ) as! AWSCognitoAuthPlugin - let service = internalPlugin.getEscapeHatch() - switch service { - case .userPool: - XCTFail("Should return identityPool") - case .userPoolAndIdentityPool: - XCTFail("Should return identityPool") - case .identityPool: - print("") + ] + let plugin = AWSCognitoAuthPlugin() + try plugin.configure(using: configuration) + let escapeHatch = plugin.getEscapeHatch() + guard case .identityPool = escapeHatch else { + XCTFail("Expected .identityPool, got \(escapeHatch)") + return } } /// Test escape hatch with valid config for only user pool /// - /// - Given: Given valid config for only user pool + /// - Given: A AWSCognitoAuthPlugin configured with only User Pool /// - When: - /// - I configure auth with the given configuration and invoke getEscapeHatch + /// - I call getEscapeHatch /// - Then: - /// - I should get the Cognito User pool client + /// - I should get only the User Pool client /// func testEscapeHatchWithOnlyUserPool() throws { - if skipBrokenTests { - throw XCTSkip("TODO: fix this test") - } - - let plugin = AWSCognitoAuthPlugin() - try Amplify.add(plugin: plugin) - - let categoryConfig = AuthCategoryConfiguration(plugins: [ - "awsCognitoAuthPlugin": [ - "CognitoUserPool": ["Default": [ + let configuration: JSONValue = [ + "CognitoUserPool": [ + "Default": [ "PoolId": "xx", "Region": "us-east-1", "AppClientId": "xx", - "AppClientSecret": "xx"]] + "AppClientSecret": "xx" + ] ] - ]) - let amplifyConfig = AmplifyConfiguration(auth: categoryConfig) - try Amplify.configure(amplifyConfig) - let internalPlugin = try Amplify.Auth.getPlugin( - for: "awsCognitoAuthPlugin" - ) as! AWSCognitoAuthPlugin - let service = internalPlugin.getEscapeHatch() - switch service { - case .userPool: - break - case .identityPool: - XCTFail("Should return userPool") - case .userPoolAndIdentityPool: - XCTFail("Should return userPool") + ] + let plugin = AWSCognitoAuthPlugin() + try plugin.configure(using: configuration) + let escapeHatch = plugin.getEscapeHatch() + guard case .userPool = escapeHatch else { + XCTFail("Expected .userPool, got \(escapeHatch)") + return + } + } + + /// Test escape hatch without a valid configuration + /// + /// - Given: A AWSCognitoAuthPlugin plugin without being configured + /// - When: + /// - I call getEscapeHatch + /// - Then: + /// - A fatalError is thrown + /// + func testEscapeHatchWithoutConfiguration() throws { + let plugin = AWSCognitoAuthPlugin() + try XCTAssertThrowFatalError { + _ = plugin.getEscapeHatch() } } - } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/AWSAuthCognitoSessionTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/AWSAuthCognitoSessionTests.swift index fdb8862284..53075a7adc 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/AWSAuthCognitoSessionTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/AWSAuthCognitoSessionTests.swift @@ -26,8 +26,7 @@ class AWSAuthCognitoSessionTests: XCTestCase { let error = AuthError.unknown("", nil) let tokens = AWSCognitoUserPoolTokens(idToken: CognitoAuthTestHelper.buildToken(for: tokenData), accessToken: CognitoAuthTestHelper.buildToken(for: tokenData), - refreshToken: "refreshToken", - expiresIn: 121) + refreshToken: "refreshToken") let session = AWSAuthCognitoSession(isSignedIn: true, identityIdResult: .failure(error), @@ -53,8 +52,7 @@ class AWSAuthCognitoSessionTests: XCTestCase { let error = AuthError.unknown("", nil) let tokens = AWSCognitoUserPoolTokens(idToken: CognitoAuthTestHelper.buildToken(for: tokenData), accessToken: CognitoAuthTestHelper.buildToken(for: tokenData), - refreshToken: "refreshToken", - expiresIn: 121) + refreshToken: "refreshToken") let session = AWSAuthCognitoSession(isSignedIn: true, identityIdResult: .failure(error), @@ -65,4 +63,161 @@ class AWSAuthCognitoSessionTests: XCTestCase { XCTAssertFalse(cognitoTokens.doesExpire()) } + func testGetUserSub_shouldReturnResult() { + let tokenData = [ + "sub": "1234567890", + "name": "John Doe", + "iat": "1516239022", + "exp": String(Date(timeIntervalSinceNow: 121).timeIntervalSince1970) + ] + + let error = AuthError.unknown("", nil) + let tokens = AWSCognitoUserPoolTokens( + idToken: CognitoAuthTestHelper.buildToken(for: tokenData), + accessToken: CognitoAuthTestHelper.buildToken(for: tokenData), + refreshToken: "refreshToken" + ) + + let session = AWSAuthCognitoSession( + isSignedIn: true, + identityIdResult: .failure(error), + awsCredentialsResult: .failure(error), + cognitoTokensResult: .success(tokens) + ) + + guard case .success(let userSub) = session.getUserSub() else { + XCTFail("Unable to retrieve userSub") + return + } + XCTAssertEqual(userSub, "1234567890") + } + + func testGetUserSub_withoutSub_shouldReturnError() { + let tokenData = [ + "name": "John Doe", + "iat": "1516239022", + "exp": String(Date(timeIntervalSinceNow: 121).timeIntervalSince1970) + ] + + let error = AuthError.unknown("", nil) + let tokens = AWSCognitoUserPoolTokens( + idToken: CognitoAuthTestHelper.buildToken(for: tokenData), + accessToken: CognitoAuthTestHelper.buildToken(for: tokenData), + refreshToken: "refreshToken" + ) + + let session = AWSAuthCognitoSession( + isSignedIn: true, + identityIdResult: .failure(error), + awsCredentialsResult: .failure(error), + cognitoTokensResult: .success(tokens) + ) + + guard case .failure(let error) = session.getUserSub(), + case .unknown(let errorDescription, _) = error else { + XCTFail("Expected AuthError.unknown") + return + } + + XCTAssertEqual(errorDescription, "Could not retreive user sub from the fetched Cognito tokens.") + } + + func testGetUserSub_signedOut_shouldReturnError() { + let error = AuthError.signedOut("", "", nil) + let session = AWSAuthCognitoSession( + isSignedIn: false, + identityIdResult: .failure(error), + awsCredentialsResult: .failure(error), + cognitoTokensResult: .failure(error) + ) + + guard case .failure(let error) = session.getUserSub(), + case .signedOut(let errorDescription, let recoverySuggestion, _) = error else { + XCTFail("Expected AuthError.signedOut") + return + } + + XCTAssertEqual(errorDescription, AuthPluginErrorConstants.userSubSignOutError.errorDescription) + XCTAssertEqual(recoverySuggestion, AuthPluginErrorConstants.userSubSignOutError.recoverySuggestion) + } + + func testGetUserSub_serviceError_shouldReturnError() { + let serviceError = AuthError.service("Something went wrong", "Try again", nil) + let session = AWSAuthCognitoSession( + isSignedIn: false, + identityIdResult: .failure(serviceError), + awsCredentialsResult: .failure(serviceError), + cognitoTokensResult: .failure(serviceError) + ) + + guard case .failure(let error) = session.getUserSub() else { + XCTFail("Expected AuthError.signedOut") + return + } + + XCTAssertEqual(error, serviceError) + } + + func testSessionsAreEqual() { + let expiration = Date(timeIntervalSinceNow: 121) + let tokenData1 = [ + "sub": "1234567890", + "name": "John Doe", + "iat": "1516239022", + "exp": String(expiration.timeIntervalSince1970) + ] + + let credentials1 = AuthAWSCognitoCredentials( + accessKeyId: "accessKeyId", + secretAccessKey: "secretAccessKey", + sessionToken: "sessionToken", + expiration: expiration + ) + + let tokens1 = AWSCognitoUserPoolTokens( + idToken: CognitoAuthTestHelper.buildToken(for: tokenData1), + accessToken: CognitoAuthTestHelper.buildToken(for: tokenData1), + refreshToken: "refreshToken" + ) + + let session1 = AWSAuthCognitoSession( + isSignedIn: true, + identityIdResult: .success("identityId"), + awsCredentialsResult: .success(credentials1), + cognitoTokensResult: .success(tokens1) + ) + + let tokenData2 = [ + "sub": "1234567890", + "name": "John Doe", + "iat": "1516239022", + "exp": String(expiration.timeIntervalSince1970) + ] + + let credentials2 = AuthAWSCognitoCredentials( + accessKeyId: "accessKeyId", + secretAccessKey: "secretAccessKey", + sessionToken: "sessionToken", + expiration: expiration + ) + + let tokens2 = AWSCognitoUserPoolTokens( + idToken: CognitoAuthTestHelper.buildToken(for: tokenData2), + accessToken: CognitoAuthTestHelper.buildToken(for: tokenData2), + refreshToken: "refreshToken" + ) + + let session2 = AWSAuthCognitoSession( + isSignedIn: true, + identityIdResult: .success("identityId"), + awsCredentialsResult: .success(credentials2), + cognitoTokensResult: .success(tokens2) + ) + + XCTAssertEqual(session1, session2) + XCTAssertEqual(session1.debugDictionary.count, session2.debugDictionary.count) + for key in session1.debugDictionary.keys where key != "AWS Credentials" { + XCTAssertEqual(session1.debugDictionary[key] as? String, session2.debugDictionary[key] as? String) + } + } } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/HostedUIASWebAuthenticationSessionTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/HostedUIASWebAuthenticationSessionTests.swift new file mode 100644 index 0000000000..14a8c8c367 --- /dev/null +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/HostedUIASWebAuthenticationSessionTests.swift @@ -0,0 +1,233 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#if os(iOS) || os(macOS) +import Amplify +import AuthenticationServices +@testable import AWSCognitoAuthPlugin +import XCTest + +class HostedUIASWebAuthenticationSessionTests: XCTestCase { + private var session: HostedUIASWebAuthenticationSession! + private var factory: ASWebAuthenticationSessionFactory! + + override func setUp() { + session = HostedUIASWebAuthenticationSession() + factory = ASWebAuthenticationSessionFactory() + session.authenticationSessionFactory = factory.createSession(url:callbackURLScheme:completionHandler:) + } + + override func tearDown() { + session = nil + factory = nil + } + + func testShowHostedUI_withUrlInCallback_withQueryItems_shouldReturnQueryItems() { + let expectation = expectation(description: "showHostedUI") + factory.mockedURL = createURL(queryItems: [.init(name: "name", value: "value")]) + + session.showHostedUI() { result in + do { + let queryItems = try result.get() + XCTAssertEqual(queryItems.count, 1) + XCTAssertEqual(queryItems.first?.name, "name") + XCTAssertEqual(queryItems.first?.value, "value") + } catch { + XCTFail("Expected .success(queryItems), got \(result)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testShowHostedUI_withUrlInCallback_withoutQueryItems_shouldReturnEmptyQueryItems() { + let expectation = expectation(description: "showHostedUI") + factory.mockedURL = createURL() + + session.showHostedUI() { result in + do { + let queryItems = try result.get() + XCTAssertTrue(queryItems.isEmpty) + } catch { + XCTFail("Expected .success(queryItems), got \(result)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testShowHostedUI_withUrlInCallback_withErrorInQueryItems_shouldReturnServiceMessageError() { + let expectation = expectation(description: "showHostedUI") + factory.mockedURL = createURL( + queryItems: [ + .init(name: "error", value: "Error."), + .init(name: "error_description", value: "Something went wrong") + ] + ) + + session.showHostedUI() { result in + do { + _ = try result.get() + XCTFail("Expected failure(.serviceMessage), got \(result)") + } catch let error as HostedUIError { + if case .serviceMessage(let message) = error { + XCTAssertEqual(message, "Error. Something went wrong") + } else { + XCTFail("Expected HostedUIError.serviceMessage, got \(error)") + } + } catch { + XCTFail("Expected HostedUIError.serviceMessage, got \(error)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testShowHostedUI_withASWebAuthenticationSessionErrors_shouldReturnRightError() { + let errorMap: [ASWebAuthenticationSessionError.Code: HostedUIError] = [ + .canceledLogin: .cancelled, + .presentationContextNotProvided: .invalidContext, + .presentationContextInvalid: .invalidContext + ] + + let errorCodes: [ASWebAuthenticationSessionError.Code] = [ + .canceledLogin, + .presentationContextNotProvided, + .presentationContextInvalid, + .init(rawValue: 500)! + ] + + for code in errorCodes { + factory.mockedError = ASWebAuthenticationSessionError(code) + let expectedError = errorMap[code] ?? .unknown + let expectation = expectation(description: "showHostedUI for error \(code)") + session.showHostedUI() { result in + do { + _ = try result.get() + XCTFail("Expected failure(.\(expectedError)), got \(result)") + } catch let error as HostedUIError { + XCTAssertEqual(error, expectedError) + } catch { + XCTFail("Expected HostedUIError.\(expectedError), got \(error)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + } + + func testShowHostedUI_withOtherError_shouldReturnUnknownError() { + factory.mockedError = CancellationError() + let expectation = expectation(description: "showHostedUI") + session.showHostedUI() { result in + do { + _ = try result.get() + XCTFail("Expected failure(.unknown), got \(result)") + } catch let error as HostedUIError { + XCTAssertEqual(error, .unknown) + } catch { + XCTFail("Expected HostedUIError.unknown, got \(error)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } + + private func createURL(queryItems: [URLQueryItem] = []) -> URL { + var components = URLComponents(string: "https://test.com")! + components.queryItems = queryItems + return components.url! + } +} + +class ASWebAuthenticationSessionFactory { + var mockedURL: URL? + var mockedError: Error? + + func createSession( + url URL: URL, + callbackURLScheme: String?, + completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler + ) -> ASWebAuthenticationSession { + let session = MockASWebAuthenticationSession( + url: URL, + callbackURLScheme: callbackURLScheme, + completionHandler: completionHandler + ) + session.mockedURL = mockedURL + session.mockedError = mockedError + return session + } +} + +class MockASWebAuthenticationSession: ASWebAuthenticationSession { + private var callback: ASWebAuthenticationSession.CompletionHandler + override init( + url URL: URL, + callbackURLScheme: String?, + completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler + ) { + self.callback = completionHandler + super.init( + url: URL, + callbackURLScheme: callbackURLScheme, + completionHandler: completionHandler + ) + } + + var mockedURL: URL? = nil + var mockedError: Error? = nil + override func start() -> Bool { + callback(mockedURL, mockedError) + return presentationContextProvider?.presentationAnchor(for: self) != nil + } +} + +extension HostedUIASWebAuthenticationSession { + func showHostedUI(callback: @escaping (Result<[URLQueryItem], HostedUIError>) -> Void) { + showHostedUI( + url: URL(string: "https://test.com")!, + callbackScheme: "https", + inPrivate: false, + presentationAnchor: nil, + callback: callback) + } +} +#else + +@testable import AWSCognitoAuthPlugin +import XCTest + +class HostedUIASWebAuthenticationSessionTests: XCTestCase { + func testShowHostedUI_shouldThrowServiceError() { + let expectation = expectation(description: "showHostedUI") + let session = HostedUIASWebAuthenticationSession() + session.showHostedUI( + url: URL(string: "https://test.com")!, + callbackScheme: "https", + inPrivate: false, + presentationAnchor: nil + ) { result in + do { + _ = try result.get() + XCTFail("Expected failure(.serviceMessage), got \(result)") + } catch let error as HostedUIError { + if case .serviceMessage(let message) = error { + XCTAssertEqual(message, "HostedUI is only available in iOS and macOS") + } else { + XCTFail("Expected HostedUIError.serviceMessage, got \(error)") + } + } catch { + XCTFail("Expected HostedUIError.serviceMessage, got \(error)") + } + expectation.fulfill() + } + waitForExpectations(timeout: 1) + } +} + +#endif