diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 70066bd2241017e30bc4702ec1127cc25fed1688..57081f3ef7c5ae6abcf0cb130aa994d8a6460abc 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -57,6 +57,7 @@ let package = Package( .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXDatabase", package: "client-ios-db"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXModels", package: "client-ios-db"), ], swiftSettings: swiftSettings diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift new file mode 100644 index 0000000000000000000000000000000000000000..c6e03cbedb7f1d0020d11a94c6fc0172a5fc5247 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift @@ -0,0 +1,49 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct AuthCallbackHandler { + public typealias OnError = (Error) -> Void + + public var run: (@escaping OnError) -> Cancellable + + public func callAsFunction(onError: @escaping OnError) -> Cancellable { + run(onError) + } +} + +extension AuthCallbackHandler { + public static func live( + messenger: Messenger, + handleRequest: AuthCallbackHandlerRequest, + handleConfirm: AuthCallbackHandlerConfirm, + handleReset: AuthCallbackHandlerReset + ) -> AuthCallbackHandler { + AuthCallbackHandler { onError in + messenger.registerAuthCallbacks(.init { callback in + do { + switch callback { + case .request(let contact, _, _, _): + try handleRequest(contact) + + case .confirm(let contact, _, _, _): + try handleConfirm(contact) + + case .reset(let contact, _, _, _): + try handleReset(contact) + } + } catch { + onError(error) + } + }) + } + } +} + +extension AuthCallbackHandler { + public static let unimplemented = AuthCallbackHandler( + run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {}) + ) +} diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift new file mode 100644 index 0000000000000000000000000000000000000000..2aa6787fff741651d649fb8f88397669b448d92f --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift @@ -0,0 +1,31 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXModels + +public struct AuthCallbackHandlerConfirm { + public var run: (XXClient.Contact) throws -> Void + + public func callAsFunction(_ contact: XXClient.Contact) throws { + try run(contact) + } +} + +extension AuthCallbackHandlerConfirm { + public static func live(db: DBManagerGetDB) -> AuthCallbackHandlerConfirm { + AuthCallbackHandlerConfirm { xxContact in + let id = try xxContact.getId() + guard var dbContact = try db().fetchContacts(.init(id: [id])).first else { + return + } + dbContact.authStatus = .friend + dbContact = try db().saveContact(dbContact) + } + } +} + +extension AuthCallbackHandlerConfirm { + public static let unimplemented = AuthCallbackHandlerConfirm( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift new file mode 100644 index 0000000000000000000000000000000000000000..d8ac3d18a496b64427b293fcc476c341d93353cb --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift @@ -0,0 +1,54 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct AuthCallbackHandlerRequest { + public var run: (XXClient.Contact) throws -> Void + + public func callAsFunction(_ contact: XXClient.Contact) throws { + try run(contact) + } +} + +extension AuthCallbackHandlerRequest { + public static func live( + db: DBManagerGetDB, + messenger: Messenger, + now: @escaping () -> Date + ) -> AuthCallbackHandlerRequest { + AuthCallbackHandlerRequest { xxContact in + let id = try xxContact.getId() + guard try db().fetchContacts(.init(id: [id])).isEmpty else { + return + } + var dbContact = XXModels.Contact(id: id) + dbContact.marshaled = xxContact.data + dbContact.username = try xxContact.getFact(.username)?.value + dbContact.email = try xxContact.getFact(.email)?.value + dbContact.phone = try xxContact.getFact(.phone)?.value + dbContact.authStatus = .verificationInProgress + dbContact.createdAt = now() + dbContact = try db().saveContact(dbContact) + + do { + try messenger.waitForNetwork() + try messenger.waitForNodes() + let verified = try messenger.verifyContact(xxContact) + dbContact.authStatus = verified ? .verified : .verificationFailed + dbContact = try db().saveContact(dbContact) + } catch { + dbContact.authStatus = .verificationFailed + dbContact = try db().saveContact(dbContact) + throw error + } + } + } +} + +extension AuthCallbackHandlerRequest { + public static let unimplemented = AuthCallbackHandlerRequest( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift new file mode 100644 index 0000000000000000000000000000000000000000..729fe13eff8ee42ed1a12ba2ce03d082e5b2cfa8 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift @@ -0,0 +1,31 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXModels + +public struct AuthCallbackHandlerReset { + public var run: (XXClient.Contact) throws -> Void + + public func callAsFunction(_ contact: XXClient.Contact) throws { + try run(contact) + } +} + +extension AuthCallbackHandlerReset { + public static func live(db: DBManagerGetDB) -> AuthCallbackHandlerReset { + AuthCallbackHandlerReset { xxContact in + let id = try xxContact.getId() + guard var dbContact = try db().fetchContacts(.init(id: [id])).first else { + return + } + dbContact.authStatus = .stranger + dbContact = try db().saveContact(dbContact) + } + } +} + +extension AuthCallbackHandlerReset { + public static let unimplemented = AuthCallbackHandlerReset( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 61bfba1f3163cf119e610606f9d89ea67a122171..5267fcc3f1a42445702386278b8fbeeb600785ae 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -16,6 +16,16 @@ extension AppEnvironment { let dbManager = DBManager.live() let messengerEnv = MessengerEnvironment.live() let messenger = Messenger.live(messengerEnv) + let authHandler = AuthCallbackHandler.live( + messenger: messenger, + handleRequest: .live( + db: dbManager.getDB, + messenger: messenger, + now: Date.init + ), + handleConfirm: .live(db: dbManager.getDB), + handleReset: .live(db: dbManager.getDB) + ) let mainQueue = DispatchQueue.main.eraseToAnyScheduler() let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler() @@ -53,6 +63,7 @@ extension AppEnvironment { HomeEnvironment( messenger: messenger, dbManager: dbManager, + authHandler: authHandler, mainQueue: mainQueue, bgQueue: bgQueue, register: { diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index faa5757c43a4e22a9c8c3b7dd7a99ee83e059a84..e28008f39e924af1c5816bb33c9e4dce11798476 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -6,12 +6,15 @@ import ContactsFeature import Foundation import RegisterFeature import UserSearchFeature +import XCTestDynamicOverlay import XXClient import XXMessengerClient +import XXModels public struct HomeState: Equatable { public init( failure: String? = nil, + authFailure: String? = nil, isNetworkHealthy: Bool? = nil, networkNodesReport: NodeRegistrationReport? = nil, isDeletingAccount: Bool = false, @@ -21,6 +24,7 @@ public struct HomeState: Equatable { userSearch: UserSearchState? = nil ) { self.failure = failure + self.authFailure = authFailure self.isNetworkHealthy = isNetworkHealthy self.isDeletingAccount = isDeletingAccount self.alert = alert @@ -30,6 +34,7 @@ public struct HomeState: Equatable { } public var failure: String? + public var authFailure: String? public var isNetworkHealthy: Bool? public var networkNodesReport: NodeRegistrationReport? public var isDeletingAccount: Bool @@ -47,6 +52,13 @@ public enum HomeAction: Equatable { case failure(NSError) } + public enum AuthHandler: Equatable { + case start + case stop + case failure(NSError) + case failureDismissed + } + public enum NetworkMonitor: Equatable { case start case stop @@ -62,6 +74,7 @@ public enum HomeAction: Equatable { } case messenger(Messenger) + case authHandler(AuthHandler) case networkMonitor(NetworkMonitor) case deleteAccount(DeleteAccount) case didDismissAlert @@ -79,6 +92,7 @@ public struct HomeEnvironment { public init( messenger: Messenger, dbManager: DBManager, + authHandler: AuthCallbackHandler, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, register: @escaping () -> RegisterEnvironment, @@ -87,6 +101,7 @@ public struct HomeEnvironment { ) { self.messenger = messenger self.dbManager = dbManager + self.authHandler = authHandler self.mainQueue = mainQueue self.bgQueue = bgQueue self.register = register @@ -96,6 +111,7 @@ public struct HomeEnvironment { public var messenger: Messenger public var dbManager: DBManager + public var authHandler: AuthCallbackHandler public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> public var register: () -> RegisterEnvironment @@ -107,6 +123,7 @@ extension HomeEnvironment { public static let unimplemented = HomeEnvironment( messenger: .unimplemented, dbManager: .unimplemented, + authHandler: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, register: { .unimplemented }, @@ -119,10 +136,12 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> { state, action, env in enum NetworkHealthEffectId {} enum NetworkNodesEffectId {} + enum AuthCallbacksEffectId {} switch action { case .messenger(.start): return .merge( + Effect(value: .authHandler(.start)), Effect(value: .networkMonitor(.stop)), Effect.result { do { @@ -160,6 +179,29 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> state.failure = error.localizedDescription return .none + case .authHandler(.start): + return Effect.run { subscriber in + let cancellable = env.authHandler(onError: { error in + subscriber.send(.authHandler(.failure(error as NSError))) + }) + return AnyCancellable { cancellable.cancel() } + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + .cancellable(id: AuthCallbacksEffectId.self, cancelInFlight: true) + + case .authHandler(.stop): + return .cancel(id: AuthCallbacksEffectId.self) + + case .authHandler(.failure(let error)): + state.authFailure = error.localizedDescription + return .none + + case .authHandler(.failureDismissed): + state.authFailure = nil + return .none + case .networkMonitor(.start): return .merge( Effect.run { subscriber in diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift index f1abfb2b9ebf3d4b2ad4d20aa477b2f49d96fb80..f6a964896e6e67544e693d83cb26378756414b86 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -15,12 +15,14 @@ public struct HomeView: View { struct ViewState: Equatable { var failure: String? + var authFailure: String? var isNetworkHealthy: Bool? var networkNodesReport: NodeRegistrationReport? var isDeletingAccount: Bool init(state: HomeState) { failure = state.failure + authFailure = state.authFailure isNetworkHealthy = state.isNetworkHealthy isDeletingAccount = state.isDeletingAccount networkNodesReport = state.networkNodesReport @@ -31,21 +33,32 @@ public struct HomeView: View { WithViewStore(store, observe: ViewState.init) { viewStore in NavigationView { Form { - Section { - if let failure = viewStore.failure { + if let failure = viewStore.failure { + Section { Text(failure) Button { viewStore.send(.messenger(.start)) } label: { Text("Retry") } - } - } header: { - if viewStore.failure != nil { + } header: { Text("Error") } } + if let authFailure = viewStore.authFailure { + Section { + Text(authFailure) + Button { + viewStore.send(.authHandler(.failureDismissed)) + } label: { + Text("Dismiss") + } + } header: { + Text("Auth Error") + } + } + Section { HStack { Text("Health") diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..1a1b5f19573669403a069bd4e3cecd2e11347e73 --- /dev/null +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift @@ -0,0 +1,54 @@ +import CustomDump +import XCTest +import XXModels +import XXClient +@testable import AppCore + +final class AuthCallbackHandlerConfirmTests: XCTestCase { + func testConfirm() throws { + var didFetchContacts: [XXModels.Contact.Query] = [] + var didSaveContact: [XXModels.Contact] = [] + + let dbContact = XXModels.Contact( + id: "id".data(using: .utf8)!, + authStatus: .requested + ) + let confirm = AuthCallbackHandlerConfirm.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { query in + didFetchContacts.append(query) + return [dbContact] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + } + ) + var xxContact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + xxContact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + + try confirm(xxContact) + + XCTAssertNoDifference(didFetchContacts, [.init(id: ["id".data(using: .utf8)!])]) + var expectedSavedContact = dbContact + expectedSavedContact.authStatus = .friend + XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) + } + + func testConfirmWhenContactNotInDatabase() throws { + let confirm = AuthCallbackHandlerConfirm.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { _ in [] } + return db + } + ) + var contact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + + try confirm(contact) + } +} diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..65786884bfbe0821343509f20bd1f0942998768d --- /dev/null +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift @@ -0,0 +1,182 @@ +import CustomDump +import XCTest +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels +@testable import AppCore + +final class AuthCallbackHandlerRequestTests: XCTestCase { + func testRequestFromNewContact() throws { + let now = Date() + var didFetchContacts: [XXModels.Contact.Query] = [] + var didVerifyContact: [XXClient.Contact] = [] + var didSaveContact: [XXModels.Contact] = [] + + var messenger: Messenger = .unimplemented + messenger.waitForNetwork.run = { _ in } + messenger.waitForNodes.run = { _, _, _, _ in } + messenger.verifyContact.run = { contact in + didVerifyContact.append(contact) + return true + } + + let request = AuthCallbackHandlerRequest.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { query in + didFetchContacts.append(query) + return [] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + }, + messenger: messenger, + now: { now } + ) + var xxContact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + xxContact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + xxContact.getFactsFromContact.run = { _ in + [ + Fact(type: .username, value: "username"), + Fact(type: .email, value: "email"), + Fact(type: .phone, value: "phone"), + ] + } + + try request(xxContact) + + XCTAssertNoDifference(didFetchContacts, [.init(id: ["id".data(using: .utf8)!])]) + XCTAssertNoDifference(didSaveContact, [ + .init( + id: "id".data(using: .utf8)!, + marshaled: "contact".data(using: .utf8)!, + username: "username", + email: "email", + phone: "phone", + authStatus: .verificationInProgress, + createdAt: now + ), + .init( + id: "id".data(using: .utf8)!, + marshaled: "contact".data(using: .utf8)!, + username: "username", + email: "email", + phone: "phone", + authStatus: .verified, + createdAt: now + ) + ]) + } + + func testRequestWhenContactInDatabase() throws { + let request = AuthCallbackHandlerRequest.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { _ in [.init(id: "id".data(using: .utf8)!)] } + return db + }, + messenger: .unimplemented, + now: XCTUnimplemented("now", placeholder: Date()) + ) + var contact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + + try request(contact) + } + + func testRequestFromNewContactVerificationFalse() throws { + let now = Date() + var didSaveContact: [XXModels.Contact] = [] + + var messenger: Messenger = .unimplemented + messenger.waitForNetwork.run = { _ in } + messenger.waitForNodes.run = { _, _, _, _ in } + messenger.verifyContact.run = { _ in false } + + let request = AuthCallbackHandlerRequest.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { query in return [] } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + }, + messenger: messenger, + now: { now } + ) + var xxContact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + xxContact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + xxContact.getFactsFromContact.run = { _ in [] } + + try request(xxContact) + + XCTAssertNoDifference(didSaveContact, [ + .init( + id: "id".data(using: .utf8)!, + marshaled: "contact".data(using: .utf8)!, + authStatus: .verificationInProgress, + createdAt: now + ), + .init( + id: "id".data(using: .utf8)!, + marshaled: "contact".data(using: .utf8)!, + authStatus: .verificationFailed, + createdAt: now + ) + ]) + } + + func testRequestFromNewContactVerificationFailure() throws { + struct Failure: Error, Equatable {} + let failure = Failure() + let now = Date() + var didSaveContact: [XXModels.Contact] = [] + + var messenger: Messenger = .unimplemented + messenger.waitForNetwork.run = { _ in } + messenger.waitForNodes.run = { _, _, _, _ in } + messenger.verifyContact.run = { _ in throw failure } + + let request = AuthCallbackHandlerRequest.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { query in return [] } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + }, + messenger: messenger, + now: { now } + ) + var xxContact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + xxContact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + xxContact.getFactsFromContact.run = { _ in [] } + + XCTAssertThrowsError(try request(xxContact)) { error in + XCTAssertNoDifference(error as? Failure, failure) + } + + XCTAssertNoDifference(didSaveContact, [ + .init( + id: "id".data(using: .utf8)!, + marshaled: "contact".data(using: .utf8)!, + authStatus: .verificationInProgress, + createdAt: now + ), + .init( + id: "id".data(using: .utf8)!, + marshaled: "contact".data(using: .utf8)!, + authStatus: .verificationFailed, + createdAt: now + ) + ]) + } +} diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..9a4407bf758a0c0ba922c154d199b758dea701be --- /dev/null +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift @@ -0,0 +1,54 @@ +import CustomDump +import XCTest +import XXModels +import XXClient +@testable import AppCore + +final class AuthCallbackHandlerResetTests: XCTestCase { + func testReset() throws { + var didFetchContacts: [XXModels.Contact.Query] = [] + var didSaveContact: [XXModels.Contact] = [] + + let dbContact = XXModels.Contact( + id: "id".data(using: .utf8)!, + authStatus: .friend + ) + let reset = AuthCallbackHandlerReset.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { query in + didFetchContacts.append(query) + return [dbContact] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + } + ) + var xxContact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + xxContact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + + try reset(xxContact) + + XCTAssertNoDifference(didFetchContacts, [.init(id: ["id".data(using: .utf8)!])]) + var expectedSavedContact = dbContact + expectedSavedContact.authStatus = .stranger + XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) + } + + func testResetWhenContactNotInDatabase() throws { + let reset = AuthCallbackHandlerReset.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { _ in [] } + return db + } + ) + var contact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + + try reset(contact) + } +} diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..2eddbbb9e3b9d7524e6ea0e1e65e481cbe921437 --- /dev/null +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerTests.swift @@ -0,0 +1,114 @@ +import CustomDump +import XCTest +import XXClient +import XXMessengerClient +@testable import AppCore + +final class AuthCallbackHandlerTests: XCTestCase { + func testCallbackHandling() throws { + struct TestState: Equatable { + var didRegisterAuthCallbacks = 0 + var didCancelAuthCallbacks = 0 + var didHandleRequest: [Contact] = [] + var didHandleConfirm: [Contact] = [] + var didHandleReset: [Contact] = [] + } + var registeredAuthCallbacks: [AuthCallbacks] = [] + var state = TestState() + var expectedState = state + + var messenger: Messenger = .unimplemented + messenger.registerAuthCallbacks.run = { callbacks in + state.didRegisterAuthCallbacks += 1 + registeredAuthCallbacks.append(callbacks) + return Cancellable { state.didCancelAuthCallbacks += 1 } + } + + let handle = AuthCallbackHandler.live( + messenger: messenger, + handleRequest: .init { state.didHandleRequest.append($0) }, + handleConfirm: .init { state.didHandleConfirm.append($0) }, + handleReset: .init { state.didHandleReset.append($0) } + ) + + var cancellable: Any? = handle(onError: { error in + XCTFail("Unexpected error: \(error)") + }) + + expectedState.didRegisterAuthCallbacks += 1 + XCTAssertNoDifference(state, expectedState) + + let contact1 = XXClient.Contact.unimplemented("1".data(using: .utf8)!) + registeredAuthCallbacks.first?.handle( + .request(contact: contact1, receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + expectedState.didHandleRequest.append(contact1) + XCTAssertNoDifference(state, expectedState) + + let contact2 = XXClient.Contact.unimplemented("2".data(using: .utf8)!) + registeredAuthCallbacks.first?.handle( + .confirm(contact: contact2, receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + expectedState.didHandleConfirm.append(contact2) + XCTAssertNoDifference(state, expectedState) + + let contact3 = XXClient.Contact.unimplemented("3".data(using: .utf8)!) + registeredAuthCallbacks.first?.handle( + .reset(contact: contact3, receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + expectedState.didHandleReset.append(contact3) + XCTAssertNoDifference(state, expectedState) + + cancellable = nil + + expectedState.didCancelAuthCallbacks += 1 + XCTAssertNoDifference(state, expectedState) + + _ = cancellable + } + + func testCallbackHandlingFailure() { + enum Failure: Error, Equatable { + case request + case confirm + case reset + } + var registeredAuthCallbacks: [AuthCallbacks] = [] + var errors: [Error] = [] + + var messenger: Messenger = .unimplemented + messenger.registerAuthCallbacks.run = { callbacks in + registeredAuthCallbacks.append(callbacks) + return Cancellable {} + } + + let handle = AuthCallbackHandler.live( + messenger: messenger, + handleRequest: .init { _ in throw Failure.request }, + handleConfirm: .init { _ in throw Failure.confirm }, + handleReset: .init { _ in throw Failure.reset } + ) + + let cancellable = handle(onError: { errors.append($0) }) + + registeredAuthCallbacks.first?.handle( + .request(contact: .unimplemented(Data()), receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + registeredAuthCallbacks.first?.handle( + .confirm(contact: .unimplemented(Data()), receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + registeredAuthCallbacks.first?.handle( + .reset(contact: .unimplemented(Data()), receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + XCTAssertNoDifference( + errors.map { $0 as? Failure }, + [.request, .confirm, .reset] + ) + + _ = cancellable + } +} diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index 89aef4c9f00a7d887f3b1f6bfead0063ade5f263..f3e5bcf8f787e963820b4a33cd09cb31f4494fd5 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -1,3 +1,4 @@ +import AppCore import ComposableArchitecture import ContactsFeature import RegisterFeature @@ -21,6 +22,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { messengerDidConnect += 1 } @@ -32,10 +34,13 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidConnect, 1) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartUnregistered)) { $0.register = RegisterState() } + + store.send(.authHandler(.stop)) } func testMessengerStartRegistered() { @@ -51,6 +56,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { messengerDidConnect += 1 } @@ -73,11 +79,13 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidConnect, 1) XCTAssertNoDifference(messengerDidLogIn, 1) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartRegistered)) store.receive(.networkMonitor(.start)) store.send(.networkMonitor(.stop)) + store.send(.authHandler(.stop)) } func testRegisterFinished() { @@ -94,6 +102,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isLoggedIn.run = { false } @@ -118,11 +127,13 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidLogIn, 1) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartRegistered)) store.receive(.networkMonitor(.start)) store.send(.networkMonitor(.stop)) + store.send(.authHandler(.stop)) } func testMessengerStartFailure() { @@ -137,14 +148,18 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in throw error } store.send(.messenger(.start)) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } + + store.send(.authHandler(.stop)) } func testMessengerStartConnectFailure() { @@ -159,16 +174,20 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { throw error } store.send(.messenger(.start)) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } + + store.send(.authHandler(.stop)) } func testMessengerStartIsRegisteredFailure() { @@ -183,6 +202,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isLoggedIn.run = { false } @@ -190,10 +210,13 @@ final class HomeFeatureTests: XCTestCase { store.send(.messenger(.start)) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } + + store.send(.authHandler(.stop)) } func testMessengerStartLogInFailure() { @@ -208,6 +231,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isLoggedIn.run = { false } @@ -216,10 +240,13 @@ final class HomeFeatureTests: XCTestCase { store.send(.messenger(.start)) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } + + store.send(.authHandler(.stop)) } func testNetworkMonitorStart() { @@ -491,4 +518,45 @@ final class HomeFeatureTests: XCTestCase { $0.contacts = nil } } + + func testAuthCallbacks() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + var didRunAuthHandler = 0 + var didCancelAuthHandler = 0 + var authHandlerOnError: [AuthCallbackHandler.OnError] = [] + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.authHandler.run = { onError in + didRunAuthHandler += 1 + authHandlerOnError.append(onError) + return Cancellable { didCancelAuthHandler += 1 } + } + + store.send(.authHandler(.start)) + + XCTAssertNoDifference(didRunAuthHandler, 1) + + struct AuthHandlerError: Error { var id: Int } + authHandlerOnError.first?(AuthHandlerError(id: 1)) + + store.receive(.authHandler(.failure(AuthHandlerError(id: 1) as NSError))) { + $0.authFailure = AuthHandlerError(id: 1).localizedDescription + } + + store.send(.authHandler(.failureDismissed)) { + $0.authFailure = nil + } + + store.send(.authHandler(.stop)) + + XCTAssertNoDifference(didCancelAuthHandler, 1) + + authHandlerOnError.first?(AuthHandlerError(id: 2)) + } } diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerRegisterAuthCallbacks.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerRegisterAuthCallbacks.swift index ad184957aae0c17d24a457d77d047506541eb922..3a8b0a5bcb3585ba2b999710a0c572e6e9186a74 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerRegisterAuthCallbacks.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerRegisterAuthCallbacks.swift @@ -19,6 +19,6 @@ extension MessengerRegisterAuthCallbacks { extension MessengerRegisterAuthCallbacks { public static let unimplemented = MessengerRegisterAuthCallbacks( - run: XCTUnimplemented("\(Self.self)") + run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {}) ) }